Merge branch 'master' into 2.9-release

This commit is contained in:
Vivek Maskara 2018-12-19 21:56:21 +05:30 committed by GitHub
commit 189898b6ea
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
232 changed files with 4507 additions and 1322 deletions

View file

@ -1,54 +1,55 @@
language: android
addons:
apt:
packages:
- w3m
- w3m
env:
global:
- ANDROID_TARGET=android-22
- ANDROID_ABI=armeabi-v7a
- ADB_INSTALL_TIMEOUT=12 # in minutes
- ANDROID_TARGET=android-22
- ANDROID_ABI=armeabi-v7a
- ADB_INSTALL_TIMEOUT=12
- secure: okdkna5DaH/2Fay9vI6Enrx7u9UwRm4/IJXvcaWJcvjF3JTsLQr0r+dlMT2X5E1GsNk4WcoGcfZJcVonULkaW4S96B43g3EyevWbLFWjii0cMUO00OshToKyboSvNUf+d5B6rghrbnxTIBNel2ZBFj8MXHdtz6Az20q8VywqPeBZupo7olyKKS1nYdvoo7ypNScVjDGEjEPonWplztYlSDT1w81Vww4kF9oiOPEzDOPw1lOiD8FTyKLXhK0WYlnc3cnyFjZwVMlKcomnFYPfe/J2zO6OP/XInxYSXRkZ6wiOC5gMPYAYanUAuzm91vsTBQMk6jMCglSM9Nl6dPusGgEqOyTwLVALlgvS3km9HNVsHuVJhU+bmJ6scFBWrAOhbsV2ioSEsQ8NgU0Zv1SC0wN9ZruF4ae03Re+k+eHgwA3taZXrT2pvkkSmfRex6oFZReypcPGFQYiHo31NsO39WPRYYxr4edYisVXw75x/BJyOcUULhG1YmwHYYeXOzbNp0Sf9ADtUDi0oip/BO2tkLxbE+z1GJSmC83fX2YpoK+IwDHNm+4w8OJAJBvdxA3Q4HrJBAbd8jnQYP+sBBaki8t5WuwJmfOucx0vgKJ7pzqRY/MOUVe/dACnjLgFDLuS7MMqr6xU/oMM6/rrt4209tL+GQbn/R98UKtmMRRq1hY=
jdk:
- oraclejdk8
- oraclejdk8
android:
components:
- tools
- platform-tools
- build-tools-27.0.3
- extra-google-m2repository
- extra-android-m2repository
- android-22
- android-27
- sys-img-${ANDROID_ABI}-${ANDROID_TARGET}
- tools
- platform-tools
- build-tools-27.0.3
- extra-google-m2repository
- extra-android-m2repository
- android-22
- android-27
- sys-img-${ANDROID_ABI}-${ANDROID_TARGET}
licenses:
- 'android-sdk-license-.+'
- android-sdk-license-.+
before_script:
- echo no | android create avd --force -n test -t $ANDROID_TARGET --abi $ANDROID_ABI
- emulator -avd test -no-audio -no-window -no-boot-anim &
- android-wait-for-emulator
- echo no | android create avd --force -n test -t $ANDROID_TARGET --abi $ANDROID_ABI
- emulator -avd test -no-audio -no-window -no-boot-anim &
- android-wait-for-emulator
script:
- ./gradlew clean check connectedCheck jacocoTestReport
- "./gradlew clean check connectedCheck jacocoTestReport"
- if [ "$TRAVIS_PULL_REQUEST" == "false" ] && [ "$TRAVIS_BRANCH" == "master" ]; then
./gradlew publishProdReleaseApk;
fi
after_success:
- bash <(curl -s https://codecov.io/bash)
- bash <(curl -s https://codecov.io/bash)
after_failure:
- echo '*** Debug Unit Test Results ***'
- w3m -dump ${TRAVIS_BUILD_DIR}/app/build/reports/tests/*/classes/*Test.html
- echo '*** Connected Test Results ***'
- w3m -dump ${TRAVIS_BUILD_DIR}/app/build/reports/androidTests/connected/flavors/*/*Test.html
- echo '*** Debug Unit Test Results ***'
- w3m -dump ${TRAVIS_BUILD_DIR}/app/build/reports/tests/*/classes/*Test.html
- echo '*** Connected Test Results ***'
- w3m -dump ${TRAVIS_BUILD_DIR}/app/build/reports/androidTests/connected/flavors/*/*Test.html
before_cache:
- rm -f $HOME/.gradle/caches/modules-2/modules-2.lock
- rm -fr $HOME/.gradle/caches/*/plugin-resolution/
- rm -f $HOME/.gradle/caches/modules-2/modules-2.lock
- rm -fr $HOME/.gradle/caches/*/plugin-resolution/
cache:
directories:
- $HOME/.gradle/caches/
- $HOME/.gradle/wrapper/
- "$HOME/.gradle/caches/"
- "$HOME/.gradle/wrapper/"
before_install:
- if [ "$TRAVIS_PULL_REQUEST" == "false" ] && [ "$TRAVIS_BRANCH" == "master" ]; then
openssl aes-256-cbc -K $encrypted_7b5c925cc32c_key -iv $encrypted_7b5c925cc32c_iv -in nr-commons.keystore.enc -out nr-commons.keystore -d;
fi
- if [ "$TRAVIS_PULL_REQUEST" == "false" ] && [ "$TRAVIS_BRANCH" == "master" ]; then
openssl aes-256-cbc -K $encrypted_38ac1a5053f6_key -iv $encrypted_38ac1a5053f6_iv -in play.p12.enc -out play.p12 -d;
fi

View file

@ -11,37 +11,13 @@ Initially started by the Wikimedia Foundation, this app is now maintained by gra
## Documentation
We try to have an extensive documentation at [our wiki here at Github][5]:
We try to have an extensive documentation at [our wiki here at Github][4]:
* [User Documentation][6]
* [Contributor Documentation][7]
* [Volunteers Welcome!][9]
* [User Documentation][5]
* [Contributor Documentation][6]
* [Volunteers Welcome!][7]
* [Developer Documentation][8]
## Libraries Used ##
* [Picasso][11]
* [RSS-Parser][12]
* [ViewPagerIndicator][13]
* [PhotoView][14]
* [Acra][15]
* [Renderers][16]
* [Gson][17]
* [Timber][18]
* [Java-String-Similarity][19]
* [ReadMoreTextView][20]
* [MaterialShowcaseView][21]
* [Butterknife][22]
* [OKHttp][23]
* [Okio][24]
* [RxJava][25]
* [JSoup][26]
* [Fresco][27]
* [Stetho][28]
* [Dagger][29]
* [Java-HTTP-Fluent][30]
* [CircleProgressBar][31]
* [Leak Canary][32]
* [Libraries Used][9]
## Contributors ##
@ -60,37 +36,18 @@ Thank you all for your work!
## License ##
This software is open source, licensed under the [Apache License 2.0][4].
This software is open source, licensed under the [Apache License 2.0][10].
[1]: https://play.google.com/store/apps/details?id=fr.free.nrw.commons
[2]: https://commons-app.github.io/
[3]: https://github.com/commons-app/apps-android-commons/issues
[4]: https://www.apache.org/licenses/LICENSE-2.0
[5]: https://github.com/commons-app/apps-android-commons/wiki
[6]: https://github.com/commons-app/apps-android-commons/wiki#user-documentation
[7]: https://github.com/commons-app/apps-android-commons/wiki#contributor-documentation
[4]: https://github.com/commons-app/apps-android-commons/wiki
[5]: https://github.com/commons-app/apps-android-commons/wiki#user-documentation
[6]: https://github.com/commons-app/apps-android-commons/wiki#contributor-documentation
[7]: https://github.com/commons-app/apps-android-commons/wiki/Volunteers-welcome%21
[8]: https://github.com/commons-app/apps-android-commons/wiki#developer-documentation
[9]: https://github.com/commons-app/apps-android-commons/wiki/Volunteers-welcome%21
[10]: https://meta.wikimedia.org/wiki/Grants:Project/Improve_%27Upload_to_Commons%27_Android_App/Renewal
[11]: https://github.com/square/picasso
[13]: https://github.com/avianey/Android-ViewPagerIndicator
[14]: https://github.com/chrisbanes/PhotoView
[15]: https://github.com/ACRA/acra
[16]: https://github.com/pedrovgs/Renderers
[17]: https://github.com/google/gson
[18]: https://github.com/JakeWharton/timber
[19]: https://github.com/tdebatty/java-string-similarity
[20]: https://github.com/bravoborja/ReadMoreTextView
[21]: https://github.com/deano2390/MaterialShowcaseView
[22]: https://github.com/JakeWharton/butterknife
[23]: https://github.com/square/okhttp
[24]: https://github.com/square/okio
[25]: https://github.com/ReactiveX/RxJava
[27]: https://github.com/facebook/fresco
[28]: https://github.com/facebook/stetho
[29]: https://github.com/google/dagger
[30]: https://github.com/yuvipanda/java-http-fluent/blob/master/src/main/java/in/yuvi/http/fluent/Http.java
[31]: https://github.com/dinuscxj/CircleProgressBar
[32]: https://github.com/square/leakcanary
[9]: https://github.com/commons-app/apps-android-commons/wiki/Libraries-used
[10]: https://www.apache.org/licenses/LICENSE-2.0

View file

@ -5,83 +5,90 @@ apply plugin: 'kotlin-kapt'
apply plugin: 'jacoco-android'
apply from: 'quality.gradle'
def isRunningOnTravisAndIsNotPRBuild = System.getenv("CI") == "true" && file('../play.p12').exists()
if(isRunningOnTravisAndIsNotPRBuild) {
apply plugin: 'com.github.triplet.play'
}
dependencies {
// Utils
implementation 'com.github.nicolas-raoul:Quadtree:ac16ea8035bf07'
implementation 'fr.avianey.com.viewpagerindicator:library:2.4.1.1@aar'
implementation 'in.yuvi:http.fluent:1.3'
implementation 'com.github.chrisbanes:PhotoView:2.0.0'
implementation 'ch.acra:acra:4.9.2'
implementation 'org.mediawiki:api:1.3'
implementation 'commons-codec:commons-codec:1.10'
implementation 'com.github.pedrovgs:renderers:3.3.3'
implementation 'com.google.code.gson:gson:2.8.5'
implementation 'com.jakewharton.timber:timber:4.4.0'
implementation 'info.debatty:java-string-similarity:0.24'
implementation 'com.borjabravo:readmoretextview:2.1.0'
implementation 'com.android.support.constraint:constraint-layout:1.1.3'
implementation 'org.slf4j:slf4j-api:1.7.25'
api ("com.github.tony19:logback-android-classic:1.1.1-6") {
exclude group: 'com.google.android', module: 'android'
}
implementation('com.mapbox.mapboxsdk:mapbox-android-sdk:5.5.0@aar') {
transitive = true
}
implementation 'com.github.deano2390:MaterialShowcaseView:1.2.0'
//noinspection GradleCompatible
implementation "com.android.support:support-v4:$SUPPORT_LIB_VERSION"
implementation "com.android.support:appcompat-v7:$SUPPORT_LIB_VERSION"
implementation "com.android.support:design:$SUPPORT_LIB_VERSION"
implementation "com.android.support:customtabs:$SUPPORT_LIB_VERSION"
implementation "com.android.support:cardview-v7:$SUPPORT_LIB_VERSION"
implementation "com.jakewharton:butterknife:$BUTTERKNIFE_VERSION"
kapt "com.jakewharton:butterknife-compiler:$BUTTERKNIFE_VERSION"
implementation 'in.yuvi:http.fluent:1.3'
implementation 'com.squareup.okhttp3:okhttp:3.10.0'
implementation 'com.squareup.okio:okio:1.14.0'
implementation 'io.reactivex.rxjava2:rxandroid:2.1.0'
// Because RxAndroid releases are few and far between, it is recommended you also
// explicitly depend on RxJava's latest version for bug fixes and new features.
implementation 'io.reactivex.rxjava2:rxjava:2.2.0'
implementation 'com.jakewharton.rxbinding2:rxbinding:2.1.1'
implementation 'com.jakewharton.rxbinding2:rxbinding-support-v4:2.1.1'
implementation 'com.jakewharton.rxbinding2:rxbinding-appcompat-v7:2.1.1'
implementation 'com.jakewharton.rxbinding2:rxbinding-design:2.1.1'
implementation 'com.facebook.fresco:fresco:1.10.0'
implementation 'com.facebook.stetho:stetho:1.5.0'
// UI
implementation 'fr.avianey.com.viewpagerindicator:library:2.4.1.1@aar'
implementation 'com.github.chrisbanes:PhotoView:2.0.0'
implementation 'com.github.pedrovgs:renderers:3.3.3'
implementation 'com.mapzen.android:lost:3.0.4'
implementation('com.mapbox.mapboxsdk:mapbox-android-sdk:5.5.0@aar') {
transitive = true
}
implementation 'com.github.deano2390:MaterialShowcaseView:1.2.0'
implementation 'com.dinuscxj:circleprogressbar:1.1.1'
implementation 'com.karumi:dexter:5.0.0'
implementation files('libs/simplemagic-1.9.jar')
implementation "com.jakewharton:butterknife:$BUTTERKNIFE_VERSION"
kapt "com.jakewharton:butterknife-compiler:$BUTTERKNIFE_VERSION"
// Logging
implementation 'ch.acra:acra:4.9.2'
implementation 'com.jakewharton.timber:timber:4.4.0'
implementation 'org.slf4j:slf4j-api:1.7.25'
api ("com.github.tony19:logback-android-classic:1.1.1-6") {
exclude group: 'com.google.android', module: 'android'
}
// Dependency injector
implementation "com.google.dagger:dagger:$DAGGER_VERSION"
implementation "com.google.dagger:dagger-android-support:$DAGGER_VERSION"
kapt "com.google.dagger:dagger-android-processor:$DAGGER_VERSION"
kapt "com.google.dagger:dagger-compiler:$DAGGER_VERSION"
// Unit testing
testImplementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
testImplementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
testImplementation 'junit:junit:4.12'
testImplementation 'org.robolectric:robolectric:3.7.1'
testImplementation 'com.nhaarman:mockito-kotlin:1.5.0'
testImplementation 'com.squareup.okhttp3:mockwebserver:3.10.0'
implementation 'com.dinuscxj:circleprogressbar:1.1.1'
implementation 'com.tspoon.traceur:traceur:1.0.1'
// Android testing
androidTestImplementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
androidTestImplementation 'com.squareup.okhttp3:mockwebserver:3.10.0'
androidTestImplementation "com.android.support:support-annotations:$SUPPORT_LIB_VERSION"
androidTestImplementation 'com.android.support.test:rules:1.0.2'
androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation "com.android.support:support-annotations:$SUPPORT_LIB_VERSION"
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
androidTestImplementation "org.mockito:mockito-core:2.10.0"
// Debugging
implementation 'com.tspoon.traceur:traceur:1.0.1'
implementation 'com.facebook.stetho:stetho:1.5.0'
debugImplementation "com.squareup.leakcanary:leakcanary-android:$LEAK_CANARY"
releaseImplementation "com.squareup.leakcanary:leakcanary-android-no-op:$LEAK_CANARY"
testImplementation "com.squareup.leakcanary:leakcanary-android-no-op:$LEAK_CANARY"
//For handling runtime permissions
implementation 'com.karumi:dexter:5.0.0'
implementation files('libs/simplemagic-1.9.jar')
// Support libraries
implementation "com.android.support:support-v4:$SUPPORT_LIB_VERSION"
implementation "com.android.support:appcompat-v7:$SUPPORT_LIB_VERSION"
implementation "com.android.support:design:$SUPPORT_LIB_VERSION"
implementation "com.android.support:customtabs:$SUPPORT_LIB_VERSION"
implementation "com.android.support:cardview-v7:$SUPPORT_LIB_VERSION"
implementation 'com.android.support.constraint:constraint-layout:1.1.3'
}
android {
@ -101,6 +108,8 @@ android {
}
testOptions {
unitTests.returnDefaultValues = true
unitTests.all {
jvmArgs '-noverify'
}
@ -115,11 +124,18 @@ android {
test.resources.srcDirs += 'src/main/resoures'
}
signingConfigs {
release
}
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
testProguardFile 'test-proguard-rules.txt'
if(isRunningOnTravisAndIsNotPRBuild) {
signingConfig signingConfigs.release
}
}
debug {
minifyEnabled true
@ -129,6 +145,14 @@ android {
versionNameSuffix "-debug-" + getBranchName() + "~" + getBuildVersion()
}
}
if (isRunningOnTravisAndIsNotPRBuild) {
// configure keystore based on env vars in Travis for automated alpha builds
signingConfigs.release.storeFile = file("../nr-commons.keystore")
signingConfigs.release.storePassword = System.getenv("keystore_password")
signingConfigs.release.keyAlias = System.getenv("key_alias")
signingConfigs.release.keyPassword = System.getenv("key_password")
}
flavorDimensions 'tier'
productFlavors {
@ -204,3 +228,17 @@ android {
buildToolsVersion buildToolsVersion
}
if(isRunningOnTravisAndIsNotPRBuild) {
play {
track = "alpha"
userFraction = 1
serviceAccountEmail = System.getenv("SERVICE_ACCOUNT_NAME")
serviceAccountCredentials = file("../play.p12")
resolutionStrategy = "auto"
outputProcessor { // this: ApkVariantOutput
versionNameOverride = "$versionNameOverride.$versionCode"
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#FFFFFF</color>
</resources>

View file

@ -24,7 +24,7 @@
<application
android:name=".CommonsApplication"
android:icon="@drawable/ic_launcher"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:theme="@style/LightAppTheme"
android:supportsRtl="true" >
@ -44,7 +44,7 @@
<activity android:name=".WelcomeActivity" />
<activity android:name=".upload.UploadActivity"
android:icon="@drawable/ic_launcher"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name">
<intent-filter android:label="@string/intent_share_upload_label">
<action android:name="android.intent.action.SEND" />
@ -65,7 +65,7 @@
</activity>
<activity
android:name=".contributions.MainActivity"
android:icon="@drawable/ic_launcher"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" />
<activity
android:name=".settings.SettingsActivity"
@ -102,7 +102,7 @@
<activity
android:name=".explore.SearchActivity"
android:label="@string/title_activity_search"
android:configChanges="orientation|keyboardHidden"
android:configChanges="orientation|keyboardHidden|screenSize"
android:parentActivityName=".contributions.MainActivity"
/>

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View file

@ -0,0 +1,16 @@
package fr.free.nrw.commons;
/**
* Base presenter, enforcing contracts to atach and detach view
*/
public interface BasePresenter {
/**
* Until a view is attached, it is open to listen events from the presenter
*/
void onAttachView(MvpView view);
/**
* Detaching a view makes sure that the view no more receives events from the presenter
*/
void onDetachView();
}

View file

@ -91,6 +91,7 @@ public class CommonsApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
ACRA.init(this);
if (BuildConfig.DEBUG) {
//FIXME: Traceur should be disabled for release builds until error fixed
//See https://github.com/commons-app/apps-android-commons/issues/1877
@ -118,8 +119,7 @@ public class CommonsApplication extends Application {
// Empty temp directory in case some temp files are created and never removed.
ContributionUtils.emptyTemporaryDirectory();
initAcra();
if (BuildConfig.DEBUG) {
if (BuildConfig.DEBUG && !isRoboUnitTest()) {
Stetho.initializeWithDefaults(this);
}
@ -152,14 +152,8 @@ public class CommonsApplication extends Application {
Timber.plant(new Timber.DebugTree());
}
/**
* Remove ACRA's UncaughtExceptionHandler
* We do this because ACRA's handler spawns a new process possibly screwing up with a few things
*/
private void initAcra() {
Thread.UncaughtExceptionHandler exceptionHandler = Thread.getDefaultUncaughtExceptionHandler();
ACRA.init(this);
Thread.setDefaultUncaughtExceptionHandler(exceptionHandler);
public static boolean isRoboUnitTest() {
return "robolectric".equals(Build.FINGERPRINT);
}
private ThreadPoolService getFileLoggingThreadPool() {

View file

@ -0,0 +1,8 @@
package fr.free.nrw.commons;
/**
* Base interface for all the views
*/
public interface MvpView {
void showMessage(String message);
}

View file

@ -2,20 +2,33 @@ package fr.free.nrw.commons;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.support.v4.view.ViewPager;
import android.view.View;
import com.viewpagerindicator.CirclePageIndicator;
import javax.inject.Inject;
import javax.inject.Named;
import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.OnClick;
import butterknife.Optional;
import fr.free.nrw.commons.quiz.QuizActivity;
import fr.free.nrw.commons.theme.BaseActivity;
public class WelcomeActivity extends BaseActivity {
@BindView(R.id.welcomePager) ViewPager pager;
@BindView(R.id.welcomePagerIndicator) CirclePageIndicator indicator;
@Inject
@Named("application_preferences")
SharedPreferences prefs;
@BindView(R.id.welcomePager)
ViewPager pager;
@BindView(R.id.welcomePagerIndicator)
CirclePageIndicator indicator;
private WelcomePagerAdapter adapter = new WelcomePagerAdapter();
private boolean isQuiz;
@ -38,15 +51,20 @@ public class WelcomeActivity extends BaseActivity {
if (bundle != null) {
isQuiz = bundle.getBoolean("isQuiz");
}
} else{
} else {
isQuiz = false;
}
// Enable skip button if beta flavor
if (BuildConfig.FLAVOR == "beta") {
findViewById(R.id.finishTutorialButton).setVisibility(View.VISIBLE);
}
ButterKnife.bind(this);
pager.setAdapter(adapter);
indicator.setViewPager(pager);
adapter.setCallback(this::finish);
adapter.setCallback(this::finishTutorial);
}
/**
@ -54,7 +72,7 @@ public class WelcomeActivity extends BaseActivity {
*/
@Override
public void onDestroy() {
if (isQuiz){
if (isQuiz) {
Intent i = new Intent(WelcomeActivity.this, QuizActivity.class);
startActivity(i);
}
@ -71,4 +89,22 @@ public class WelcomeActivity extends BaseActivity {
Intent welcomeIntent = new Intent(context, WelcomeActivity.class);
context.startActivity(welcomeIntent);
}
/**
* Override onBackPressed() to go to previous tutorial 'pages' if not on first page
*/
@Override
public void onBackPressed() {
if (pager.getCurrentItem() != 0) {
pager.setCurrentItem(pager.getCurrentItem() - 1, true);
} else {
finish();
}
}
@OnClick(R.id.finishTutorialButton)
public void finishTutorial() {
prefs.edit().putBoolean("firstrun", false).apply();
finish();
}
}

View file

@ -14,7 +14,7 @@ import butterknife.OnClick;
import butterknife.Optional;
public class WelcomePagerAdapter extends PagerAdapter {
static final int[] PAGE_LAYOUTS = new int[]{
private static final int[] PAGE_LAYOUTS = new int[]{
R.layout.welcome_wikipedia,
R.layout.welcome_do_upload,
R.layout.welcome_dont_upload,
@ -57,29 +57,31 @@ public class WelcomePagerAdapter extends PagerAdapter {
@Override
public Object instantiateItem(ViewGroup container, int position) {
this.container=container;
this.container = container;
LayoutInflater inflater = LayoutInflater.from(container.getContext());
ViewGroup layout = (ViewGroup) inflater.inflate(PAGE_LAYOUTS[position], container, false);
if (BuildConfig.FLAVOR == "beta") {
TextView textView = layout.findViewById(R.id.welcomeYesButton);
if (textView.getVisibility() != View.VISIBLE) {
textView.setVisibility(View.VISIBLE);
}
ViewHolder holder = new ViewHolder(layout);
layout.setTag(holder);
if (position == PAGE_FINAL){
TextView moreInfo = layout.findViewById(R.id.welcomeInfo);
moreInfo.setText(Html.fromHtml(WelcomeActivity.moreInformation));
ViewHolder holder1 = new ViewHolder(layout);
layout.setTag(holder1);
}
} else {
if (position == PAGE_FINAL) {
ViewHolder holder = new ViewHolder(layout);
layout.setTag(holder);
}
// If final page
if (position == PAGE_FINAL) {
// Add link to more information
TextView moreInfo = layout.findViewById(R.id.welcomeInfo);
moreInfo.setText(Html.fromHtml(WelcomeActivity.moreInformation));
moreInfo.setOnClickListener(view -> {
try {
Utils.handleWebUrl(
container.getContext(),
Uri.parse("https://commons.wikimedia.org/wiki/Help:Contents")
);
} catch (Exception e) {
e.printStackTrace();
}
});
// Handle click of finishTutorialButton ("YES!" button) inside layout
layout.findViewById(R.id.finishTutorialButton)
.setOnClickListener(view -> callback.finishTutorial());
}
container.addView(layout);
return layout;
}
@ -96,33 +98,6 @@ public class WelcomePagerAdapter extends PagerAdapter {
}
public interface Callback {
void onYesClicked();
}
class ViewHolder {
ViewHolder(View view) {
ButterKnife.bind(this, view);
}
/**
* Triggers on click callback on button click
*/
@OnClick(R.id.welcomeYesButton)
void onClicked() {
if (callback != null) {
callback.onYesClicked();
}
}
@Optional
@OnClick(R.id.welcomeInfo)
void onHelpClicked () {
try {
Utils.handleWebUrl(container.getContext(),Uri.parse("https://commons.wikimedia.org/wiki/Help:Contents" ));
} catch (Exception e) {
e.printStackTrace();
}
}
void finishTutorial();
}
}

View file

@ -177,7 +177,6 @@ public class LoginActivity extends AccountAuthenticatorActivity {
super.onResume();
if (prefs.getBoolean("firstrun", true)) {
WelcomeActivity.startYourself(this);
prefs.edit().putBoolean("firstrun", false).apply();
}
if (sessionManager.getCurrentAccount() != null
@ -215,6 +214,7 @@ public class LoginActivity extends AccountAuthenticatorActivity {
loginCurrentlyInProgress = true;
Timber.d("Login to start!");
final String username = canonicializeUsername(usernameEdit.getText().toString());
final String rawUsername = Utils.capitalize(usernameEdit.getText().toString().trim());
final String password = passwordEdit.getText().toString();
String twoFactorCode = twoFactorEdit.getText().toString();
@ -222,7 +222,7 @@ public class LoginActivity extends AccountAuthenticatorActivity {
Observable.fromCallable(() -> login(username, password, twoFactorCode))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(result -> handleLogin(username, password, result));
.subscribe(result -> handleLogin(username, rawUsername, password, result));
}
private String login(String username, String password, String twoFactorCode) {
@ -238,10 +238,10 @@ public class LoginActivity extends AccountAuthenticatorActivity {
}
}
private void handleLogin(String username, String password, String result) {
private void handleLogin(String username, String rawUsername, String password, String result) {
Timber.d("Login done!");
if (result.equals("PASS")) {
handlePassResult(username, password);
handlePassResult(username, rawUsername , password);
} else {
loginCurrentlyInProgress = false;
errorMessageShown = true;
@ -259,7 +259,7 @@ public class LoginActivity extends AccountAuthenticatorActivity {
progressDialog.show();
}
private void handlePassResult(String username, String password) {
private void handlePassResult(String username, String rawUsername, String password) {
showSuccessAndDismissDialog();
requestAuthToken();
AccountAuthenticatorResponse response = null;
@ -276,7 +276,7 @@ public class LoginActivity extends AccountAuthenticatorActivity {
}
}
sessionManager.createAccount(response, username, password);
sessionManager.createAccount(response, username, rawUsername, password);
startMainActivity();
}

View file

@ -28,7 +28,8 @@ public class SessionManager {
private final MediaWikiApi mediaWikiApi;
private Account currentAccount; // Unlike a savings account... ;-)
private SharedPreferences sharedPreferences;
private static final String KEY_RAWUSERNAME = "rawusername";
private Bundle userdata = new Bundle();
public SessionManager(Context context,
MediaWikiApi mediaWikiApi,
@ -44,13 +45,15 @@ public class SessionManager {
*
* @param response
* @param username
* @param rawusername
* @param password
*/
public void createAccount(@Nullable AccountAuthenticatorResponse response,
String username, String password) {
String username, String rawusername, String password) {
Account account = new Account(username, BuildConfig.ACCOUNT_TYPE);
boolean created = accountManager().addAccountExplicitly(account, password, null);
userdata.putString(KEY_RAWUSERNAME, rawusername);
boolean created = accountManager().addAccountExplicitly(account, password, userdata);
Timber.d("account creation " + (created ? "successful" : "failure"));
@ -97,6 +100,17 @@ public class SessionManager {
return account == null ? null : account.name;
}
@Nullable
public String getRawUserName() {
Account account = getCurrentAccount();
return account == null ? null : accountManager().getUserData(account, KEY_RAWUSERNAME);
}
public String getAuthorName(){
return getRawUserName() == null ? getUserName() : getRawUserName();
}
@Nullable
public String getPassword() {
Account account = getCurrentAccount();

View file

@ -34,6 +34,8 @@ import timber.log.Timber;
import static android.app.Activity.RESULT_OK;
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
import static fr.free.nrw.commons.wikidata.WikidataConstants.WIKIDATA_ENTITY_ID_PREF;
import static fr.free.nrw.commons.wikidata.WikidataConstants.WIKIDATA_ITEM_LOCATION;
public class BookmarkLocationsFragment extends DaggerFragment {
@ -136,13 +138,14 @@ public class BookmarkLocationsFragment extends DaggerFragment {
if (resultCode == RESULT_OK) {
Timber.d("OnActivityResult() parameters: Req code: %d Result code: %d Data: %s",
requestCode, resultCode, data);
String wikidataEntityId = directPrefs.getString("WikiDataEntityId", null);
String wikidataEntityId = directPrefs.getString(WIKIDATA_ENTITY_ID_PREF, null);
String wikidataItemLocation = directPrefs.getString(WIKIDATA_ITEM_LOCATION, null);
if (requestCode == ContributionController.SELECT_FROM_CAMERA) {
// If coming from camera, pass null as uri. Because camera photos get saved to a
// fixed directory
contributionController.handleImagePicked(requestCode, null, true, wikidataEntityId);
contributionController.handleImagePicked(requestCode, null, true, wikidataEntityId, wikidataItemLocation);
} else {
contributionController.handleImagePicked(requestCode, data.getData(), true, wikidataEntityId);
contributionController.handleImagePicked(requestCode, data.getData(), true, wikidataEntityId, wikidataItemLocation);
}
} else {
Timber.e("OnActivityResult() parameters: Req code: %d Result code: %d Data: %s",

View file

@ -0,0 +1,55 @@
package fr.free.nrw.commons.campaigns;
import com.google.gson.annotations.SerializedName;
/**
* A data class to hold a campaign
*/
public class Campaign {
@SerializedName("title") private String title;
@SerializedName("description") private String description;
@SerializedName("startDate") private String startDate;
@SerializedName("endDate") private String endDate;
@SerializedName("link") private String link;
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public String getStartDate() {
return startDate;
}
public void setStartDate(String startDate) {
this.startDate = startDate;
}
public String getEndDate() {
return endDate;
}
public void setEndDate(String endDate) {
this.endDate = endDate;
}
public String getLink() {
return link;
}
public void setLink(String link) {
this.link = link;
}
}

View file

@ -0,0 +1,12 @@
package fr.free.nrw.commons.campaigns;
import com.google.gson.annotations.SerializedName;
/**
* A data class to hold the campaign configs
*/
class CampaignConfig {
@SerializedName("showOnlyLiveCampaigns") private boolean showOnlyLiveCampaigns;
@SerializedName("sortBy") private String sortBy;
}

View file

@ -0,0 +1,24 @@
package fr.free.nrw.commons.campaigns;
import com.google.gson.annotations.SerializedName;
import java.util.List;
/**
* Data class to hold the response from the campaigns api
*/
public class CampaignResponseDTO {
@SerializedName("config")
private CampaignConfig campaignConfig;
@SerializedName("campaigns")
private List<Campaign> campaigns;
public CampaignConfig getCampaignConfig() {
return campaignConfig;
}
public List<Campaign> getCampaigns() {
return campaigns;
}
}

View file

@ -0,0 +1,110 @@
package fr.free.nrw.commons.campaigns;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.View;
import android.widget.TextView;
import butterknife.BindView;
import butterknife.ButterKnife;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.contributions.MainActivity;
import fr.free.nrw.commons.utils.SwipableCardView;
import fr.free.nrw.commons.utils.ViewUtil;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* A view which represents a single campaign
*/
public class CampaignView extends SwipableCardView {
Campaign campaign = null;
private ViewHolder viewHolder;
public CampaignView(@NonNull Context context) {
super(context);
init();
}
public CampaignView(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
public CampaignView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
public void setCampaign(Campaign campaign) {
this.campaign = campaign;
if (campaign != null) {
this.setVisibility(View.VISIBLE);
viewHolder.init();
} else {
this.setVisibility(View.GONE);
}
}
@Override public boolean onSwipe(View view) {
view.setVisibility(View.GONE);
((MainActivity) getContext()).prefs.edit()
.putBoolean("displayCampaignsCardView", false)
.apply();
ViewUtil.showLongToast(getContext(),
getResources().getString(R.string.nearby_campaign_dismiss_message));
return true;
}
private void init() {
View rootView = inflate(getContext(), R.layout.layout_campagin, this);
viewHolder = new ViewHolder(rootView);
setOnClickListener(view -> {
if (campaign != null) {
showCampaignInBrowser(campaign.getLink());
}
});
}
/**
* open the url associated with the campaign in the system's default browser
*/
private void showCampaignInBrowser(String link) {
Intent view = new Intent();
view.setAction(Intent.ACTION_VIEW);
view.setData(Uri.parse(link));
getContext().startActivity(view);
}
public class ViewHolder {
@BindView(R.id.tv_title) TextView tvTitle;
@BindView(R.id.tv_description) TextView tvDescription;
@BindView(R.id.tv_dates) TextView tvDates;
public ViewHolder(View itemView) {
ButterKnife.bind(this, itemView);
}
public void init() {
if (campaign != null) {
tvTitle.setText(campaign.getTitle());
tvDescription.setText(campaign.getDescription());
SimpleDateFormat inputDateFormat = new SimpleDateFormat("yyyy-MM-dd");
SimpleDateFormat outputDateFormat = new SimpleDateFormat("dd MMM");
try {
Date startDate = inputDateFormat.parse(campaign.getStartDate());
Date endDate = inputDateFormat.parse(campaign.getEndDate());
tvDates.setText(String.format("%1s - %2s", outputDateFormat.format(startDate),
outputDateFormat.format(endDate)));
} catch (ParseException e) {
e.printStackTrace();
}
}
}
}
}

View file

@ -0,0 +1,101 @@
package fr.free.nrw.commons.campaigns;
import android.util.Log;
import fr.free.nrw.commons.BasePresenter;
import fr.free.nrw.commons.MvpView;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import io.reactivex.Single;
import io.reactivex.SingleObserver;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Collections;
import java.util.Date;
import java.util.List;
/**
* The presenter for the campaigns view, fetches the campaigns from the api and informs the view on
* success and error
*/
public class CampaignsPresenter implements BasePresenter {
private final String TAG = "#CampaignsPresenter#";
private ICampaignsView view;
private MediaWikiApi mediaWikiApi;
private Disposable disposable;
private Campaign campaign;
@Override public void onAttachView(MvpView view) {
this.view = (ICampaignsView) view;
this.mediaWikiApi = ((ICampaignsView) view).getMediaWikiApi();
}
@Override public void onDetachView() {
this.view = null;
disposable.dispose();
}
/**
* make the api call to fetch the campaigns
*/
public void getCampaigns() {
if (view != null && mediaWikiApi != null) {
//If we already have a campaign, lets not make another call
if (this.campaign != null) {
view.showCampaigns(campaign);
return;
}
Single<CampaignResponseDTO> campaigns = mediaWikiApi.getCampaigns();
campaigns.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(Schedulers.io())
.subscribeWith(new SingleObserver<CampaignResponseDTO>() {
@Override public void onSubscribe(Disposable d) {
disposable = d;
}
@Override public void onSuccess(CampaignResponseDTO campaignResponseDTO) {
List<Campaign> campaigns = campaignResponseDTO.getCampaigns();
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
if (campaigns == null || campaigns.isEmpty()) {
Log.e(TAG, "The campaigns list is empty");
view.showCampaigns(null);
}
Collections.sort(campaigns, (campaign, t1) -> {
Date date1, date2;
try {
date1 = dateFormat.parse(campaign.getStartDate());
date2 = dateFormat.parse(t1.getStartDate());
} catch (ParseException e) {
e.printStackTrace();
return -1;
}
return date1.compareTo(date2);
});
Date campaignEndDate, campaignStartDate;
Date currentDate = new Date();
try {
for (Campaign aCampaign : campaigns) {
campaignEndDate = dateFormat.parse(aCampaign.getEndDate());
campaignStartDate =
dateFormat.parse(aCampaign.getStartDate());
if (campaignEndDate.compareTo(currentDate) >= 0
&& campaignStartDate.compareTo(currentDate) <= 0) {
campaign = aCampaign;
break;
}
}
} catch (ParseException e) {
e.printStackTrace();
}
view.showCampaigns(campaign);
}
@Override public void onError(Throwable e) {
Log.e(TAG, "could not fetch campaigns: " + e.getMessage());
}
});
}
}
}

View file

@ -0,0 +1,13 @@
package fr.free.nrw.commons.campaigns;
import fr.free.nrw.commons.MvpView;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
/**
* Interface which defines the view contracts of the campaign view
*/
public interface ICampaignsView extends MvpView {
MediaWikiApi getMediaWikiApi();
void showCampaigns(Campaign campaign);
}

View file

@ -26,9 +26,11 @@ public class CategoryImageUtils {
*/
public static List<Media> getMediaList(NodeList childNodes) {
List<Media> categoryImages = new ArrayList<>();
for (int i = 0; i < childNodes.getLength(); i++) {
Node node = childNodes.item(i);
if (getMediaFromPage(node).getFilename().substring(0,5).equals("File:")){
if (getFileName(node).substring(0, 5).equals("File:")) {
categoryImages.add(getMediaFromPage(node));
}
}
@ -46,7 +48,7 @@ public class CategoryImageUtils {
List<String> subCategories = new ArrayList<>();
for (int i = 0; i < childNodes.getLength(); i++) {
Node node = childNodes.item(i);
subCategories.add(getMediaFromPage(node).getFilename());
subCategories.add(getFileName(node));
}
Collections.sort(subCategories);
return subCategories;

View file

@ -30,6 +30,7 @@ import static fr.free.nrw.commons.contributions.Contribution.SOURCE_CAMERA;
import static fr.free.nrw.commons.contributions.Contribution.SOURCE_GALLERY;
import static fr.free.nrw.commons.upload.UploadService.EXTRA_SOURCE;
import static fr.free.nrw.commons.wikidata.WikidataConstants.WIKIDATA_ENTITY_ID_PREF;
import static fr.free.nrw.commons.wikidata.WikidataConstants.WIKIDATA_ITEM_LOCATION;
public class ContributionController {
@ -131,7 +132,7 @@ public class ContributionController {
}
}
public void handleImagePicked(int requestCode, @Nullable Uri uri, boolean isDirectUpload, String wikiDataEntityId) {
public void handleImagePicked(int requestCode, @Nullable Uri uri, boolean isDirectUpload, String wikiDataEntityId, String wikidateItemLocation) {
FragmentActivity activity = fragment.getActivity();
Timber.d("handleImagePicked() called with onActivityResult(). Boolean isDirectUpload: " + isDirectUpload + "String wikiDataEntityId: " + wikiDataEntityId);
Intent shareIntent = new Intent(activity, UploadActivity.class);
@ -163,6 +164,7 @@ public class ContributionController {
try {
if (wikiDataEntityId != null && !wikiDataEntityId.equals("")) {
shareIntent.putExtra(WIKIDATA_ENTITY_ID_PREF, wikiDataEntityId);
shareIntent.putExtra(WIKIDATA_ITEM_LOCATION, wikidateItemLocation);
}
} catch (SecurityException e) {
Timber.e(e, "Security Exception");

View file

@ -14,6 +14,7 @@ import android.os.Bundle;
import android.os.IBinder;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentTransaction;
@ -22,6 +23,8 @@ import android.support.v4.content.Loader;
import android.support.v4.app.LoaderManager;
import android.support.v4.widget.CursorAdapter;
import android.support.v7.app.AlertDialog;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@ -30,6 +33,14 @@ import android.widget.AdapterView;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.Toast;
import butterknife.BindView;
import butterknife.ButterKnife;
import fr.free.nrw.commons.campaigns.Campaign;
import fr.free.nrw.commons.campaigns.CampaignResponseDTO;
import fr.free.nrw.commons.campaigns.CampaignView;
import fr.free.nrw.commons.campaigns.CampaignsPresenter;
import fr.free.nrw.commons.campaigns.ICampaignsView;
import java.util.ArrayList;
import java.util.concurrent.CountDownLatch;
@ -60,6 +71,7 @@ import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;
import org.acra.util.ToastSender;
import timber.log.Timber;
import static fr.free.nrw.commons.contributions.Contribution.STATE_FAILED;
@ -76,7 +88,7 @@ public class ContributionsFragment
MediaDetailPagerFragment.MediaDetailProvider,
FragmentManager.OnBackStackChangedListener,
ContributionsListFragment.SourceRefresher,
LocationUpdateListener
LocationUpdateListener,ICampaignsView
{
@Inject
@Named("default_preferences")
@ -112,6 +124,10 @@ public class ContributionsFragment
private boolean isFragmentAttachedBefore = false;
private View checkBoxView;
private CheckBox checkBox;
private CampaignsPresenter presenter;
@BindView(R.id.campaigns_view) CampaignView campaignView;
/**
* Since we will need to use parent activity on onAuthCookieAcquired, we have to wait
@ -142,6 +158,10 @@ public class ContributionsFragment
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_contributions, container, false);
ButterKnife.bind(this, view);
presenter = new CampaignsPresenter();
presenter.onAttachView(this);
campaignView.setVisibility(View.GONE);
nearbyNoificationCardView = view.findViewById(R.id.card_view_nearby);
checkBoxView = View.inflate(getActivity(), R.layout.nearby_permission_dialog, null);
checkBox = (CheckBox) checkBoxView.findViewById(R.id.never_ask_again);
@ -173,6 +193,27 @@ public class ContributionsFragment
setUploadCount();
}
getChildFragmentManager().registerFragmentLifecycleCallbacks(
new FragmentManager.FragmentLifecycleCallbacks() {
@Override public void onFragmentResumed(FragmentManager fm, Fragment f) {
super.onFragmentResumed(fm, f);
//If media detail pager fragment is visible, hide the campaigns view [might not be the best way to do, this but yeah, this proves to work for now]
Log.e("#CF#", "onFragmentResumed" + f.getClass().getName());
if (f instanceof MediaDetailPagerFragment) {
campaignView.setVisibility(View.GONE);
}
}
@Override public void onFragmentDetached(FragmentManager fm, Fragment f) {
super.onFragmentDetached(fm, f);
Log.e("#CF#", "onFragmentDetached" + f.getClass().getName());
//If media detail pager fragment is detached, ContributionsList fragment is gonna be visible, [becomes tightly coupled though]
if (f instanceof MediaDetailPagerFragment) {
fetchCampaigns();
}
}
}, true);
return view;
}
@ -537,7 +578,7 @@ public class ContributionsFragment
nearbyNoificationCardView.setVisibility(View.GONE);
}
fetchCampaigns();
}
/**
@ -622,7 +663,7 @@ public class ContributionsFragment
curLatLng = locationManager.getLastLocation();
placesDisposable = Observable.fromCallable(() -> nearbyController
.loadAttractionsFromLocation(curLatLng, true)) // thanks to boolean, it will only return closest result
.loadAttractionsFromLocation(curLatLng, curLatLng, true, false)) // thanks to boolean, it will only return closest result
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(this::updateNearbyNotification,
@ -694,5 +735,38 @@ public class ContributionsFragment
// Update closest nearby card view if location changed more than 500 meters
updateClosestNearbyCardViewInfo();
}
@Override public void onViewCreated(@NonNull View view,
@Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
}
/**
* ask the presenter to fetch the campaigns only if user has not manually disabled it
*/
private void fetchCampaigns() {
if (prefs.getBoolean("displayCampaignsCardView", true)) {
presenter.getCampaigns();
}
}
@Override public void showMessage(String message) {
Toast.makeText(getContext(), message, Toast.LENGTH_SHORT).show();
}
@Override public MediaWikiApi getMediaWikiApi() {
return mediaWikiApi;
}
@Override public void showCampaigns(Campaign campaign) {
if (campaign != null) {
campaignView.setCampaign(campaign);
}
}
@Override public void onDestroyView() {
super.onDestroyView();
presenter.onDetachView();
}
}

View file

@ -255,11 +255,11 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment {
if (requestCode == ContributionController.SELECT_FROM_CAMERA) {
// If coming from camera, pass null as uri. Because camera photos get saved to a
// fixed directory
controller.handleImagePicked(requestCode, null, false, null);
controller.handleImagePicked(requestCode, null, false, null, null);
} else if (requestCode == ContributionController.PICK_IMAGE_MULTIPLE) {
handleMultipleImages(requestCode, data);
} else if (requestCode == ContributionController.SELECT_FROM_GALLERY){
controller.handleImagePicked(requestCode, data.getData(), false, null);
controller.handleImagePicked(requestCode, data.getData(), false, null, null);
}
} else {
Timber.e("OnActivityResult() parameters: Req code: %d Result code: %d Data: %s",
@ -319,7 +319,7 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment {
Log.v("LOG_TAG", "Selected Images" + mArrayUri.size());
controller.handleImagesPicked(requestCode, mArrayUri);
} else if(data.getData() != null) {
controller.handleImagePicked(SELECT_FROM_GALLERY, data.getData(), false, null);
controller.handleImagePicked(SELECT_FROM_GALLERY, data.getData(), false, null, null);
}
}

View file

@ -0,0 +1,101 @@
package fr.free.nrw.commons.delete;
import android.accounts.Account;
import android.content.Context;
import android.util.Log;
import com.google.gson.JsonObject;
import org.json.JSONException;
import org.json.JSONObject;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import javax.inject.Inject;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.achievements.FeedbackResponse;
import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import fr.free.nrw.commons.utils.ViewUtil;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.schedulers.Schedulers;
import timber.log.Timber;
public class ReasonBuilder {
private SessionManager sessionManager;
private MediaWikiApi mediaWikiApi;
private CompositeDisposable compositeDisposable = new CompositeDisposable();
private String reason;
private Context context;
private Media media;
public ReasonBuilder(String reason,
Context context,
Media media,
SessionManager sessionManager,
MediaWikiApi mediaWikiApi){
this.reason = reason;
this.context = context;
this.media = media;
this.sessionManager = sessionManager;
this.mediaWikiApi = mediaWikiApi;
}
private String prettyUploadedDate(Media media) {
Date date = media.getDateUploaded();
if (date == null || date.toString() == null || date.toString().isEmpty()) {
return "Uploaded date not available";
}
SimpleDateFormat formatter = new SimpleDateFormat("dd MMM yyyy", Locale.getDefault());
return formatter.format(date);
}
private void fetchArticleNumber() {
if (checkAccount()) {
compositeDisposable.add(mediaWikiApi
.getAchievements(sessionManager.getCurrentAccount().name)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
jsonObject -> appendArticlesUsed(jsonObject),
t -> Timber.e(t, "Fetching achievements statistics failed")
));
}
}
private void appendArticlesUsed(FeedbackResponse object){
reason += context.getString(R.string.uploaded_by_myself).toString() + prettyUploadedDate(media);
reason += context.getString(R.string.used_by).toString()
+ object.getArticlesUsingImages()
+ context.getString(R.string.articles).toString();
Log.i("New Reason", reason);
}
public String getReason(){
fetchArticleNumber();
return reason;
}
/**
* check to ensure that user is logged in
* @return
*/
private boolean checkAccount(){
Account currentAccount = sessionManager.getCurrentAccount();
if(currentAccount == null) {
Timber.d("Current account is null");
ViewUtil.showLongToast(context, context.getResources().getString(R.string.user_not_logged_in));
sessionManager.forceLogin(context);
return false;
}
return true;
}
}

View file

@ -3,6 +3,7 @@ package fr.free.nrw.commons.explore;
import android.content.res.Configuration;
import android.database.DataSetObserver;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.support.design.widget.TabLayout;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
@ -57,9 +58,16 @@ public class SearchActivity extends NavigationBaseActivity implements MediaDetai
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
boolean currentThemeIsDark = PreferenceManager.getDefaultSharedPreferences(this).getBoolean("theme", false);
setContentView(R.layout.activity_search);
ButterKnife.bind(this);
initDrawer();
if (currentThemeIsDark) {
searchView.setBackgroundResource(R.color.vpi__bright_foreground_disabled_holo_dark);
tabLayout.setBackgroundResource(R.color.vpi__bright_foreground_disabled_holo_dark);
toolbar.setBackgroundResource(R.color.vpi__bright_foreground_disabled_holo_dark);
viewPager.setBackgroundResource(R.color.vpi__bright_foreground_disabled_holo_dark);
}
setTitle(getString(R.string.title_activity_search));
toolbar.setNavigationOnClickListener(v->onBackPressed());
supportFragmentManager = getSupportFragmentManager();
@ -93,9 +101,9 @@ public class SearchActivity extends NavigationBaseActivity implements MediaDetai
searchImageFragment = new SearchImageFragment();
searchCategoryFragment= new SearchCategoryFragment();
fragmentList.add(searchImageFragment);
titleList.add("MEDIA");
titleList.add(getResources().getString(R.string.search_tab_title_media));
fragmentList.add(searchCategoryFragment);
titleList.add("CATEGORIES");
titleList.add(getResources().getString(R.string.search_tab_title_categories));
viewPagerAdapter.setTabData(fragmentList, titleList);
viewPagerAdapter.notifyDataSetChanged();

View file

@ -1,6 +1,8 @@
package fr.free.nrw.commons.explore.recentsearches;
import android.content.Context;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.support.v7.app.AlertDialog;
import android.view.LayoutInflater;
import android.view.View;
@ -8,6 +10,7 @@ import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;
import java.util.List;
@ -31,6 +34,9 @@ public class RecentSearchesFragment extends CommonsDaggerSupportFragment {
ArrayAdapter adapter;
@BindView(R.id.recent_searches_delete_button)
ImageView recent_searches_delete_button;
boolean currentThemeIsDark = false;
@BindView(R.id.recent_searches_text_view)
TextView recent_searches_text_view;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
@ -38,21 +44,33 @@ public class RecentSearchesFragment extends CommonsDaggerSupportFragment {
View rootView = inflater.inflate(R.layout.fragment_search_history, container, false);
ButterKnife.bind(this, rootView);
recentSearches = recentSearchesDao.recentSearches(10);
recent_searches_delete_button.setOnClickListener(v -> new AlertDialog.Builder(getContext())
.setMessage(getString(R.string.delete_recent_searches_dialog))
.setPositiveButton(android.R.string.yes, (dialog, which) -> {
recentSearchesDao.deleteAll(recentSearches);
Toast.makeText(getContext(),getString(R.string.search_history_deleted),Toast.LENGTH_SHORT).show();
recentSearches = recentSearchesDao.recentSearches(10);
adapter = new ArrayAdapter<String>(getContext(),R.layout.item_recent_searches, recentSearches);
recentSearchesList.setAdapter(adapter);
adapter.notifyDataSetChanged();
dialog.dismiss();
})
.setNegativeButton(android.R.string.no, null)
.create()
.show());
adapter = new ArrayAdapter<String>(getContext(),R.layout.item_recent_searches, recentSearches);
if(recentSearches.isEmpty()) {
recent_searches_delete_button.setVisibility(View.GONE);
recent_searches_text_view.setText(R.string.no_recent_searches);
}
recent_searches_delete_button.setOnClickListener(v -> {
new AlertDialog.Builder(getContext())
.setMessage(getString(R.string.delete_recent_searches_dialog))
.setPositiveButton(android.R.string.yes, (dialog, which) -> {
recentSearchesDao.deleteAll(recentSearches);
recent_searches_delete_button.setVisibility(View.GONE);
recent_searches_text_view.setText(R.string.no_recent_searches);
Toast.makeText(getContext(),getString(R.string.search_history_deleted),Toast.LENGTH_SHORT).show();
recentSearches = recentSearchesDao.recentSearches(10);
adapter = new ArrayAdapter<String>(getContext(),R.layout.item_recent_searches, recentSearches);
recentSearchesList.setAdapter(adapter);
adapter.notifyDataSetChanged();
dialog.dismiss();
})
.setNegativeButton(android.R.string.no, null)
.create()
.show();
});
currentThemeIsDark = PreferenceManager.getDefaultSharedPreferences(getContext()).getBoolean("theme", false);
setAdapterForThemes(getContext(), currentThemeIsDark);
recentSearchesList.setAdapter(adapter);
recentSearchesList.setOnItemClickListener((parent, view, position, id) -> (
(SearchActivity)getContext()).updateText(recentSearches.get(position)));
@ -76,8 +94,21 @@ public class RecentSearchesFragment extends CommonsDaggerSupportFragment {
*/
public void updateRecentSearches() {
recentSearches = recentSearchesDao.recentSearches(10);
adapter = new ArrayAdapter<String>(getContext(),R.layout.item_recent_searches, recentSearches);
setAdapterForThemes(getContext(), currentThemeIsDark);
recentSearchesList.setAdapter(adapter);
adapter.notifyDataSetChanged();
if(!recentSearches.isEmpty()) {
recent_searches_delete_button.setVisibility(View.VISIBLE);
recent_searches_text_view.setText(R.string.search_recent_header);
}
}
private void setAdapterForThemes(Context context, boolean currentThemeIsDark) {
if (currentThemeIsDark) {
adapter = new ArrayAdapter<String>(context, R.layout.item_recent_searches_dark_theme, recentSearches);
} else {
adapter = new ArrayAdapter<String>(context, R.layout.item_recent_searches, recentSearches);
}
}
}

View file

@ -14,15 +14,18 @@ import android.text.Editable;
import android.text.Html;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.util.Log;
import android.util.TypedValue;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.ScrollView;
import android.widget.Spinner;
import android.widget.TextView;
import android.widget.Toast;
@ -44,9 +47,11 @@ import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.MediaDataExtractor;
import fr.free.nrw.commons.MediaWikiImageView;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.category.CategoryDetailsActivity;
import fr.free.nrw.commons.contributions.ContributionsFragment;
import fr.free.nrw.commons.delete.DeleteTask;
import fr.free.nrw.commons.delete.ReasonBuilder;
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
import fr.free.nrw.commons.location.LatLng;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
@ -65,6 +70,8 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
private MediaDetailPagerFragment.MediaDetailProvider detailProvider;
private int index;
private Locale locale;
private boolean isDeleted = false;
public static MediaDetailFragment forMedia(int index, boolean editable, boolean isCategoryImage) {
MediaDetailFragment mf = new MediaDetailFragment();
@ -85,6 +92,8 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
Provider<MediaDataExtractor> mediaDataExtractorProvider;
@Inject
MediaWikiApi mwApi;
@Inject
SessionManager sessionManager;
private int initialListTop = 0;
@ -128,6 +137,8 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
//Had to make this class variable, to implement various onClicks, which access the media, also I fell why make separate variables when one can serve the purpose
private Media media;
private ArrayList<String> reasonList;
@Override
public void onSaveInstanceState(Bundle outState) {
@ -163,6 +174,13 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
initialListTop = 0;
}
reasonList = new ArrayList<>();
reasonList.add(getString(R.string.deletion_reason_uploaded_by_mistake));
reasonList.add(getString(R.string.deletion_reason_publicly_visible));
reasonList.add(getString(R.string.deletion_reason_not_interesting));
reasonList.add(getString(R.string.deletion_reason_no_longer_want_public));
reasonList.add(getString(R.string.deletion_reason_bad_for_my_privacy));
categoryNames = new ArrayList<>();
categoryNames.add(getString(R.string.detail_panel_cats_loading));
@ -379,48 +397,43 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
@OnClick(R.id.nominateDeletion)
public void onDeleteButtonClicked(){
//Reviewer correct me if i have misunderstood something over here
//But how does this if (delete.getVisibility() == View.VISIBLE) {
// enableDeleteButton(true); makes sense ?
AlertDialog.Builder alert = new AlertDialog.Builder(getActivity());
alert.setMessage("Why should this file be deleted?");
final EditText input = new EditText(getActivity());
alert.setView(input);
input.requestFocus();
alert.setPositiveButton(R.string.ok, (dialog, whichButton) -> {
String reason = input.getText().toString();
DeleteTask deleteTask = new DeleteTask(getActivity(), media, reason);
deleteTask.execute();
enableDeleteButton(false);
});
alert.setNegativeButton(R.string.cancel, (dialog, whichButton) -> {
});
AlertDialog d = alert.create();
input.addTextChangedListener(new TextWatcher() {
private void handleText() {
final Button okButton = d.getButton(AlertDialog.BUTTON_POSITIVE);
if (input.getText().length() == 0) {
okButton.setEnabled(false);
} else {
okButton.setEnabled(true);
}
}
final ArrayAdapter<String> languageAdapter = new ArrayAdapter<String>(getActivity(),
R.layout.simple_spinner_dropdown_list, reasonList);
final Spinner spinner = new Spinner(getActivity());
spinner.setLayoutParams(new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT));
spinner.setAdapter(languageAdapter);
spinner.setGravity(17);
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder.setView(spinner);
builder.setTitle(R.string.nominate_delete)
.setPositiveButton(R.string.about_translate_proceed, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
String reason = spinner.getSelectedItem().toString();
ReasonBuilder reasonBuilder = new ReasonBuilder(reason,
getActivity(),
media,
sessionManager,
mwApi);
reason = reasonBuilder.getReason();
DeleteTask deleteTask = new DeleteTask(getActivity(), media, reason);
deleteTask.execute();
isDeleted = true;
enableDeleteButton(false);
}
});
builder.setNegativeButton(R.string.about_translate_cancel, new DialogInterface.OnClickListener() {
@Override
public void afterTextChanged(Editable arg0) {
handleText();
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
}
});
d.show();
d.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false);
AlertDialog dialog = builder.create();
dialog.show();
if(isDeleted) {
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false);
}
}
@OnClick(R.id.seeMore)
@ -442,8 +455,19 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
private void rebuildCatList() {
categoryContainer.removeAllViews();
// @fixme add the category items
for (String cat : categoryNames) {
View catLabel = buildCatLabel(cat, categoryContainer);
//As per issue #1826(see https://github.com/commons-app/apps-android-commons/issues/1826), some categories come suffixed with strings prefixed with |. As per the discussion
//that was meant for alphabetical sorting of the categories and can be safely removed.
for (int i = 0; i < categoryNames.size(); i++) {
String categoryName = categoryNames.get(i);
//Removed everything after '|'
int indexOfPipe = categoryName.indexOf('|');
if (indexOfPipe != -1) {
categoryName = categoryName.substring(0, indexOfPipe);
//Set the updated category to the list as well
categoryNames.set(i, categoryName);
}
View catLabel = buildCatLabel(categoryName, categoryContainer);
categoryContainer.addView(catLabel);
}
}

View file

@ -343,7 +343,7 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple
@Override
public void onPageScrolled(int i, float v, int i2) {
if (getActivity() == null) {
if(getActivity() == null) {
Timber.d("Returning as activity is destroyed!");
return;
}
@ -398,7 +398,7 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple
public Fragment getItem(int i) {
if (i == 0) {
// See bug https://code.google.com/p/android/issues/detail?id=27526
if (getActivity() == null) {
if(getActivity() == null) {
Timber.d("Skipping getItem. Returning as activity is destroyed!");
return null;
}

View file

@ -11,6 +11,7 @@ import android.text.TextUtils;
import com.google.gson.Gson;
import fr.free.nrw.commons.campaigns.CampaignResponseDTO;
import org.apache.http.HttpResponse;
import org.apache.http.conn.ClientConnectionManager;
import org.apache.http.conn.scheme.PlainSocketFactory;
@ -77,6 +78,8 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
private SharedPreferences categoryPreferences;
private Gson gson;
private final OkHttpClient okHttpClient;
private final String WIKIMEDIA_CAMPAIGNS_BASE_URL =
"https://raw.githubusercontent.com/commons-app/campaigns/master/campaigns.json";
public ApacheHttpClientMediaWikiApi(Context context,
String apiURL,
@ -1056,4 +1059,18 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
}
}
@Override public Single<CampaignResponseDTO> getCampaigns() {
return Single.fromCallable(() -> {
Request request = new Request.Builder().url(WIKIMEDIA_CAMPAIGNS_BASE_URL).build();
Response response = okHttpClient.newCall(request).execute();
if (response != null && response.body() != null && response.isSuccessful()) {
String json = response.body().string();
if (json == null) {
return null;
}
return gson.fromJson(json, CampaignResponseDTO.class);
}
return null;
});
}
}

View file

@ -4,6 +4,7 @@ import android.net.Uri;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import fr.free.nrw.commons.campaigns.CampaignResponseDTO;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
@ -105,6 +106,8 @@ public interface MediaWikiApi {
void logout();
Single<CampaignResponseDTO> getCampaigns();
interface ProgressListener {
void onProgress(long transferred, long total);
}

View file

@ -5,6 +5,7 @@ import android.content.SharedPreferences;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.support.graphics.drawable.VectorDrawableCompat;
import android.util.Log;
import com.mapbox.mapboxsdk.annotations.IconFactory;
@ -31,6 +32,8 @@ public class NearbyController {
private static final int MAX_RESULTS = 1000;
private final NearbyPlaces nearbyPlaces;
private final SharedPreferences prefs;
public static double searchedRadius = 10.0; //in kilometers
public static LatLng currentLocation;
@Inject
public NearbyController(NearbyPlaces nearbyPlaces,
@ -44,18 +47,21 @@ public class NearbyController {
* Prepares Place list to make their distance information update later.
*
* @param curLatLng current location for user
* @param latLangToSearchAround the location user wants to search around
* @param returnClosestResult if this search is done to find closest result or all results
* @return NearbyPlacesInfo a variable holds Place list without distance information
* and boundary coordinates of current Place List
*/
public NearbyPlacesInfo loadAttractionsFromLocation(LatLng curLatLng, boolean returnClosestResult) throws IOException {
Timber.d("Loading attractions near %s", curLatLng);
public NearbyPlacesInfo loadAttractionsFromLocation(LatLng curLatLng, LatLng latLangToSearchAround, boolean returnClosestResult, boolean checkingAroundCurrentLocation) throws IOException {
Timber.d("Loading attractions near %s", latLangToSearchAround);
NearbyPlacesInfo nearbyPlacesInfo = new NearbyPlacesInfo();
if (curLatLng == null) {
if (latLangToSearchAround == null) {
Timber.d("Loading attractions neari, but curLatLng is null");
return null;
}
List<Place> places = nearbyPlaces.getFromWikidataQuery(curLatLng, Locale.getDefault().getLanguage(), returnClosestResult);
List<Place> places = nearbyPlaces.getFromWikidataQuery(latLangToSearchAround, Locale.getDefault().getLanguage(), returnClosestResult);
if (null != places && places.size() > 0) {
LatLng[] boundaryCoordinates = {places.get(0).location, // south
@ -93,6 +99,11 @@ public class NearbyController {
}
nearbyPlacesInfo.placeList = places;
nearbyPlacesInfo.boundaryCoordinates = boundaryCoordinates;
if (!returnClosestResult && checkingAroundCurrentLocation) {
// Do not update searched radius, if controller is used for nearby card notification
searchedRadius = nearbyPlaces.radius;
currentLocation = curLatLng;
}
return nearbyPlacesInfo;
}
else {

View file

@ -14,6 +14,7 @@ import android.support.design.widget.BottomSheetBehavior;
import android.support.design.widget.Snackbar;
import android.support.v4.app.FragmentTransaction;
import android.support.v7.app.AlertDialog;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@ -64,8 +65,6 @@ public class NearbyFragment extends CommonsDaggerSupportFragment
LinearLayout bottomSheetDetails;
@BindView(R.id.transparentView)
View transparentView;
@BindView(R.id.fab_recenter)
View fabRecenter;
@Inject
LocationServiceManager locationManager;
@ -87,16 +86,19 @@ public class NearbyFragment extends CommonsDaggerSupportFragment
private LatLng curLatLng;
private Disposable placesDisposable;
private Disposable placesDisposableCustom;
private boolean lockNearbyView; //Determines if the nearby places needs to be refreshed
public View view;
private Snackbar snackbar;
private LatLng lastKnownLocation;
private LatLng customLatLng;
private final String NETWORK_INTENT_ACTION = "android.net.conn.CONNECTIVITY_CHANGE";
private BroadcastReceiver broadcastReceiver;
private boolean onOrientationChanged = false;
private boolean populateForCurrentLocation = false;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
@ -215,24 +217,27 @@ public class NearbyFragment extends CommonsDaggerSupportFragment
@Override
public void onLocationChangedSignificantly(LatLng latLng) {
refreshView(LOCATION_SIGNIFICANTLY_CHANGED);
refreshView(LOCATION_SIGNIFICANTLY_CHANGED);
}
@Override
public void onLocationChangedSlightly(LatLng latLng) {
refreshView(LOCATION_SLIGHTLY_CHANGED);
refreshView(LOCATION_SLIGHTLY_CHANGED);
}
@Override
public void onLocationChangedMedium(LatLng latLng) {
// For nearby map actions, there are no differences between 500 meter location change (aka medium change) and slight change
refreshView(LOCATION_SLIGHTLY_CHANGED);
refreshView(LOCATION_SLIGHTLY_CHANGED);
}
@Override
public void onWikidataEditSuccessful() {
refreshView(MAP_UPDATED);
// Do not refresh nearby map if we are checking other areas with search this area button
if (!nearbyMapFragment.searchThisAreaModeOn) {
refreshView(MAP_UPDATED);
}
}
/**
@ -240,7 +245,7 @@ public class NearbyFragment extends CommonsDaggerSupportFragment
*
* @param locationChangeType defines if location shanged significantly or slightly
*/
private void refreshView(LocationServiceManager.LocationChangeType locationChangeType) {
public void refreshView(LocationServiceManager.LocationChangeType locationChangeType) {
Timber.d("Refreshing nearby places");
if (lockNearbyView) {
return;
@ -256,9 +261,11 @@ public class NearbyFragment extends CommonsDaggerSupportFragment
if (curLatLng != null && curLatLng.equals(lastLocation)
&& !locationChangeType.equals(MAP_UPDATED)) { //refresh view only if location has changed
// Two exceptional cases to refresh nearby map manually.
if (!onOrientationChanged) {
return;
}
}
curLatLng = lastLocation;
@ -291,7 +298,7 @@ public class NearbyFragment extends CommonsDaggerSupportFragment
bundle.putString("CurLatLng", gsonCurLatLng);
placesDisposable = Observable.fromCallable(() -> nearbyController
.loadAttractionsFromLocation(curLatLng, false))
.loadAttractionsFromLocation(curLatLng, curLatLng, false, true))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(this::populatePlaces,
@ -300,6 +307,7 @@ public class NearbyFragment extends CommonsDaggerSupportFragment
showErrorMessage(getString(R.string.error_fetching_nearby_places));
progressBar.setVisibility(View.GONE);
});
} else if (locationChangeType
.equals(LOCATION_SLIGHTLY_CHANGED)) {
Gson gson = new GsonBuilder()
@ -307,7 +315,62 @@ public class NearbyFragment extends CommonsDaggerSupportFragment
.create();
String gsonCurLatLng = gson.toJson(curLatLng);
bundle.putString("CurLatLng", gsonCurLatLng);
updateMapFragment(true);
updateMapFragment(false,true, null, null);
}
if (nearbyMapFragment != null) {
nearbyMapFragment.searchThisAreaButton.setVisibility(View.GONE);
}
}
/**
* This method should be used with "Search this are button". This method will search nearby
* points around any custom location (target location when user clicked on search this area)
* button. It populates places for custom location.
* @param customLatLng Custom area which we will search around
*/
public void refreshViewForCustomLocation(LatLng customLatLng, boolean refreshForCurrentLocation) {
if (customLatLng == null) {
// If null, return
return;
}
populateForCurrentLocation = refreshForCurrentLocation;
this.customLatLng = customLatLng;
placesDisposableCustom = Observable.fromCallable(() -> nearbyController
.loadAttractionsFromLocation(curLatLng, customLatLng, false, populateForCurrentLocation))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(this::populatePlacesFromCustomLocation,
throwable -> {
Timber.d(throwable);
showErrorMessage(getString(R.string.error_fetching_nearby_places));
});
if (nearbyMapFragment != null) {
nearbyMapFragment.searchThisAreaButton.setVisibility(View.GONE);
}
}
/**
* Populates places for custom location, should be used for finding nearby places around a
* location where you are not at.
* @param nearbyPlacesInfo This variable has place list information and distances.
*/
private void populatePlacesFromCustomLocation(NearbyController.NearbyPlacesInfo nearbyPlacesInfo) {
//NearbyMapFragment nearbyMapFragment = getMapFragment();
if (nearbyMapFragment != null) {
nearbyMapFragment.searchThisAreaButtonProgressBar.setVisibility(View.GONE);
}
if (nearbyMapFragment != null && curLatLng != null) {
if (!populateForCurrentLocation) {
nearbyMapFragment.updateMapSignificantlyForCustomLocation(customLatLng, nearbyPlacesInfo.placeList);
} else {
updateMapFragment(true,true, customLatLng, nearbyPlacesInfo);
}
updateListFragmentForCustomLocation(nearbyPlacesInfo.placeList);
}
}
@ -341,7 +404,7 @@ public class NearbyFragment extends CommonsDaggerSupportFragment
} else {
// There are fragments, just update the map and list
Timber.d("Map fragment already exists, just update the map and list");
updateMapFragment(false);
updateMapFragment(false,false, null, null);
updateListFragment();
}
}
@ -363,7 +426,11 @@ public class NearbyFragment extends CommonsDaggerSupportFragment
}
}
private void updateMapFragment(boolean isSlightUpdate) {
private void updateMapFragment(boolean updateViaButton, boolean isSlightUpdate, @Nullable LatLng customLatLng, @Nullable NearbyController.NearbyPlacesInfo nearbyPlacesInfo) {
if (nearbyMapFragment.searchThisAreaModeOn) {
return;
}
/*
Significant update means updating nearby place markers. Slightly update means only
updating current location marker and camera target.
@ -379,14 +446,14 @@ public class NearbyFragment extends CommonsDaggerSupportFragment
* If we are close to nearby places boundaries, we need a significant update to
* get new nearby places. Check order is south, north, west, east
* */
if (nearbyMapFragment.boundaryCoordinates != null
if (nearbyMapFragment.boundaryCoordinates != null && !nearbyMapFragment.searchThisAreaModeOn
&& (curLatLng.getLatitude() <= nearbyMapFragment.boundaryCoordinates[0].getLatitude()
|| curLatLng.getLatitude() >= nearbyMapFragment.boundaryCoordinates[1].getLatitude()
|| curLatLng.getLongitude() <= nearbyMapFragment.boundaryCoordinates[2].getLongitude()
|| curLatLng.getLongitude() >= nearbyMapFragment.boundaryCoordinates[3].getLongitude())) {
// populate places
placesDisposable = Observable.fromCallable(() -> nearbyController
.loadAttractionsFromLocation(curLatLng, false))
.loadAttractionsFromLocation(curLatLng, curLatLng, false, updateViaButton))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(this::populatePlaces,
@ -396,11 +463,16 @@ public class NearbyFragment extends CommonsDaggerSupportFragment
progressBar.setVisibility(View.GONE);
});
nearbyMapFragment.setBundleForUpdtes(bundle);
nearbyMapFragment.updateMapSignificantly();
nearbyMapFragment.updateMapSignificantlyForCurrentLocation();
updateListFragment();
return;
}
if (updateViaButton) {
nearbyMapFragment.updateMapSignificantlyForCustomLocation(customLatLng, nearbyPlacesInfo.placeList);
return;
}
/*
If this is the map update just after orientation change, then it is not a slight update
anymore. We want to significantly update map after each orientation change
@ -415,7 +487,7 @@ public class NearbyFragment extends CommonsDaggerSupportFragment
nearbyMapFragment.updateMapSlightly();
} else {
nearbyMapFragment.setBundleForUpdtes(bundle);
nearbyMapFragment.updateMapSignificantly();
nearbyMapFragment.updateMapSignificantlyForCurrentLocation();
updateListFragment();
}
} else {
@ -432,6 +504,15 @@ public class NearbyFragment extends CommonsDaggerSupportFragment
nearbyListFragment.updateNearbyListSignificantly();
}
/**
* Updates nearby list for custom location, will be used with search this area method. When you
* want to search for a place where you are not at.
* @param placeList List of places around your manually chosen target location from map.
*/
private void updateListFragmentForCustomLocation(List<Place> placeList) {
nearbyListFragment.updateNearbyListSignificantlyForCustomLocation(placeList);
}
/**
* Calls fragment for map view.
*/
@ -658,6 +739,9 @@ public class NearbyFragment extends CommonsDaggerSupportFragment
placesDisposable.dispose();
}
wikidataEditListener.setAuthenticationStateListener(null);
if (placesDisposableCustom != null) {
placesDisposableCustom.dispose();
}
}
@Override

View file

@ -35,6 +35,8 @@ import timber.log.Timber;
import static android.app.Activity.RESULT_OK;
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
import static fr.free.nrw.commons.wikidata.WikidataConstants.WIKIDATA_ENTITY_ID_PREF;
import static fr.free.nrw.commons.wikidata.WikidataConstants.WIKIDATA_ITEM_LOCATION;
public class NearbyListFragment extends DaggerFragment {
private Bundle bundleForUpdates; // Carry information from activity about changed nearby places and current location
@ -98,6 +100,19 @@ public class NearbyListFragment extends DaggerFragment {
}
}
/**
* While nearby updates for current location held with bundle, automatically, custom updates are
* done by calling this methods, triddered by search this are button input from user.
* @param placeList
*/
public void updateNearbyListSignificantlyForCustomLocation(List<Place> placeList) {
try {
adapterFactory.updateAdapterData(placeList, (RVRendererAdapter<Place>) recyclerView.getAdapter());
} catch (NullPointerException e) {
Timber.e("Null pointer exception from calling recyclerView.getAdapter()");
}
}
private List<Place> getPlaceListFromBundle(Bundle bundle) {
List<Place> placeList = Collections.emptyList();
@ -146,13 +161,14 @@ public class NearbyListFragment extends DaggerFragment {
if (resultCode == RESULT_OK) {
Timber.d("OnActivityResult() parameters: Req code: %d Result code: %d Data: %s",
requestCode, resultCode, data);
String wikidataEntityId = directPrefs.getString("WikiDataEntityId", null);
String wikidataEntityId = directPrefs.getString(WIKIDATA_ENTITY_ID_PREF, null);
String wikidataItemLocation = directPrefs.getString(WIKIDATA_ITEM_LOCATION, null);
if (requestCode == ContributionController.SELECT_FROM_CAMERA) {
// If coming from camera, pass null as uri. Because camera photos get saved to a
// fixed directory
controller.handleImagePicked(requestCode, null, true, wikidataEntityId);
controller.handleImagePicked(requestCode, null, true, wikidataEntityId, wikidataItemLocation);
} else {
controller.handleImagePicked(requestCode, data.getData(), true, wikidataEntityId);
controller.handleImagePicked(requestCode, data.getData(), true, wikidataEntityId, wikidataItemLocation);
}
} else {
Timber.e("OnActivityResult() parameters: Req code: %d Result code: %d Data: %s",

View file

@ -23,8 +23,10 @@ import android.view.View;
import android.view.ViewGroup;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.TextView;
import com.google.gson.Gson;
@ -60,14 +62,17 @@ import fr.free.nrw.commons.Utils;
import fr.free.nrw.commons.auth.LoginActivity;
import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao;
import fr.free.nrw.commons.contributions.ContributionController;
import fr.free.nrw.commons.location.LocationServiceManager;
import fr.free.nrw.commons.utils.LocationUtils;
import fr.free.nrw.commons.utils.PlaceUtils;
import fr.free.nrw.commons.utils.UriDeserializer;
import fr.free.nrw.commons.utils.ViewUtil;
import timber.log.Timber;
import uk.co.deanwild.materialshowcaseview.MaterialShowcaseView;
import static android.app.Activity.RESULT_OK;
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
import static fr.free.nrw.commons.wikidata.WikidataConstants.WIKIDATA_ENTITY_ID_PREF;
import static fr.free.nrw.commons.wikidata.WikidataConstants.WIKIDATA_ITEM_LOCATION;
public class NearbyMapFragment extends DaggerFragment {
@ -115,16 +120,21 @@ public class NearbyMapFragment extends DaggerFragment {
private Place place;
private Marker selected;
private Marker currentLocationMarker;
private MapboxMap mapboxMap;
public MapboxMap mapboxMap;
private PolygonOptions currentLocationPolygonOptions;
public Button searchThisAreaButton;
public ProgressBar searchThisAreaButtonProgressBar;
private boolean isBottomListSheetExpanded;
private final double CAMERA_TARGET_SHIFT_FACTOR_PORTRAIT = 0.06;
private final double CAMERA_TARGET_SHIFT_FACTOR_LANDSCAPE = 0.04;
private boolean isMapReady;
public boolean searchThisAreaModeOn = false;
private Bundle bundleForUpdtes;// Carry information from activity about changed nearby places and current location
private boolean searchedAroundCurrentLocation = true;
@Inject
@Named("prefs")
@ -246,8 +256,8 @@ public class NearbyMapFragment extends DaggerFragment {
* called when user is out of boundaries (south, north, east or west) of markers drawn by
* previous nearby call.
*/
public void updateMapSignificantly() {
Timber.d("updateMapSignificantly called, bundle is:"+bundleForUpdtes);
public void updateMapSignificantlyForCurrentLocation() {
Timber.d("updateMapSignificantlyForCurrentLocation called, bundle is:"+bundleForUpdtes);
if (mapboxMap != null) {
if (bundleForUpdtes != null) {
Gson gson = new GsonBuilder()
@ -271,10 +281,32 @@ public class NearbyMapFragment extends DaggerFragment {
mapboxMap.clear();
addCurrentLocationMarker(mapboxMap);
updateMapToTrackPosition();
addNearbyMarkerstoMapBoxMap();
// We are trying to find nearby places around our current location, thus custom parameter is nullified
addNearbyMarkerstoMapBoxMap(null);
}
}
/**
* Will be used for map vew updates for custom locations (ie. with search this area method).
* Clears the map, adds current location marker, adds nearby markers around custom location,
* re-enables map gestures which was locked during place load, remove progress bar.
* @param customLatLng custom location that we will search around
* @param placeList places around of custom location
*/
public void updateMapSignificantlyForCustomLocation(fr.free.nrw.commons.location.LatLng customLatLng, List<Place> placeList) {
List<NearbyBaseMarker> customBaseMarkerOptions = NearbyController
.loadAttractionsFromLocationToBaseMarkerOptions(curLatLng, // Curlatlang will be used to calculate distances
placeList,
getActivity());
mapboxMap.clear();
// We are trying to find nearby places around our custom searched area, thus custom parameter is nonnull
addNearbyMarkerstoMapBoxMap(customBaseMarkerOptions);
addCurrentLocationMarker(mapboxMap);
// Re-enable mapbox gestures on custom location markers load
mapboxMap.getUiSettings().setAllGesturesEnabled(true);
searchThisAreaButtonProgressBar.setVisibility(View.GONE);
}
// Only update current position marker and camera view
private void updateMapToTrackPosition() {
@ -299,6 +331,9 @@ public class NearbyMapFragment extends DaggerFragment {
// Make camera to follow user on location change
CameraPosition position ;
// Do not update camera position is search this area mode on
if (!searchThisAreaModeOn) {
if (ViewUtil.isPortrait(getActivity())){
position = new CameraPosition.Builder()
.target(isBottomListSheetExpanded ?
@ -323,10 +358,10 @@ public class NearbyMapFragment extends DaggerFragment {
mapboxMap.animateCamera(CameraUpdateFactory
.newCameraPosition(position), 1000);
}
}
}
private void initViews() {
Timber.d("initViews called");
bottomSheetList = ((NearbyFragment)getParentFragment()).view.findViewById(R.id.bottom_sheet);
@ -366,6 +401,9 @@ public class NearbyMapFragment extends DaggerFragment {
bookmarkButton = getActivity().findViewById(R.id.bookmarkButton);
bookmarkButtonImage = getActivity().findViewById(R.id.bookmarkButtonImage);
searchThisAreaButton = ((NearbyFragment)getParentFragment()).view.findViewById(R.id.search_this_area_button);
searchThisAreaButtonProgressBar = ((NearbyFragment)getParentFragment()).view.findViewById(R.id.search_this_area_button_progres_bar);
}
private void setListeners() {
@ -495,13 +533,77 @@ public class NearbyMapFragment extends DaggerFragment {
@Override
public void onMapReady(MapboxMap mapboxMap) {
NearbyMapFragment.this.mapboxMap = mapboxMap;
updateMapSignificantly();
addMapMovementListeners();
updateMapSignificantlyForCurrentLocation();
}
});
mapView.setStyleUrl("asset://mapstyle.json");
}
}
private void addMapMovementListeners() {
mapboxMap.addOnCameraMoveListener(new MapboxMap.OnCameraMoveListener() {
@Override
public void onCameraMove() {
if (NearbyController.currentLocation != null) { // If our nearby markers are calculated at least once
if (searchThisAreaButton.getVisibility() == View.GONE) {
searchThisAreaButton.setVisibility(View.VISIBLE);
}
double distance = mapboxMap.getCameraPosition().target
.distanceTo(new LatLng(NearbyController.currentLocation.getLatitude()
, NearbyController.currentLocation.getLongitude()));
if (distance > NearbyController.searchedRadius*1000*3/4) { //Convert to meter, and compare if our distance is bigger than 3/4 or our searched area
if (!searchThisAreaModeOn) { // If we are changing mode, then change click action
searchThisAreaModeOn = true;
searchThisAreaButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
searchThisAreaModeOn = true;
// Lock map operations during search this area operation
mapboxMap.getUiSettings().setAllGesturesEnabled(false);
searchThisAreaButtonProgressBar.setVisibility(View.VISIBLE);
searchThisAreaButton.setVisibility(View.GONE);
searchedAroundCurrentLocation = false;
((NearbyFragment)getParentFragment())
.refreshViewForCustomLocation(LocationUtils
.mapBoxLatLngToCommonsLatLng(mapboxMap.getCameraPosition().target), false);
}
});
}
} else {
if (searchThisAreaModeOn) {
searchThisAreaModeOn = false; // This flag will help us to understand should we folor users location or not
searchThisAreaButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
searchThisAreaModeOn = true;
// Lock map operations during search this area operation
mapboxMap.getUiSettings().setAllGesturesEnabled(false);
searchThisAreaButtonProgressBar.setVisibility(View.VISIBLE);
fabRecenter.callOnClick();
searchThisAreaButton.setVisibility(View.GONE);
searchedAroundCurrentLocation = true;
((NearbyFragment)getParentFragment())
.refreshViewForCustomLocation(LocationUtils
.mapBoxLatLngToCommonsLatLng(mapboxMap.getCameraPosition().target), true);
}
});
}
if (searchedAroundCurrentLocation) {
searchThisAreaButton.setVisibility(View.GONE);
}
}
}
}
});
}
/**
* onLogoutComplete is called after shared preferences and data stored in local database are cleared.
*/
@ -554,10 +656,17 @@ public class NearbyMapFragment extends DaggerFragment {
/**
* Adds markers for nearby places to mapbox map
*/
private void addNearbyMarkerstoMapBoxMap() {
private void addNearbyMarkerstoMapBoxMap(@Nullable List<NearbyBaseMarker> customNearbyBaseMarker) {
List<NearbyBaseMarker> baseMarkerOptions;
Timber.d("addNearbyMarkerstoMapBoxMap is called");
if (customNearbyBaseMarker != null) {
// If we try to update nearby points for a custom location choosen from map (we are not there)
baseMarkerOptions = customNearbyBaseMarker;
} else {
// If we try to display nearby markers around our curret location
baseMarkerOptions = this.baseMarkerOptions;
}
mapboxMap.addMarkers(baseMarkerOptions);
mapboxMap.setOnInfoWindowCloseListener(marker -> {
if (marker == selected) {
bottomSheetDetailsBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
@ -781,6 +890,7 @@ public class NearbyMapFragment extends DaggerFragment {
editor.putString("Desc", place.getLongDescription());
editor.putString("Category", place.getCategory());
editor.putString(WIKIDATA_ENTITY_ID_PREF, place.getWikiDataEntityId());
editor.putString(WIKIDATA_ITEM_LOCATION, PlaceUtils.latLangToString(place.location));
editor.apply();
}
@ -817,13 +927,14 @@ public class NearbyMapFragment extends DaggerFragment {
if (resultCode == RESULT_OK) {
Timber.d("OnActivityResult() parameters: Req code: %d Result code: %d Data: %s",
requestCode, resultCode, data);
String wikidataEntityId = directPrefs.getString("WikiDataEntityId", null);
String wikidataEntityId = directPrefs.getString(WIKIDATA_ENTITY_ID_PREF, null);
String wikidataItemLocation = directPrefs.getString(WIKIDATA_ITEM_LOCATION, null);
if (requestCode == ContributionController.SELECT_FROM_CAMERA) {
// If coming from camera, pass null as uri. Because camera photos get saved to a
// fixed directory
controller.handleImagePicked(requestCode, null, true, wikidataEntityId);
controller.handleImagePicked(requestCode, null, true, wikidataEntityId, wikidataItemLocation);
} else {
controller.handleImagePicked(requestCode, data.getData(), true, wikidataEntityId);
controller.handleImagePicked(requestCode, data.getData(), true, wikidataEntityId, wikidataItemLocation);
}
} else {
Timber.e("OnActivityResult() parameters: Req code: %d Result code: %d Data: %s",

View file

@ -6,12 +6,8 @@ import android.content.res.Resources;
import android.os.Handler;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.design.widget.CoordinatorLayout;
import android.support.design.widget.SwipeDismissBehavior;
import android.support.v7.app.AlertDialog;
import android.support.v7.widget.CardView;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.widget.Button;
@ -20,19 +16,17 @@ import android.widget.ProgressBar;
import android.widget.RelativeLayout;
import android.widget.TextView;
import android.widget.Toast;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.contributions.MainActivity;
import fr.free.nrw.commons.utils.SwipableCardView;
import fr.free.nrw.commons.utils.ViewUtil;
import timber.log.Timber;
/**
* Custom card view for nearby notification card view on main screen, above contributions list
*/
public class NearbyNoificationCardView extends CardView{
public class NearbyNoificationCardView extends SwipableCardView {
private static final float MINIMUM_THRESHOLD_FOR_SWIPE = 100;
private Context context;
private Button permissionRequestButton;
@ -99,41 +93,15 @@ public class NearbyNoificationCardView extends CardView{
private void setActionListeners() {
this.setOnClickListener(view -> ((MainActivity)context).viewPager.setCurrentItem(1));
this.setOnTouchListener(
(v, event) -> {
boolean isSwipe = false;
float deltaX=0.0f;
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
x1 = event.getX();
break;
case MotionEvent.ACTION_UP:
x2 = event.getX();
deltaX = x2 - x1;
if (deltaX < 0) {
//Right to left swipe
isSwipe = true;
} else if (deltaX > 0) {
//Left to right swipe
isSwipe = true;
}
break;
}
if (isSwipe && (pixelToDp(Math.abs(deltaX)) > MINIMUM_THRESHOLD_FOR_SWIPE)) {
v.setVisibility(GONE);
// Save shared preference for nearby card view accordingly
((MainActivity) context).prefs.edit()
.putBoolean("displayNearbyCardView", false).apply();
ViewUtil.showLongToast(context, getResources().getString(R.string.nearby_notification_dismiss_message));
return true;
}
return false;
});
}
private float pixelToDp(float pixels) {
return (pixels / Resources.getSystem().getDisplayMetrics().density);
@Override public boolean onSwipe(View view) {
view.setVisibility(GONE);
// Save shared preference for nearby card view accordingly
((MainActivity) context).prefs.edit().putBoolean("displayNearbyCardView", false).apply();
ViewUtil.showLongToast(context,
getResources().getString(R.string.nearby_notification_dismiss_message));
return true;
}
/**

View file

@ -30,7 +30,7 @@ public class NearbyPlaces {
private static final Uri WIKIDATA_QUERY_URL = Uri.parse("https://query.wikidata.org/sparql");
private static final Uri WIKIDATA_QUERY_UI_URL = Uri.parse("https://query.wikidata.org/");
private final String wikidataQuery;
private double radius = INITIAL_RADIUS;
public double radius = INITIAL_RADIUS;
public NearbyPlaces() {
try {
@ -55,6 +55,7 @@ public class NearbyPlaces {
} else {
MIN_RESULTS = 40;
MAX_RADIUS = 300.0; // in kilometers
radius = INITIAL_RADIUS;
}
// increase the radius gradually to find a satisfactory number of nearby places

View file

@ -30,10 +30,12 @@ import fr.free.nrw.commons.auth.LoginActivity;
import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao;
import fr.free.nrw.commons.contributions.ContributionController;
import fr.free.nrw.commons.di.ApplicationlessInjection;
import fr.free.nrw.commons.utils.PlaceUtils;
import timber.log.Timber;
import static fr.free.nrw.commons.theme.NavigationBaseActivity.startActivityWithFlags;
import static fr.free.nrw.commons.wikidata.WikidataConstants.WIKIDATA_ENTITY_ID_PREF;
import static fr.free.nrw.commons.wikidata.WikidataConstants.WIKIDATA_ITEM_LOCATION;
public class PlaceRenderer extends Renderer<Place> {
@ -193,6 +195,7 @@ public class PlaceRenderer extends Renderer<Place> {
editor.putString("Desc", place.getLongDescription());
editor.putString("Category", place.getCategory());
editor.putString(WIKIDATA_ENTITY_ID_PREF, place.getWikiDataEntityId());
editor.putString(WIKIDATA_ITEM_LOCATION, PlaceUtils.latLangToString(place.location));
editor.apply();
}

View file

@ -1,18 +1,12 @@
package fr.free.nrw.commons.notification;
import android.graphics.Color;
import android.text.Html;
import android.text.SpannableString;
import android.text.Spanned;
import android.text.TextPaint;
import android.text.style.ClickableSpan;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import com.borjabravo.readmoretextview.ReadMoreTextView;
import com.pedrogomez.renderers.Renderer;
import butterknife.BindView;
@ -24,7 +18,7 @@ import fr.free.nrw.commons.R;
*/
public class NotificationRenderer extends Renderer<Notification> {
@BindView(R.id.title) ReadMoreTextView title;
@BindView(R.id.title) TextView title;
@BindView(R.id.time) TextView time;
@BindView(R.id.icon) ImageView icon;
private NotificationClicked listener;
@ -64,26 +58,12 @@ public class NotificationRenderer extends Renderer<Notification> {
private void setTitle(String notificationText) {
notificationText = notificationText.trim().replaceAll("(^\\s*)|(\\s*$)", "");
notificationText = Html.fromHtml(notificationText).toString();
if(notificationText.length()>280){
notificationText = notificationText.substring(0,279);
notificationText = notificationText.concat("...");
}
notificationText = notificationText.concat(" ");
SpannableString ss = new SpannableString(notificationText);
ClickableSpan clickableSpan = new ClickableSpan() {
@Override
public void onClick(View view) {
listener.notificationClicked(getContent());
}
@Override
public void updateDrawState(TextPaint ds) {
super.updateDrawState(ds);
ds.setUnderlineText(false);
ds.setColor(Color.BLACK);
}
};
// Attach a ClickableSpan to the range (start:0, end:notificationText.length()) of the String
ss.setSpan(clickableSpan, 0, notificationText.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
title.setText(ss, TextView.BufferType.SPANNABLE);
title.setText(notificationText);
}
public interface NotificationClicked{

View file

@ -174,7 +174,10 @@ class DescriptionsAdapter extends RecyclerView.Adapter<DescriptionsAdapter.ViewH
descItemEditText.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null);
}
descItemEditText.addTextChangedListener(new AbstractTextWatcher(description::setDescriptionText));
descItemEditText.addTextChangedListener(new AbstractTextWatcher(descriptionText->{
descriptions.get(position - 1).setDescriptionText(descriptionText);
}));
descItemEditText.setOnFocusChangeListener((v, hasFocus) -> {
if (!hasFocus) {
ViewUtil.hideKeyboard(v);

View file

@ -2,37 +2,31 @@ package fr.free.nrw.commons.upload;
import android.annotation.SuppressLint;
import android.content.ContentResolver;
import android.content.Context;
import android.content.SharedPreferences;
import android.media.ExifInterface;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.ParcelFileDescriptor;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v7.app.AppCompatActivity;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.Date;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import fr.free.nrw.commons.caching.CacheController;
import fr.free.nrw.commons.di.ApplicationlessInjection;
import fr.free.nrw.commons.mwapi.CategoryApi;
import io.reactivex.schedulers.Schedulers;
import timber.log.Timber;
import static com.mapbox.mapboxsdk.Mapbox.getApplicationContext;
/**
* Processing of the image file that is about to be uploaded via ShareActivity is done here
*/
@Singleton
public class FileProcessor implements SimilarImageDialogFragment.onResponse {
@Inject
@ -47,24 +41,23 @@ public class FileProcessor implements SimilarImageDialogFragment.onResponse {
private String filePath;
private ContentResolver contentResolver;
private GPSExtractor imageObj;
private Context context;
private String decimalCoords;
private ExifInterface exifInterface;
private boolean useExtStorage;
private boolean haveCheckedForOtherImages = false;
private GPSExtractor tempImageObj;
FileProcessor(@NonNull String filePath, ContentResolver contentResolver, Context context) {
@Inject
FileProcessor() {
}
void initFileDetails(@NonNull String filePath, ContentResolver contentResolver) {
this.filePath = filePath;
this.contentResolver = contentResolver;
this.context = context;
ApplicationlessInjection.getInstance(context.getApplicationContext()).getCommonsApplicationComponent().inject(this);
try {
exifInterface=new ExifInterface(filePath);
exifInterface = new ExifInterface(filePath);
} catch (IOException e) {
Timber.e(e);
}
useExtStorage = prefs.getBoolean("useExternalStorage", true);
}
/**
@ -85,10 +78,6 @@ public class FileProcessor implements SimilarImageDialogFragment.onResponse {
return imageObj;
}
String getDecimalCoords() {
return decimalCoords;
}
/**
* Find other images around the same location that were taken within the last 20 sec
* @param similarImageInterface
@ -142,7 +131,7 @@ public class FileProcessor implements SimilarImageDialogFragment.onResponse {
* Then initiates the calls to MediaWiki API through an instance of CategoryApi.
*/
@SuppressLint("CheckResult")
public void useImageCoords() {
private void useImageCoords() {
if (decimalCoords != null) {
Timber.d("Decimal coords of image: %s", decimalCoords);
Timber.d("is EXIF data present:" + imageObj.imageCoordsExists + " from findOther image");

View file

@ -6,6 +6,7 @@ import android.content.ContentUris;
import android.content.Context;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.media.ExifInterface;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
@ -78,6 +79,25 @@ public class FileUtils {
}
}
/**
* Get Geolocation of file from input file path
*/
static String getGeolocationOfFile(String filePath) {
try {
ExifInterface exifInterface=new ExifInterface(filePath);
GPSExtractor imageObj = new GPSExtractor(exifInterface);
if (imageObj.imageCoordsExists) { // If image has geolocation information in its EXIF
return imageObj.getCoords();
} else {
return "";
}
} catch (IOException e) {
e.printStackTrace();
return "";
}
}
/**
* In older devices getPath() may fail depending on the source URI. Creating and using a copy of the file seems to work instead.
*
@ -234,8 +254,8 @@ public class FileUtils {
* @return The value of the _data column, which is typically a file path.
*/
@Nullable
public static String getDataColumn(Context context, Uri uri, String selection,
String[] selectionArgs) {
private static String getDataColumn(Context context, Uri uri, String selection,
String[] selectionArgs) {
Cursor cursor = null;
final String column = MediaStore.Images.ImageColumns.DATA;
@ -311,7 +331,7 @@ public class FileUtils {
* @param destination file path copied to
* @throws IOException thrown when failing to read source or opening destination file
*/
public static void copy(@NonNull FileDescriptor source, @NonNull String destination)
private static void copy(@NonNull FileDescriptor source, @NonNull String destination)
throws IOException {
copy(new FileInputStream(source), new FileOutputStream(destination));
}
@ -415,7 +435,7 @@ public class FileUtils {
return result;
}
public static String getFileExt(String fileName){
static String getFileExt(String fileName){
//Default file extension
String extension=".jpg";
@ -426,7 +446,11 @@ public class FileUtils {
return extension;
}
public static String getFileExt(Uri uri, ContentResolver contentResolver) {
private static String getFileExt(Uri uri, ContentResolver contentResolver) {
return getFileExt(getFilename(uri, contentResolver));
}
public static FileInputStream getFileInputStream(String filePath) throws FileNotFoundException {
return new FileInputStream(filePath);
}
}

View file

@ -0,0 +1,46 @@
package fr.free.nrw.commons.upload;
import android.content.ContentResolver;
import android.content.Context;
import android.net.Uri;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import javax.inject.Inject;
import javax.inject.Singleton;
@Singleton
public class FileUtilsWrapper {
@Inject
public FileUtilsWrapper() {
}
public String createExternalCopyPathAndCopy(Uri uri, ContentResolver contentResolver) throws IOException {
return FileUtils.createExternalCopyPathAndCopy(uri, contentResolver);
}
public String createCopyPathAndCopy(Uri uri, Context context) throws IOException {
return FileUtils.createCopyPathAndCopy(uri, context);
}
public String getFileExt(String fileName) {
return FileUtils.getFileExt(fileName);
}
public String getSHA1(InputStream is) {
return FileUtils.getSHA1(is);
}
public FileInputStream getFileInputStream(String filePath) throws FileNotFoundException {
return FileUtils.getFileInputStream(filePath);
}
public String getGeolocationOfFile(String filePath) {
return FileUtils.getGeolocationOfFile(filePath);
}
}

View file

@ -14,12 +14,12 @@ import timber.log.Timber;
* Extracts geolocation to be passed to API for category suggestions. If a picture with geolocation
* is uploaded, extract latitude and longitude from EXIF data of image.
*/
public class GPSExtractor {
class GPSExtractor {
public static final GPSExtractor DUMMY= new GPSExtractor();
static final GPSExtractor DUMMY= new GPSExtractor();
private double decLatitude;
private double decLongitude;
public boolean imageCoordsExists;
boolean imageCoordsExists;
private String latitude;
private String longitude;
private String latitudeRef;
@ -37,7 +37,7 @@ public class GPSExtractor {
* @param fileDescriptor the file descriptor of the image
*/
@RequiresApi(24)
public GPSExtractor(@NonNull FileDescriptor fileDescriptor) {
GPSExtractor(@NonNull FileDescriptor fileDescriptor) {
try {
ExifInterface exif = new ExifInterface(fileDescriptor);
processCoords(exif);
@ -51,7 +51,7 @@ public class GPSExtractor {
* @param path file path of the image
*
*/
public GPSExtractor(@NonNull String path) {
GPSExtractor(@NonNull String path) {
try {
ExifInterface exif = new ExifInterface(path);
processCoords(exif);
@ -65,7 +65,7 @@ public class GPSExtractor {
* @param exif exif interface of the image
*
*/
public GPSExtractor(@NonNull ExifInterface exif){
GPSExtractor(@NonNull ExifInterface exif){
processCoords(exif);
}
@ -89,7 +89,7 @@ public class GPSExtractor {
* @return coordinates as string (needs to be passed as a String in API query)
*/
@Nullable
public String getCoords() {
String getCoords() {
if(decimalCoords!=null){
return decimalCoords;
}else if (latitude!=null && latitudeRef!=null && longitude!=null && longitudeRef!=null) {
@ -103,11 +103,11 @@ public class GPSExtractor {
}
}
public double getDecLatitude() {
double getDecLatitude() {
return decLatitude;
}
public double getDecLongitude() {
double getDecLongitude() {
return decLongitude;
}

View file

@ -65,6 +65,7 @@ import timber.log.Timber;
import static fr.free.nrw.commons.utils.ImageUtils.Result;
import static fr.free.nrw.commons.utils.ImageUtils.getErrorMessageForResult;
import static fr.free.nrw.commons.wikidata.WikidataConstants.WIKIDATA_ENTITY_ID_PREF;
import static fr.free.nrw.commons.wikidata.WikidataConstants.WIKIDATA_ITEM_LOCATION;
public class UploadActivity extends AuthenticatedActivity implements UploadView, SimilarImageInterface {
@Inject InputMethodManager inputMethodManager;
@ -251,13 +252,14 @@ public class UploadActivity extends AuthenticatedActivity implements UploadView,
@SuppressLint("StringFormatInvalid")
@Override
public void updateLicenseSummary(String selectedLicense) {
public void updateLicenseSummary(String selectedLicense, int imageCount) {
String licenseHyperLink = "<a href='" + Utils.licenseUrlFor(selectedLicense)+"'>" +
getString(Utils.licenseNameFor(selectedLicense)) + "</a><br>";
licenseSummary.setMovementMethod(LinkMovementMethod.getInstance());
licenseSummary.setText(
Html.fromHtml(
getString(R.string.share_license_summary, licenseHyperLink)));
getResources().getQuantityString(R.plurals.share_license_summary,
imageCount, licenseHyperLink)));
}
@Override
@ -350,6 +352,9 @@ public class UploadActivity extends AuthenticatedActivity implements UploadView,
@Override
public void showBadPicturePopup(@Result int result) {
if (result >= 8 ) { // If location of image and nearby does not match, then set shared preferences to disable wikidata edits
directPrefs.edit().putBoolean("Picture_Has_Correct_Location",false);
}
String errorMessageForResult = getErrorMessageForResult(this, result);
if (StringUtils.isNullOrWhiteSpace(errorMessageForResult)) {
return;
@ -553,7 +558,8 @@ public class UploadActivity extends AuthenticatedActivity implements UploadView,
String imageDesc = directPrefs.getString("Desc", "");
Timber.i("Received direct upload with title %s and description %s", imageTitle, imageDesc);
String wikidataEntityIdPref = intent.getStringExtra(WIKIDATA_ENTITY_ID_PREF);
presenter.receiveDirect(mediaUri, mimeType, source, wikidataEntityIdPref, imageTitle, imageDesc);
String wikidataItemLocation = intent.getStringExtra(WIKIDATA_ITEM_LOCATION);
presenter.receiveDirect(mediaUri, mimeType, source, wikidataEntityIdPref, imageTitle, imageDesc, wikidataItemLocation);
} else {
Timber.i("Received single upload");
presenter.receive(mediaUri, mimeType, source);

View file

@ -51,7 +51,7 @@ public class UploadController {
}
private boolean isUploadServiceConnected;
private ServiceConnection uploadServiceConnection = new ServiceConnection() {
public ServiceConnection uploadServiceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName componentName, IBinder binder) {
uploadService = (UploadService) ((HandlerService.HandlerServiceLocalBinder) binder).getService();
@ -61,6 +61,7 @@ public class UploadController {
@Override
public void onServiceDisconnected(ComponentName componentName) {
// this should never happen
isUploadServiceConnected = false;
Timber.e(new RuntimeException("UploadService died but the rest of the process did not!"));
}
};
@ -68,7 +69,7 @@ public class UploadController {
/**
* Prepares the upload service.
*/
public void prepareService() {
void prepareService() {
Intent uploadServiceIntent = new Intent(context, UploadService.class);
uploadServiceIntent.setAction(UploadService.ACTION_START_SERVICE);
context.startService(uploadServiceIntent);
@ -78,7 +79,7 @@ public class UploadController {
/**
* Disconnects the upload service.
*/
public void cleanup() {
void cleanup() {
if (isUploadServiceConnected) {
context.unbindService(uploadServiceConnection);
}
@ -89,7 +90,7 @@ public class UploadController {
*
* @param contribution the contribution object
*/
public void startUpload(Contribution contribution) {
void startUpload(Contribution contribution) {
startUpload(contribution, c -> {});
}
@ -100,7 +101,7 @@ public class UploadController {
* @param onComplete the progress tracker
*/
@SuppressLint("StaticFieldLeak")
public void startUpload(final Contribution contribution, final ContributionUploadProgress onComplete) {
private void startUpload(final Contribution contribution, final ContributionUploadProgress onComplete) {
//Set creator, desc, and license
if (TextUtils.isEmpty(contribution.getCreator())) {
Account currentAccount = sessionManager.getCurrentAccount();
@ -110,7 +111,7 @@ public class UploadController {
sessionManager.forceLogin(context);
return;
}
contribution.setCreator(currentAccount.name);
contribution.setCreator(sessionManager.getAuthorName());
}
if (contribution.getDescription() == null) {

View file

@ -10,7 +10,6 @@ import android.net.Uri;
import android.support.annotation.Nullable;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Date;
@ -25,7 +24,9 @@ import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.contributions.Contribution;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import fr.free.nrw.commons.settings.Prefs;
import fr.free.nrw.commons.utils.BitmapRegionDecoderWrapper;
import fr.free.nrw.commons.utils.ImageUtils;
import fr.free.nrw.commons.utils.ImageUtilsWrapper;
import io.reactivex.Observable;
import io.reactivex.Single;
import io.reactivex.disposables.Disposable;
@ -53,23 +54,36 @@ public class UploadModel {
private boolean useExtStorage;
private Disposable badImageSubscription;
@Inject
SessionManager sessionManager;
private SessionManager sessionManager;
private Uri currentMediaUri;
private FileUtilsWrapper fileUtilsWrapper;
private ImageUtilsWrapper imageUtilsWrapper;
private BitmapRegionDecoderWrapper bitmapRegionDecoderWrapper;
private FileProcessor fileProcessor;
@Inject
UploadModel(@Named("licenses") List<String> licenses,
@Named("default_preferences") SharedPreferences prefs,
@Named("licenses_by_name") Map<String, String> licensesByName,
Context context,
MediaWikiApi mwApi) {
MediaWikiApi mwApi,
SessionManager sessionManager,
FileUtilsWrapper fileUtilsWrapper,
ImageUtilsWrapper imageUtilsWrapper,
BitmapRegionDecoderWrapper bitmapRegionDecoderWrapper,
FileProcessor fileProcessor) {
this.licenses = licenses;
this.prefs = prefs;
this.bitmapRegionDecoderWrapper = bitmapRegionDecoderWrapper;
this.license = Prefs.Licenses.CC_BY_SA_3;
this.licensesByName = licensesByName;
this.context = context;
this.mwApi = mwApi;
this.contentResolver = context.getContentResolver();
this.sessionManager = sessionManager;
this.fileUtilsWrapper = fileUtilsWrapper;
this.fileProcessor = fileProcessor;
this.imageUtilsWrapper = imageUtilsWrapper;
useExtStorage = this.prefs.getBoolean("useExternalStorage", false);
}
@ -84,19 +98,19 @@ public class UploadModel {
.map(filePath -> {
long fileCreatedDate = getFileCreatedDate(currentMediaUri);
Uri uri = Uri.fromFile(new File(filePath));
FileProcessor fp = new FileProcessor(filePath, context.getContentResolver(), context);
UploadItem item = new UploadItem(uri, mimeType, source, fp.processFileCoordinates(similarImageInterface),
FileUtils.getFileExt(filePath), null,fileCreatedDate);
fileProcessor.initFileDetails(filePath, context.getContentResolver());
UploadItem item = new UploadItem(uri, mimeType, source, fileProcessor.processFileCoordinates(similarImageInterface),
fileUtilsWrapper.getFileExt(filePath), null,fileCreatedDate);
Single.zip(
Single.fromCallable(() ->
new FileInputStream(filePath))
.map(FileUtils::getSHA1)
fileUtilsWrapper.getFileInputStream(filePath))
.map(fileUtilsWrapper::getSHA1)
.map(mwApi::existingFile)
.map(b -> b ? ImageUtils.IMAGE_DUPLICATE : ImageUtils.IMAGE_OK),
Single.fromCallable(() ->
new FileInputStream(filePath))
.map(file -> BitmapRegionDecoder.newInstance(file, false))
.map(ImageUtils::checkIfImageIsTooDark), //Returns IMAGE_DARK or IMAGE_OK
fileUtilsWrapper.getFileInputStream(filePath))
.map(file -> bitmapRegionDecoderWrapper.newInstance(file, false))
.map(imageUtilsWrapper::checkIfImageIsTooDark), //Returns IMAGE_DARK or IMAGE_OK
(dupe, dark) -> dupe | dark)
.observeOn(Schedulers.io())
.subscribe(item.imageQuality::onNext, Timber::e);
@ -108,29 +122,33 @@ public class UploadModel {
}
@SuppressLint("CheckResult")
void receiveDirect(Uri media, String mimeType, String source, String wikidataEntityIdPref, String title, String desc, SimilarImageInterface similarImageInterface) {
void receiveDirect(Uri media, String mimeType, String source, String wikidataEntityIdPref, String title, String desc, SimilarImageInterface similarImageInterface, String wikidataItemLocation) {
initDefaultValues();
long fileCreatedDate = getFileCreatedDate(media);
String filePath = this.cacheFileUpload(media);
Uri uri = Uri.fromFile(new File(filePath));
FileProcessor fp = new FileProcessor(filePath, context.getContentResolver(), context);
UploadItem item = new UploadItem(uri, mimeType, source, fp.processFileCoordinates(similarImageInterface),
FileUtils.getFileExt(filePath), wikidataEntityIdPref,fileCreatedDate);
fileProcessor.initFileDetails(filePath, context.getContentResolver());
UploadItem item = new UploadItem(uri, mimeType, source, fileProcessor.processFileCoordinates(similarImageInterface),
fileUtilsWrapper.getFileExt(filePath), wikidataEntityIdPref,fileCreatedDate);
item.title.setTitleText(title);
item.descriptions.get(0).setDescriptionText(desc);
//TODO figure out if default descriptions in other languages exist
item.descriptions.get(0).setLanguageCode("en");
Single.zip(
Single.fromCallable(() ->
new FileInputStream(filePath))
.map(FileUtils::getSHA1)
fileUtilsWrapper.getFileInputStream(filePath))
.map(fileUtilsWrapper::getSHA1)
.map(mwApi::existingFile)
.map(b -> b ? ImageUtils.IMAGE_DUPLICATE : ImageUtils.IMAGE_OK),
Single.fromCallable(() -> filePath)
.map(fileUtilsWrapper::getGeolocationOfFile)
.map(geoLocation -> imageUtilsWrapper.checkImageGeolocationIsDifferent(geoLocation,wikidataItemLocation))
.map(r -> r ? ImageUtils.IMAGE_GEOLOCATION_DIFFERENT : ImageUtils.IMAGE_OK),
Single.fromCallable(() ->
new FileInputStream(filePath))
.map(file -> BitmapRegionDecoder.newInstance(file, false))
.map(ImageUtils::checkIfImageIsTooDark), //Returns IMAGE_DARK or IMAGE_OK
(dupe, dark) -> dupe | dark).subscribe(item.imageQuality::onNext);
fileUtilsWrapper.getFileInputStream(filePath))
.map(file -> bitmapRegionDecoderWrapper.newInstance(file, false))
.map(imageUtilsWrapper::checkIfImageIsTooDark), //Returns IMAGE_DARK or IMAGE_OK
(dupe, wrongGeo, dark) -> dupe | wrongGeo | dark).subscribe(item.imageQuality::onNext);
items.add(item);
items.get(0).selected = true;
items.get(0).first = true;
@ -239,7 +257,7 @@ public class UploadModel {
updateItemState();
}
public void setCurrentTitleAndDescriptions(Title title, List<Description> descriptions) {
void setCurrentTitleAndDescriptions(Title title, List<Description> descriptions) {
setCurrentUploadTitle(title);
setCurrentUploadDescriptions(descriptions);
}
@ -312,7 +330,7 @@ public class UploadModel {
{
Contribution contribution = new Contribution(item.mediaUri, null, item.title + "." + item.fileExt,
Description.formatList(item.descriptions), -1,
null, null, sessionManager.getUserName(),
null, null, sessionManager.getAuthorName(),
CommonsApplication.DEFAULT_EDIT_SUMMARY, item.gpsCoords.getCoords());
contribution.setWikiDataEntityId(item.wikidataEntityId);
contribution.setCategories(categoryStringList);
@ -337,9 +355,9 @@ public class UploadModel {
try {
String copyPath;
if (useExtStorage)
copyPath = FileUtils.createExternalCopyPathAndCopy(media, contentResolver);
copyPath = fileUtilsWrapper.createExternalCopyPathAndCopy(media, contentResolver);
else
copyPath = FileUtils.createCopyPathAndCopy(media, context);
copyPath = fileUtilsWrapper.createCopyPathAndCopy(media, context);
Timber.i("File path is " + copyPath);
return copyPath;
} catch (IOException e) {
@ -362,6 +380,9 @@ public class UploadModel {
badImageSubscription = getCurrentItem().imageQuality.subscribe(consumer, Timber::e);
}
public List<UploadItem> getItems() {
return items;
}
@SuppressWarnings("WeakerAccess")
static class UploadItem {
@ -397,4 +418,4 @@ public class UploadModel {
}
}
}
}

View file

@ -2,6 +2,7 @@ package fr.free.nrw.commons.upload;
import android.annotation.SuppressLint;
import android.net.Uri;
import android.util.Log;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
@ -92,8 +93,8 @@ public class UploadPresenter {
* @param source File source from {@link Contribution.FileSource}
*/
@SuppressLint("CheckResult")
void receiveDirect(Uri media, String mimeType, @Contribution.FileSource String source, String wikidataEntityIdPref, String title, String desc) {
Completable.fromRunnable(() -> uploadModel.receiveDirect(media, mimeType, source, wikidataEntityIdPref, title, desc, similarImageInterface))
void receiveDirect(Uri media, String mimeType, @Contribution.FileSource String source, String wikidataEntityIdPref, String title, String desc, String wikidataItemLocation) {
Completable.fromRunnable(() -> uploadModel.receiveDirect(media, mimeType, source, wikidataEntityIdPref, title, desc, similarImageInterface, wikidataItemLocation))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(() -> {
@ -111,7 +112,7 @@ public class UploadPresenter {
*/
void selectLicense(String licenseName) {
uploadModel.setSelectedLicense(licenseName);
view.updateLicenseSummary(uploadModel.getSelectedLicense());
view.updateLicenseSummary(uploadModel.getSelectedLicense(), uploadModel.getCount());
}
//region Wizard step management
@ -356,7 +357,7 @@ public class UploadPresenter {
private void updateLicenses() {
String selectedLicense = uploadModel.getSelectedLicense();
view.updateLicenses(uploadModel.getLicenses(), selectedLicense);
view.updateLicenseSummary(selectedLicense);
view.updateLicenseSummary(selectedLicense, uploadModel.getCount());
}
/**

View file

@ -88,7 +88,7 @@ public class UploadService extends HandlerService<Contribution> {
String notificationProgressTitle;
String notificationFinishingTitle;
public NotificationUpdateProgressListener(String notificationTag, String notificationProgressTitle, String notificationFinishingTitle, Contribution contribution) {
NotificationUpdateProgressListener(String notificationTag, String notificationProgressTitle, String notificationFinishingTitle, Contribution contribution) {
this.notificationTag = notificationTag;
this.notificationProgressTitle = notificationProgressTitle;
this.notificationFinishingTitle = notificationFinishingTitle;

View file

@ -60,7 +60,7 @@ public interface UploadView {
void updateLicenses(List<String> licenses, String selectedLicense);
void updateLicenseSummary(String selectedLicense);
void updateLicenseSummary(String selectedLicense, int imageCount);
void updateTopCardContent();

View file

@ -1,71 +0,0 @@
package fr.free.nrw.commons.upload;
import java.util.HashMap;
/**
* This is a Util class which provides the necessary token to open the Commons License
* info in the user language
*/
public class UrlLicense {
public static HashMap<String,String> urlLicense = new HashMap<>();
static {
urlLicense.put("en", "https://commons.wikimedia.org/wiki/Commons:Licensing");
urlLicense.put("ar", "https://commons.wikimedia.org/wiki/Commons:Licensing/ar");
urlLicense.put("ast", "https://commons.wikimedia.org/wiki/Commons:Licensing/ast");
urlLicense.put("az", "https://commons.wikimedia.org/wiki/Commons:Licensing/az");
urlLicense.put("be", "https://commons.wikimedia.org/wiki/Commons:Licensing/be");
urlLicense.put("bg", "https://commons.wikimedia.org/wiki/Commons:Licensing/bg");
urlLicense.put("bn", "https://commons.wikimedia.org/wiki/Commons:Licensing/bn");
urlLicense.put("ca", "https://commons.wikimedia.org/wiki/Commons:Licensing/ca");
urlLicense.put("cs", "https://commons.wikimedia.org/wiki/Commons:Licensing/cs");
urlLicense.put("da", "https://commons.wikimedia.org/wiki/Commons:Licensing/da");
urlLicense.put("de", "https://commons.wikimedia.org/wiki/Commons:Licensing/de");
urlLicense.put("el", "https://commons.wikimedia.org/wiki/Commons:Licensing/el");
urlLicense.put("eo", "https://commons.wikimedia.org/wiki/Commons:Licensing/eo");
urlLicense.put("es", "https://commons.wikimedia.org/wiki/Commons:Licensing/es");
urlLicense.put("eu", "https://commons.wikimedia.org/wiki/Commons:Licensing/eu");
urlLicense.put("fa", "https://commons.wikimedia.org/wiki/Commons:Licensing/fa");
urlLicense.put("fi", "https://commons.wikimedia.org/wiki/Commons:Licensing/fi");
urlLicense.put("fr", "https://commons.wikimedia.org/wiki/Commons:Licensing/fr");
urlLicense.put("gl", "https://commons.wikimedia.org/wiki/Commons:Licensing/gl");
urlLicense.put("gsw", "https://commons.wikimedia.org/wiki/Commons:Licensing/gsw");
urlLicense.put("he", "https://commons.wikimedia.org/wiki/Commons:Licensing/he");
urlLicense.put("hi", "https://commons.wikimedia.org/wiki/Commons:Licensing/hi");
urlLicense.put("hu", "https://commons.wikimedia.org/wiki/Commons:Licensing/hu");
urlLicense.put("id", "https://commons.wikimedia.org/wiki/Commons:Licensing/id");
urlLicense.put("is", "https://commons.wikimedia.org/wiki/Commons:Licensing/is");
urlLicense.put("it", "https://commons.wikimedia.org/wiki/Commons:Licensing/it");
urlLicense.put("ja", "https://commons.wikimedia.org/wiki/Commons:Licensing/ja");
urlLicense.put("ka", "https://commons.wikimedia.org/wiki/Commons:Licensing/ka");
urlLicense.put("km", "https://commons.wikimedia.org/wiki/Commons:Licensing/km");
urlLicense.put("ko", "https://commons.wikimedia.org/wiki/Commons:Licensing/ko");
urlLicense.put("ku", "https://commons.wikimedia.org/wiki/Commons:Licensing/ku");
urlLicense.put("mk", "https://commons.wikimedia.org/wiki/Commons:Licensing/mk");
urlLicense.put("mr", "https://commons.wikimedia.org/wiki/Commons:Licensing/mr");
urlLicense.put("ms", "https://commons.wikimedia.org/wiki/Commons:Licensing/ms");
urlLicense.put("my", "https://commons.wikimedia.org/wiki/Commons:Licensing/my");
urlLicense.put("nl", "https://commons.wikimedia.org/wiki/Commons:Licensing/nl");
urlLicense.put("oc", "https://commons.wikimedia.org/wiki/Commons:Licensing/oc");
urlLicense.put("pl", "https://commons.wikimedia.org/wiki/Commons:Licensing/pl");
urlLicense.put("pt", "https://commons.wikimedia.org/wiki/Commons:Licensing/pt");
urlLicense.put("pt-br", "https://commons.wikimedia.org/wiki/Commons:Licensing/pt-br");
urlLicense.put("ro", "https://commons.wikimedia.org/wiki/Commons:Licensing/ro");
urlLicense.put("ru", "https://commons.wikimedia.org/wiki/Commons:Licensing/ru");
urlLicense.put("scn", "https://commons.wikimedia.org/wiki/Commons:Licensing/scn");
urlLicense.put("sk", "https://commons.wikimedia.org/wiki/Commons:Licensing/sk");
urlLicense.put("sl", "https://commons.wikimedia.org/wiki/Commons:Licensing/sl");
urlLicense.put("sv", "https://commons.wikimedia.org/wiki/Commons:Licensing/sv");
urlLicense.put("tr", "https://commons.wikimedia.org/wiki/Commons:Licensing/tr");
urlLicense.put("uk", "https://commons.wikimedia.org/wiki/Commons:Licensing/uk");
urlLicense.put("ur", "https://commons.wikimedia.org/wiki/Commons:Licensing/ur");
urlLicense.put("vi", "https://commons.wikimedia.org/wiki/Commons:Licensing/vi");
urlLicense.put("zh", "https://commons.wikimedia.org/wiki/Commons:Licensing/zh");
}
public static String getLicenseUrl ( String language){
if (urlLicense.containsKey(language)) {
return urlLicense.get(language);
} else {
return urlLicense.get("en");
}
}
}

View file

@ -1,115 +0,0 @@
package fr.free.nrw.commons.upload;
import android.content.ContentResolver;
import android.graphics.Bitmap;
import android.graphics.BitmapRegionDecoder;
import android.graphics.Point;
import android.graphics.Rect;
import android.net.Uri;
import android.provider.MediaStore;
import android.support.v4.graphics.BitmapCompat;
import android.view.View;
import android.widget.FrameLayout;
import java.io.IOException;
import java.io.InputStream;
import timber.log.Timber;
/**
* Contains utility methods for the Zoom function in ShareActivity.
*/
public class Zoom {
private View thumbView;
private ContentResolver contentResolver;
private FrameLayout flContainer;
Zoom(View thumbView, FrameLayout flContainer, ContentResolver contentResolver) {
this.thumbView = thumbView;
this.contentResolver = contentResolver;
this.flContainer = flContainer;
}
/**
* Create a scaled bitmap to display the zoomed-in image
* @param input the input stream corresponding to the uploaded image
* @param imageUri the uploaded image's URI
* @return a zoomable bitmap
*/
Bitmap createScaledImage(InputStream input, Uri imageUri) {
Bitmap scaled = null;
BitmapRegionDecoder decoder = null;
Bitmap bitmap = null;
try {
decoder = BitmapRegionDecoder.newInstance(input, false);
bitmap = decoder.decodeRegion(new Rect(10, 10, 50, 50), null);
} catch (IOException e) {
Timber.e(e);
} catch (NullPointerException e) {
Timber.e(e);
}
try {
//Compress the Image
System.gc();
Runtime rt = Runtime.getRuntime();
long maxMemory = rt.freeMemory();
bitmap = MediaStore.Images.Media.getBitmap(contentResolver, imageUri);
int bitmapByteCount = BitmapCompat.getAllocationByteCount(bitmap);
long height = bitmap.getHeight();
long width = bitmap.getWidth();
long calHeight = (long) ((height * maxMemory) / (bitmapByteCount * 1.1));
long calWidth = (long) ((width * maxMemory) / (bitmapByteCount * 1.1));
scaled = Bitmap.createScaledBitmap(bitmap, (int) Math.min(width, calWidth), (int) Math.min(height, calHeight), true);
} catch (IOException e) {
Timber.e(e);
} catch (NullPointerException e) {
Timber.e(e);
scaled = bitmap;
}
return scaled;
}
/**
* Calculate the starting and ending bounds for the zoomed-in image.
* Also set the container view's offset as the origin for the
* bounds, since that's the origin for the positioning animation
* properties (X, Y).
* @param startBounds the global visible rectangle of the thumbnail
* @param finalBounds the global visible rectangle of the container view
* @param globalOffset the container view's offset
* @return scaled start bounds
*/
float adjustStartEndBounds(Rect startBounds, Rect finalBounds, Point globalOffset) {
thumbView.getGlobalVisibleRect(startBounds);
flContainer.getGlobalVisibleRect(finalBounds, globalOffset);
startBounds.offset(-globalOffset.x, -globalOffset.y);
finalBounds.offset(-globalOffset.x, -globalOffset.y);
// Adjust the start bounds to be the same aspect ratio as the final
// bounds using the "center crop" technique. This prevents undesirable
// stretching during the animation. Also calculate the start scaling
// factor (the end scaling factor is always 1.0).
float startScale;
if ((float) finalBounds.width() / finalBounds.height()
> (float) startBounds.width() / startBounds.height()) {
// Extend start bounds horizontally
startScale = (float) startBounds.height() / finalBounds.height();
float startWidth = startScale * finalBounds.width();
float deltaWidth = (startWidth - startBounds.width()) / 2;
startBounds.left -= deltaWidth;
startBounds.right += deltaWidth;
} else {
// Extend start bounds vertically
startScale = (float) startBounds.width() / finalBounds.width();
float startHeight = startScale * finalBounds.height();
float deltaHeight = (startHeight - startBounds.height()) / 2;
startBounds.top -= deltaHeight;
startBounds.bottom += deltaHeight;
}
return startScale;
}
}

View file

@ -0,0 +1,22 @@
package fr.free.nrw.commons.utils;
import android.graphics.BitmapRegionDecoder;
import java.io.FileInputStream;
import java.io.IOException;
import javax.inject.Inject;
import javax.inject.Singleton;
@Singleton
public class BitmapRegionDecoderWrapper {
@Inject
public BitmapRegionDecoderWrapper() {
}
public BitmapRegionDecoder newInstance(FileInputStream file, boolean isSharable) throws IOException {
return BitmapRegionDecoder.newInstance(file, isSharable);
}
}

View file

@ -4,6 +4,11 @@ import android.content.Context;
import android.net.Uri;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Random;
import timber.log.Timber;
@ -29,9 +34,9 @@ public class ContributionUtils {
// TODO add exceptions for Google Drive URİ is needed
Uri result = null;
if (FileUtils.checkIfDirectoryExists(TEMP_EXTERNAL_DIRECTORY)) {
if (checkIfDirectoryExists(TEMP_EXTERNAL_DIRECTORY)) {
String destinationFilename = decideTempDestinationFileName();
result = FileUtils.saveFileFromURI(context, URIfromContentProvider, destinationFilename);
result = saveFileFromURI(context, URIfromContentProvider, destinationFilename);
} else { // If directory doesn't exist, create it and recursive call current method to check again
File file = new File(TEMP_EXTERNAL_DIRECTORY);
@ -53,29 +58,25 @@ public class ContributionUtils {
//TODO: do I have to notify file system about deletion?
File tempFile = new File(tempFileUri.getPath());
if (tempFile.exists()) {
boolean isDeleted= tempFile.delete();
boolean isDeleted = tempFile.delete();
Timber.e("removeTemporaryFile() parameters: URI tempFileUri %s, deleted status %b", tempFileUri, isDeleted);
}
}
private static String decideTempDestinationFileName() {
int i = 0;
while (true) {
if (new File(TEMP_EXTERNAL_DIRECTORY +File.separatorChar+i+"_tmp").exists()) {
// This file is in use, try enother file
i++;
} else {
// Use time stamp for file name, so that two temporary file never has same file name
// to prevent previous file reference bug
Long tsLong = System.currentTimeMillis()/1000;
String ts = tsLong.toString();
// For multiple uploads, time randomisation should be combined with another random
// parameter, since they created at same time
int multipleUploadRandomParameter = new Random().nextInt(100);
return TEMP_EXTERNAL_DIRECTORY +File.separatorChar+ts+multipleUploadRandomParameter+"_tmp";
}
while (new File(TEMP_EXTERNAL_DIRECTORY + File.separatorChar + i + "_tmp").exists()) {
i++;
}
// Use time stamp for file name, so that two temporary file never has same file name
// to prevent previous file reference bug
String timeStamp = String.valueOf(System.currentTimeMillis() / 1000);
// For multiple uploads, time randomisation should be combined with another random
// parameter, since they created at same time
int multipleUploadRandomParameter = new Random().nextInt(100);
return TEMP_EXTERNAL_DIRECTORY + File.separatorChar + timeStamp + multipleUploadRandomParameter + "_tmp";
}
public static void emptyTemporaryDirectory() {
@ -91,4 +92,58 @@ public class ContributionUtils {
}
}
}
/**
* Saves file from source URI to destination.
* @param sourceUri Uri which points to file to be saved
* @param destinationFilename where file will be located at
* @return Uri points to file saved
*/
private static Uri saveFileFromURI(Context context, Uri sourceUri, String destinationFilename) {
File file = new File(destinationFilename);
if (file.exists()) {
file.delete();
}
InputStream in = null;
OutputStream out = null;
try {
in = context.getContentResolver().openInputStream(sourceUri);
out = new FileOutputStream(new File(destinationFilename));
byte[] buf = new byte[1024];
int length;
while ((length = in.read(buf)) > 0) {
out.write(buf, 0, length);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (out != null) out.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
if (in != null) in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return Uri.parse("file://" + destinationFilename);
}
/**
* Checks if directory exists
* @param pathToCheck path of directory to check
* @return true if directory exists, false otherwise
*/
private static boolean checkIfDirectoryExists(String pathToCheck) {
File dir = new File(pathToCheck);
return dir.exists() && dir.isDirectory();
}
}

View file

@ -1,89 +0,0 @@
package fr.free.nrw.commons.utils;
import android.content.Context;
import android.net.Uri;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
/**
* Created for file operations
*/
public class FileUtils {
/**
* Saves file from source URI to destination.
* @param sourceUri Uri which points to file to be saved
* @param destinationFilename where file will be located at
* @return Uri points to file saved
*/
public static Uri saveFileFromURI(Context context, Uri sourceUri, String destinationFilename) {
File file = new File(destinationFilename);
if (file.exists()) {
file.delete();
}
InputStream in = null;
OutputStream out = null;
try {
in = context.getContentResolver().openInputStream(sourceUri);
out = new FileOutputStream(new File(destinationFilename));
byte[] buf = new byte[1024];
int len;
while((len=in.read(buf))>0){
out.write(buf,0,len);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if(out != null) {
out.close();
}
if(in != null) {
in.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
return Uri.parse("file://" + destinationFilename);
}
/**
* Checks if directory exists
* @param pathToCheck path of directory to check
* @return true if directory exists, false otherwise
*/
public static boolean checkIfDirectoryExists(String pathToCheck) {
File director = new File(pathToCheck);
if (director.exists() && director.isDirectory()) {
return true;
} else {
return false;
}
}
/**
* Creates new directory.
* @param pathToCreateAt where directory will be created at
* @return true if directory is created, false if an error occured, or already exists.
*/
public static boolean createDirectory(String pathToCreateAt) {
File directory = new File(pathToCreateAt);
if (!directory.exists()) {
return directory.mkdirs(); //true if directory is created
} else {
return false; //false if file already exists
}
}
}

View file

@ -9,6 +9,7 @@ import android.graphics.Rect;
import android.net.Uri;
import android.support.annotation.IntDef;
import android.support.annotation.Nullable;
import android.util.Log;
import com.facebook.common.executors.CallerThreadExecutor;
import com.facebook.common.references.CloseableReference;
@ -25,6 +26,7 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.location.LatLng;
import timber.log.Timber;
/**
@ -36,6 +38,7 @@ public class ImageUtils {
public static final int IMAGE_DARK = 1;
public static final int IMAGE_BLURRY = 1 << 1;
public static final int IMAGE_DUPLICATE = 1 << 2;
public static final int IMAGE_GEOLOCATION_DIFFERENT = 1 << 3;
public static final int IMAGE_OK = 0;
public static final int IMAGE_KEEP = -1;
public static final int IMAGE_WAIT = -2;
@ -54,7 +57,8 @@ public class ImageUtils {
IMAGE_WAIT,
EMPTY_TITLE,
FILE_NAME_EXISTS,
NO_CATEGORY_SELECTED
NO_CATEGORY_SELECTED,
IMAGE_GEOLOCATION_DIFFERENT
}
)
@Retention(RetentionPolicy.SOURCE)
@ -93,17 +97,30 @@ public class ImageUtils {
}
/**
* Pulls the pixels into an array and then runs through it while checking the brightness of each pixel.
* The calculation of brightness of each pixel is done by extracting the RGB constituents of the pixel
* and then applying the formula to calculate its "Luminance".
* Pixels with luminance greater than 40% are considered to be bright pixels while the ones with luminance
* greater than 26% but less than 40% are considered to be pixels with medium brightness. The rest are
* dark pixels.
* If the number of bright pixels is more than 2.5% or the number of pixels with medium brightness is
* more than 30% of the total number of pixels then the image is considered to be OK else dark.
* @param bitmap The bitmap that needs to be checked.
* @return true if bitmap is dark or null, false if bitmap is bright
* @param geolocationOfFileString Geolocation of image. If geotag doesn't exists, then this will be an empty string
* @param wikidataItemLocationString Location of wikidata item will be edited after upload
* @return false if image is neither dark nor blurry or if the input bitmapRegionDecoder provided is null
* true if geolocation of the image and wikidata item are different
*/
public static boolean checkImageGeolocationIsDifferent(String geolocationOfFileString, String wikidataItemLocationString) {
Timber.d("Comparing geolocation of file with nearby place location");
if (geolocationOfFileString == null || geolocationOfFileString == "") { // Means that geolocation for this image is not given
return false; // Since we don't know geolocation of file, we choose letting upload
}
String[] geolocationOfFile = geolocationOfFileString.split("\\|");
String[] wikidataItemLocation = wikidataItemLocationString.split("/");
Double distance = LengthUtils.computeDistanceBetween(
new LatLng(Double.parseDouble(geolocationOfFile[0]),Double.parseDouble(geolocationOfFile[1]),0)
, new LatLng(Double.parseDouble(wikidataItemLocation[0]), Double.parseDouble(wikidataItemLocation[1]),0));
if ( distance >= 1000 ) {// Distance is more than 1 km, means that geolocation is wrong
return true;
} else {
return false;
}
}
private static boolean checkIfImageIsDark(Bitmap bitmap) {
if (bitmap == null) {
Timber.e("Expected bitmap was null");
@ -206,24 +223,37 @@ public class ImageUtils {
}
public static String getErrorMessageForResult(Context context, @Result int result) {
String errorMessage;
if (result == ImageUtils.IMAGE_DARK)
errorMessage = context.getString(R.string.upload_image_problem_dark);
else if (result == ImageUtils.IMAGE_BLURRY)
errorMessage = context.getString(R.string.upload_image_problem_blurry);
else if (result == ImageUtils.IMAGE_DUPLICATE)
errorMessage = context.getString(R.string.upload_image_problem_duplicate);
else if (result == (ImageUtils.IMAGE_DARK|ImageUtils.IMAGE_BLURRY))
errorMessage = context.getString(R.string.upload_image_problem_dark_blurry);
else if (result == (ImageUtils.IMAGE_DARK|ImageUtils.IMAGE_DUPLICATE))
errorMessage = context.getString(R.string.upload_image_problem_dark_duplicate);
else if (result == (ImageUtils.IMAGE_BLURRY|ImageUtils.IMAGE_DUPLICATE))
errorMessage = context.getString(R.string.upload_image_problem_blurry_duplicate);
else if (result == (ImageUtils.IMAGE_DARK|ImageUtils.IMAGE_BLURRY|ImageUtils.IMAGE_DUPLICATE))
errorMessage = context.getString(R.string.upload_image_problem_dark_blurry_duplicate);
else
return "";
/**
* Result variable is a result of an or operation of all possbile problems. Ie. if result
* is 0001 means IMAGE_DARK, if result is 1100 IMAGE_DUPLICATE and IMAGE_GEOLOCATION_DIFFERENT
*/
StringBuilder errorMessage = new StringBuilder();
if (((IMAGE_DARK | IMAGE_GEOLOCATION_DIFFERENT | IMAGE_BLURRY | IMAGE_DUPLICATE) & result) == 0 ) {
Timber.d("No issues to warn user is found");
} else {
Timber.d("Issues found to warn user");
return errorMessage;
errorMessage.append(context.getResources().getString(R.string.upload_problem_exist));
if ((IMAGE_DARK & result) != 0 ) { // We are checking image dark bit to see if that bit is set or not
errorMessage.append("\n - ").append(context.getResources().getString(R.string.upload_problem_image_dark));
}
if ((IMAGE_BLURRY & result) != 0 ) {
errorMessage.append("\n - ").append(context.getResources().getString(R.string.upload_problem_image_blurry));
}
if ((IMAGE_DUPLICATE & result) != 0 ) {
errorMessage.append("\n - ").append(context.getResources().getString(R.string.upload_problem_image_duplicate));
}
if ((IMAGE_GEOLOCATION_DIFFERENT & result) != 0 ) {
errorMessage.append("\n - ").append(context.getResources().getString(R.string.upload_problem_different_geolocation));
}
errorMessage.append("\n\n").append(context.getResources().getString(R.string.upload_problem_do_you_continue));
}
return errorMessage.toString();
}
}

View file

@ -0,0 +1,25 @@
package fr.free.nrw.commons.utils;
import android.graphics.BitmapRegionDecoder;
import javax.inject.Inject;
import javax.inject.Singleton;
import static fr.free.nrw.commons.utils.ImageUtils.*;
@Singleton
public class ImageUtilsWrapper {
@Inject
public ImageUtilsWrapper() {
}
public @Result int checkIfImageIsTooDark(BitmapRegionDecoder bitmapRegionDecoder) {
return ImageUtils.checkIfImageIsTooDark(bitmapRegionDecoder);
}
public boolean checkImageGeolocationIsDifferent(String geolocationOfFileString, String wikidataItemLocationString) {
return ImageUtils.checkImageGeolocationIsDifferent(geolocationOfFileString, wikidataItemLocationString);
}
}

View file

@ -0,0 +1,13 @@
package fr.free.nrw.commons.utils;
import fr.free.nrw.commons.location.LatLng;
public class LocationUtils {
public static LatLng mapBoxLatLngToCommonsLatLng(com.mapbox.mapboxsdk.geometry.LatLng mapBoxLatLng) {
return new LatLng(mapBoxLatLng.getLatitude(), mapBoxLatLng.getLongitude(), 0);
}
public static com.mapbox.mapboxsdk.geometry.LatLng comonsLatLngToMapBoxLatLng(LatLng commonsLatLng) {
return new com.mapbox.mapboxsdk.geometry.LatLng(commonsLatLng.getLatitude(), commonsLatLng.getLongitude());
}
}

View file

@ -0,0 +1,25 @@
package fr.free.nrw.commons.utils;
import fr.free.nrw.commons.location.LatLng;
public class PlaceUtils {
/**
* Converts our defined LatLng to string, to put as String
* @param latLng latlang will be converted to string
* @return latitude + "/" + longitude
*/
public static String latLangToString(LatLng latLng) {
return latLng.getLatitude()+"/"+latLng.getLongitude();
}
/**
* Converts latitude + "/" + longitude string to commons LatLng
* @param latLngString latitude + "/" + longitude string
* @return commons LatLng
*/
public static LatLng stringToLatLng(String latLngString) {
String[] parts = latLngString.split("/");
return new LatLng(Double.parseDouble(parts[0]), Double.parseDouble(parts[1]), 0);
}
}

View file

@ -0,0 +1,72 @@
package fr.free.nrw.commons.utils;
import android.content.Context;
import android.content.res.Resources;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v7.widget.CardView;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
/**
* A card view which informs onSwipe events to its child
*/
public abstract class SwipableCardView extends CardView {
float x1, x2;
private static final float MINIMUM_THRESHOLD_FOR_SWIPE = 100;
public SwipableCardView(@NonNull Context context) {
super(context);
interceptOnTouchListener();
}
public SwipableCardView(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
interceptOnTouchListener();
}
public SwipableCardView(@NonNull Context context, @Nullable AttributeSet attrs,
int defStyleAttr) {
super(context, attrs, defStyleAttr);
interceptOnTouchListener();
}
private void interceptOnTouchListener() {
this.setOnTouchListener((v, event) -> {
boolean isSwipe = false;
float deltaX = 0.0f;
Log.e("#SwipableCardView#", event.getAction() + "");
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
x1 = event.getX();
break;
case MotionEvent.ACTION_UP:
x2 = event.getX();
deltaX = x2 - x1;
if (deltaX < 0) {
//Right to left swipe
isSwipe = true;
} else if (deltaX > 0) {
//Left to right swipe
isSwipe = true;
}
break;
}
if (isSwipe && (pixelToDp(Math.abs(deltaX)) > MINIMUM_THRESHOLD_FOR_SWIPE)) {
return onSwipe(v);
}
return false;
});
}
/**
* abstract function which informs swipe events to those who have inherited from it
*/
public abstract boolean onSwipe(View view);
private float pixelToDp(float pixels) {
return (pixels / Resources.getSystem().getDisplayMetrics().density);
}
}

View file

@ -1,4 +1,4 @@
package fr.free.nrw.commons.upload;
package fr.free.nrw.commons.widget;
import android.app.Activity;
import android.content.Context;
@ -13,10 +13,7 @@ import android.view.Display;
* Created by Ilgaz Er on 8/7/2018.
*/
public class HeightLimitedRecyclerView extends RecyclerView {
int height;
public HeightLimitedRecyclerView(Context context) {
super(context);
DisplayMetrics displayMetrics = new DisplayMetrics();

View file

@ -2,4 +2,5 @@ package fr.free.nrw.commons.wikidata;
public class WikidataConstants {
public static final String WIKIDATA_ENTITY_ID_PREF = "WikiDataEntityId";
public static final String WIKIDATA_ITEM_LOCATION = "WikiDataItemLocation";
}

View file

@ -58,6 +58,11 @@ public class WikidataEditService {
return;
}
if (!(directPrefs.getBoolean("Picture_Has_Correct_Location",true))) {
Timber.d("Image location and nearby place location mismatched, so Wikidata item won't be edited");
return;
}
editWikidataProperty(wikidataEntityId, fileName);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 807 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 542 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

View file

@ -0,0 +1,59 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="1024dp"
android:height="1376dp"
android:viewportWidth="610"
android:viewportHeight="820">
<path
android:pathData="M305,516m-100,0a100,100 0,1 1,200 0a100,100 0,1 1,-200 0"
android:fillColor="#900"/>
<path
android:pathData="m294,696v118h22v-118"
android:fillColor="#069"/>
<path
android:pathData="m262,701l43,-75 43,75"
android:fillColor="#069"/>
<path
android:pathData="m169.943,635.501l-83.439,83.439l15.556,15.556l83.439,-83.439"
android:fillColor="#069"/>
<path
android:pathData="m143.78,616.409l83.439,-22.627 -22.627,83.439"
android:fillColor="#069"/>
<path
android:pathData="m125,505l-118,0l-0,22l118,0"
android:fillColor="#069"/>
<path
android:pathData="m120,473l75,43 -75,43"
android:fillColor="#069"/>
<path
android:pathData="m185.499,380.943l-83.439,-83.439l-15.556,15.556l83.439,83.439"
android:fillColor="#069"/>
<path
android:pathData="m204.591,354.78l22.627,83.439 -83.439,-22.627"
android:fillColor="#069"/>
<path
android:pathData="m424.501,651.057l83.439,83.439l15.556,-15.556l-83.439,-83.439"
android:fillColor="#069"/>
<path
android:pathData="m405.409,677.22l-22.627,-83.439 83.439,22.627"
android:fillColor="#069"/>
<path
android:pathData="m485,527l118,-0l0,-22l-118,-0"
android:fillColor="#069"/>
<path
android:pathData="m490,559l-75,-43 75,-43"
android:fillColor="#069"/>
<path
android:pathData="m440.057,396.499l83.439,-83.439l-15.556,-15.556l-83.439,83.439"
android:fillColor="#069"/>
<path
android:pathData="m466.22,415.591l-83.439,22.627 22.627,-83.439"
android:fillColor="#069"/>
<path
android:pathData="M123.981,334.981A256,256 0,1 0,486.019 334.981C415.309,264.27 308.536,300.332 287.322,144.769"
android:strokeWidth="84"
android:fillColor="#00000000"
android:strokeColor="#069"/>
<path
android:pathData="m282,1s-36,135 -80,185 116,-62 170,-5 -90,-180 -90,-180z"
android:fillColor="#069"/>
</vector>

View file

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M19,9h-4V3H9v6H5l7,7 7,-7zM5,18v2h14v-2H5z"/>
</vector>

View file

@ -0,0 +1,62 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="1639.375"
android:viewportHeight="1640">
<group android:translateX="514.6875"
android:translateY="410">
<path
android:pathData="M305,516m-100,0a100,100 0,1 1,200 0a100,100 0,1 1,-200 0"
android:fillColor="#900"/>
<path
android:pathData="m294,696v118h22v-118"
android:fillColor="#069"/>
<path
android:pathData="m262,701l43,-75 43,75"
android:fillColor="#069"/>
<path
android:pathData="m169.943,635.501l-83.439,83.439l15.556,15.556l83.439,-83.439"
android:fillColor="#069"/>
<path
android:pathData="m143.78,616.409l83.439,-22.627 -22.627,83.439"
android:fillColor="#069"/>
<path
android:pathData="m125,505l-118,0l-0,22l118,0"
android:fillColor="#069"/>
<path
android:pathData="m120,473l75,43 -75,43"
android:fillColor="#069"/>
<path
android:pathData="m185.499,380.943l-83.439,-83.439l-15.556,15.556l83.439,83.439"
android:fillColor="#069"/>
<path
android:pathData="m204.591,354.78l22.627,83.439 -83.439,-22.627"
android:fillColor="#069"/>
<path
android:pathData="m424.501,651.057l83.439,83.439l15.556,-15.556l-83.439,-83.439"
android:fillColor="#069"/>
<path
android:pathData="m405.409,677.22l-22.627,-83.439 83.439,22.627"
android:fillColor="#069"/>
<path
android:pathData="m485,527l118,-0l0,-22l-118,-0"
android:fillColor="#069"/>
<path
android:pathData="m490,559l-75,-43 75,-43"
android:fillColor="#069"/>
<path
android:pathData="m440.057,396.499l83.439,-83.439l-15.556,-15.556l-83.439,83.439"
android:fillColor="#069"/>
<path
android:pathData="m466.22,415.591l-83.439,22.627 22.627,-83.439"
android:fillColor="#069"/>
<path
android:pathData="M123.981,334.981A256,256 0,1 0,486.019 334.981C415.309,264.27 308.536,300.332 287.322,144.769"
android:strokeWidth="84"
android:fillColor="#00000000"
android:strokeColor="#069"/>
<path
android:pathData="m282,1s-36,135 -80,185 116,-62 170,-5 -90,-180 -90,-180z"
android:fillColor="#069"/>
</group>
</vector>

View file

@ -9,21 +9,6 @@
android:gravity="center"
android:orientation="horizontal">
<TextView
android:id="@+id/welcomeYesButton"
android:layout_width="wrap_content"
android:layout_height="@dimen/overflow_button_dimen"
android:layout_marginEnd="@dimen/standard_gap"
android:layout_marginRight="@dimen/standard_gap"
android:layout_marginTop="@dimen/standard_gap"
android:text="@string/welcome_skip_button"
android:textColor="#fff"
android:textSize="@dimen/normal_text"
android:textStyle="bold"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<android.support.constraint.Guideline
android:id="@+id/center_guideline"
android:layout_width="wrap_content"

View file

@ -6,21 +6,6 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
android:background="#0c609c">
<TextView
android:id="@+id/welcomeYesButton"
android:layout_width="wrap_content"
android:layout_height="@dimen/overflow_button_dimen"
android:layout_marginEnd="@dimen/standard_gap"
android:layout_marginRight="@dimen/standard_gap"
android:layout_marginTop="@dimen/standard_gap"
android:text="@string/welcome_skip_button"
android:textColor="#fff"
android:textSize="@dimen/normal_text"
android:textStyle="bold"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<android.support.constraint.Guideline
android:id="@+id/center_guideline"
android:layout_width="wrap_content"

View file

@ -14,7 +14,6 @@
android:layout_marginBottom="@dimen/large_gap"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/welcome_help_button_text"
android:id="@+id/welcomeInfo"
android:layout_gravity="end|top"
android:layout_marginTop="@dimen/standard_gap"
@ -73,7 +72,7 @@
android:layout_height="@dimen/overflow_button_dimen"
android:layout_marginTop="@dimen/standard_gap"
android:text="@string/welcome_final_button_text"
android:id="@+id/welcomeYesButton"
android:id="@+id/finishTutorialButton"
android:layout_gravity="center"
android:background="@android:color/white"
android:textColor="#0c609c"

View file

@ -8,21 +8,6 @@
android:gravity="center"
android:orientation="horizontal">
<TextView
android:id="@+id/welcomeYesButton"
android:layout_width="wrap_content"
android:layout_height="@dimen/overflow_button_dimen"
android:layout_marginEnd="@dimen/standard_gap"
android:layout_marginRight="@dimen/standard_gap"
android:layout_marginTop="@dimen/standard_gap"
android:text="@string/welcome_skip_button"
android:textColor="#fff"
android:textSize="@dimen/normal_text"
android:textStyle="bold"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<android.support.constraint.Guideline
android:id="@+id/center_guideline"
android:layout_width="wrap_content"

View file

@ -7,21 +7,6 @@
android:layout_height="match_parent"
android:background="#0c609c">
<TextView
android:id="@+id/welcomeYesButton"
android:layout_width="wrap_content"
android:layout_height="@dimen/overflow_button_dimen"
android:layout_marginEnd="@dimen/standard_gap"
android:layout_marginRight="@dimen/standard_gap"
android:layout_marginTop="@dimen/standard_gap"
android:text="@string/welcome_skip_button"
android:textColor="#fff"
android:textSize="@dimen/normal_text"
android:textStyle="bold"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<android.support.constraint.Guideline
android:id="@+id/center_guideline"
android:layout_width="wrap_content"

View file

@ -26,7 +26,7 @@
android:id="@+id/backgroundImage"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:actualImageScaleType="centerCrop" />
app:actualImageScaleType="fitCenter" />
<FrameLayout
android:id="@+id/single_upload_fragment_container"

View file

@ -23,7 +23,7 @@
android:layout_height="match_parent"
android:layout_below="@id/toolbar"
android:background="@color/commons_app_blue_dark"
app:actualImageScaleType="centerCrop" />
app:actualImageScaleType="fitCenter" />
<android.support.constraint.ConstraintLayout
android:id="@+id/activity_upload_cards"

View file

@ -49,7 +49,7 @@
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_expand_less_black_24dp" />
<fr.free.nrw.commons.upload.HeightLimitedRecyclerView
<fr.free.nrw.commons.widget.HeightLimitedRecyclerView
android:id="@+id/rv_descriptions"
android:layout_width="match_parent"
android:layout_height="wrap_content"

Some files were not shown because too many files have changed in this diff Show more