diff --git a/.travis.yml b/.travis.yml index d9abeedbe..3d330c506 100644 --- a/.travis.yml +++ b/.travis.yml @@ -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 diff --git a/README.md b/README.md index 28b923351..54649a830 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/app/build.gradle b/app/build.gradle index bff2a631b..9accea101 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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" + } + } +} diff --git a/app/src/betaDebug/ic_launcher-web.png b/app/src/betaDebug/ic_launcher-web.png new file mode 100644 index 000000000..5b1546360 Binary files /dev/null and b/app/src/betaDebug/ic_launcher-web.png differ diff --git a/app/src/betaDebug/res/drawable-hdpi/ic_launcher.png b/app/src/betaDebug/res/drawable-hdpi/ic_launcher.png deleted file mode 100644 index 46c0a4202..000000000 Binary files a/app/src/betaDebug/res/drawable-hdpi/ic_launcher.png and /dev/null differ diff --git a/app/src/betaDebug/res/drawable-mdpi/ic_launcher.png b/app/src/betaDebug/res/drawable-mdpi/ic_launcher.png deleted file mode 100644 index 2e5499676..000000000 Binary files a/app/src/betaDebug/res/drawable-mdpi/ic_launcher.png and /dev/null differ diff --git a/app/src/betaDebug/res/drawable-xhdpi/ic_launcher.png b/app/src/betaDebug/res/drawable-xhdpi/ic_launcher.png deleted file mode 100644 index 0f0c702ed..000000000 Binary files a/app/src/betaDebug/res/drawable-xhdpi/ic_launcher.png and /dev/null differ diff --git a/app/src/betaDebug/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/betaDebug/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 000000000..036d09bc5 --- /dev/null +++ b/app/src/betaDebug/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/betaDebug/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/betaDebug/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 000000000..036d09bc5 --- /dev/null +++ b/app/src/betaDebug/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/betaDebug/res/mipmap-hdpi/ic_launcher.png b/app/src/betaDebug/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..90c044ccd Binary files /dev/null and b/app/src/betaDebug/res/mipmap-hdpi/ic_launcher.png differ diff --git a/app/src/betaDebug/res/mipmap-hdpi/ic_launcher_foreground.png b/app/src/betaDebug/res/mipmap-hdpi/ic_launcher_foreground.png new file mode 100644 index 000000000..f826d5544 Binary files /dev/null and b/app/src/betaDebug/res/mipmap-hdpi/ic_launcher_foreground.png differ diff --git a/app/src/betaDebug/res/mipmap-hdpi/ic_launcher_round.png b/app/src/betaDebug/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 000000000..9b273c43f Binary files /dev/null and b/app/src/betaDebug/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/app/src/betaDebug/res/mipmap-mdpi/ic_launcher.png b/app/src/betaDebug/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..b09b8d252 Binary files /dev/null and b/app/src/betaDebug/res/mipmap-mdpi/ic_launcher.png differ diff --git a/app/src/betaDebug/res/mipmap-mdpi/ic_launcher_foreground.png b/app/src/betaDebug/res/mipmap-mdpi/ic_launcher_foreground.png new file mode 100644 index 000000000..5002ec69d Binary files /dev/null and b/app/src/betaDebug/res/mipmap-mdpi/ic_launcher_foreground.png differ diff --git a/app/src/betaDebug/res/mipmap-mdpi/ic_launcher_round.png b/app/src/betaDebug/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 000000000..9aa2611ba Binary files /dev/null and b/app/src/betaDebug/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/app/src/betaDebug/res/mipmap-xhdpi/ic_launcher.png b/app/src/betaDebug/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..d7b349b4d Binary files /dev/null and b/app/src/betaDebug/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/app/src/betaDebug/res/mipmap-xhdpi/ic_launcher_foreground.png b/app/src/betaDebug/res/mipmap-xhdpi/ic_launcher_foreground.png new file mode 100644 index 000000000..9297963fd Binary files /dev/null and b/app/src/betaDebug/res/mipmap-xhdpi/ic_launcher_foreground.png differ diff --git a/app/src/betaDebug/res/mipmap-xhdpi/ic_launcher_round.png b/app/src/betaDebug/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 000000000..59b088069 Binary files /dev/null and b/app/src/betaDebug/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/app/src/betaDebug/res/mipmap-xxhdpi/ic_launcher.png b/app/src/betaDebug/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..d473d0aed Binary files /dev/null and b/app/src/betaDebug/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/app/src/betaDebug/res/mipmap-xxhdpi/ic_launcher_foreground.png b/app/src/betaDebug/res/mipmap-xxhdpi/ic_launcher_foreground.png new file mode 100644 index 000000000..aeb616311 Binary files /dev/null and b/app/src/betaDebug/res/mipmap-xxhdpi/ic_launcher_foreground.png differ diff --git a/app/src/betaDebug/res/mipmap-xxhdpi/ic_launcher_round.png b/app/src/betaDebug/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 000000000..0b7797049 Binary files /dev/null and b/app/src/betaDebug/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/app/src/betaDebug/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/betaDebug/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..e88874931 Binary files /dev/null and b/app/src/betaDebug/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/app/src/betaDebug/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/app/src/betaDebug/res/mipmap-xxxhdpi/ic_launcher_foreground.png new file mode 100644 index 000000000..fa5017d72 Binary files /dev/null and b/app/src/betaDebug/res/mipmap-xxxhdpi/ic_launcher_foreground.png differ diff --git a/app/src/betaDebug/res/mipmap-xxxhdpi/ic_launcher_round.png b/app/src/betaDebug/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 000000000..00a9e4bd5 Binary files /dev/null and b/app/src/betaDebug/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/app/src/betaDebug/res/values/ic_launcher_background.xml b/app/src/betaDebug/res/values/ic_launcher_background.xml new file mode 100644 index 000000000..c5d5899fd --- /dev/null +++ b/app/src/betaDebug/res/values/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + #FFFFFF + \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ad76ee14d..559470c84 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -24,7 +24,7 @@ @@ -44,7 +44,7 @@ @@ -65,7 +65,7 @@ diff --git a/app/src/main/ic_launcher-web.png b/app/src/main/ic_launcher-web.png new file mode 100644 index 000000000..c7f0bc3fe Binary files /dev/null and b/app/src/main/ic_launcher-web.png differ diff --git a/app/src/main/java/fr/free/nrw/commons/BasePresenter.java b/app/src/main/java/fr/free/nrw/commons/BasePresenter.java new file mode 100644 index 000000000..041fde6b2 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/BasePresenter.java @@ -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(); +} diff --git a/app/src/main/java/fr/free/nrw/commons/CommonsApplication.java b/app/src/main/java/fr/free/nrw/commons/CommonsApplication.java index d874bae40..61beb9a6b 100644 --- a/app/src/main/java/fr/free/nrw/commons/CommonsApplication.java +++ b/app/src/main/java/fr/free/nrw/commons/CommonsApplication.java @@ -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() { diff --git a/app/src/main/java/fr/free/nrw/commons/MvpView.java b/app/src/main/java/fr/free/nrw/commons/MvpView.java new file mode 100644 index 000000000..7485b2aaf --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/MvpView.java @@ -0,0 +1,8 @@ +package fr.free.nrw.commons; + +/** + * Base interface for all the views + */ +public interface MvpView { + void showMessage(String message); +} diff --git a/app/src/main/java/fr/free/nrw/commons/WelcomeActivity.java b/app/src/main/java/fr/free/nrw/commons/WelcomeActivity.java index 109d115d4..21bc8af20 100644 --- a/app/src/main/java/fr/free/nrw/commons/WelcomeActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/WelcomeActivity.java @@ -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(); + } } diff --git a/app/src/main/java/fr/free/nrw/commons/WelcomePagerAdapter.java b/app/src/main/java/fr/free/nrw/commons/WelcomePagerAdapter.java index f2c6d1054..4776abfe4 100644 --- a/app/src/main/java/fr/free/nrw/commons/WelcomePagerAdapter.java +++ b/app/src/main/java/fr/free/nrw/commons/WelcomePagerAdapter.java @@ -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(); } } diff --git a/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.java b/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.java index f4d5dd7c9..320a896eb 100644 --- a/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.java @@ -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(); } diff --git a/app/src/main/java/fr/free/nrw/commons/auth/SessionManager.java b/app/src/main/java/fr/free/nrw/commons/auth/SessionManager.java index e8745e25b..20cd06ed8 100644 --- a/app/src/main/java/fr/free/nrw/commons/auth/SessionManager.java +++ b/app/src/main/java/fr/free/nrw/commons/auth/SessionManager.java @@ -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(); diff --git a/app/src/main/java/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationsFragment.java b/app/src/main/java/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationsFragment.java index 5d67c3093..c5c445cee 100644 --- a/app/src/main/java/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationsFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationsFragment.java @@ -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", diff --git a/app/src/main/java/fr/free/nrw/commons/campaigns/Campaign.java b/app/src/main/java/fr/free/nrw/commons/campaigns/Campaign.java new file mode 100644 index 000000000..2bd4893b8 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/campaigns/Campaign.java @@ -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; + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/campaigns/CampaignConfig.java b/app/src/main/java/fr/free/nrw/commons/campaigns/CampaignConfig.java new file mode 100644 index 000000000..a715aaf63 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/campaigns/CampaignConfig.java @@ -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; +} diff --git a/app/src/main/java/fr/free/nrw/commons/campaigns/CampaignResponseDTO.java b/app/src/main/java/fr/free/nrw/commons/campaigns/CampaignResponseDTO.java new file mode 100644 index 000000000..dd0bd51ce --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/campaigns/CampaignResponseDTO.java @@ -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 campaigns; + + public CampaignConfig getCampaignConfig() { + return campaignConfig; + } + + public List getCampaigns() { + return campaigns; + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/campaigns/CampaignView.java b/app/src/main/java/fr/free/nrw/commons/campaigns/CampaignView.java new file mode 100644 index 000000000..dec62cc1b --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/campaigns/CampaignView.java @@ -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(); + } + } + } + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/campaigns/CampaignsPresenter.java b/app/src/main/java/fr/free/nrw/commons/campaigns/CampaignsPresenter.java new file mode 100644 index 000000000..c9dac27af --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/campaigns/CampaignsPresenter.java @@ -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 campaigns = mediaWikiApi.getCampaigns(); + campaigns.observeOn(AndroidSchedulers.mainThread()) + .subscribeOn(Schedulers.io()) + .subscribeWith(new SingleObserver() { + + @Override public void onSubscribe(Disposable d) { + disposable = d; + } + + @Override public void onSuccess(CampaignResponseDTO campaignResponseDTO) { + List 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()); + } + }); + } + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/campaigns/ICampaignsView.java b/app/src/main/java/fr/free/nrw/commons/campaigns/ICampaignsView.java new file mode 100644 index 000000000..8610728b3 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/campaigns/ICampaignsView.java @@ -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); +} diff --git a/app/src/main/java/fr/free/nrw/commons/category/CategoryImageUtils.java b/app/src/main/java/fr/free/nrw/commons/category/CategoryImageUtils.java index fdcae3aad..b246fb98e 100644 --- a/app/src/main/java/fr/free/nrw/commons/category/CategoryImageUtils.java +++ b/app/src/main/java/fr/free/nrw/commons/category/CategoryImageUtils.java @@ -26,9 +26,11 @@ public class CategoryImageUtils { */ public static List getMediaList(NodeList childNodes) { List 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 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; diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionController.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionController.java index c3a0f329b..82ad7765a 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionController.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionController.java @@ -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"); diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsFragment.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsFragment.java index 50666e65a..8b43c9f90 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsFragment.java @@ -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(); + } } diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListFragment.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListFragment.java index b238c8b8b..65c997a3b 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListFragment.java @@ -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); } } diff --git a/app/src/main/java/fr/free/nrw/commons/delete/ReasonBuilder.java b/app/src/main/java/fr/free/nrw/commons/delete/ReasonBuilder.java new file mode 100644 index 000000000..5a1700ec1 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/delete/ReasonBuilder.java @@ -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; + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/explore/SearchActivity.java b/app/src/main/java/fr/free/nrw/commons/explore/SearchActivity.java index f5a28304d..cfe4f2657 100644 --- a/app/src/main/java/fr/free/nrw/commons/explore/SearchActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/explore/SearchActivity.java @@ -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(); diff --git a/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesFragment.java b/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesFragment.java index 173d93129..289def4b2 100644 --- a/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesFragment.java @@ -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(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(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(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(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(context, R.layout.item_recent_searches_dark_theme, recentSearches); + } else { + adapter = new ArrayAdapter(context, R.layout.item_recent_searches, recentSearches); + } } } diff --git a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java index 4b28a40fb..32870e897 100644 --- a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java @@ -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 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 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 languageAdapter = new ArrayAdapter(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); } } diff --git a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailPagerFragment.java b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailPagerFragment.java index ad5bf77e1..4eb69af7c 100644 --- a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailPagerFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailPagerFragment.java @@ -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; } diff --git a/app/src/main/java/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApi.java b/app/src/main/java/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApi.java index f35bb7f24..5ad6b52fc 100644 --- a/app/src/main/java/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApi.java +++ b/app/src/main/java/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApi.java @@ -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 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; + }); + } } diff --git a/app/src/main/java/fr/free/nrw/commons/mwapi/MediaWikiApi.java b/app/src/main/java/fr/free/nrw/commons/mwapi/MediaWikiApi.java index d7bf65802..46d71dc26 100644 --- a/app/src/main/java/fr/free/nrw/commons/mwapi/MediaWikiApi.java +++ b/app/src/main/java/fr/free/nrw/commons/mwapi/MediaWikiApi.java @@ -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 getCampaigns(); + interface ProgressListener { void onProgress(long transferred, long total); } diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyController.java b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyController.java index a84e86218..abf02362f 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyController.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyController.java @@ -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 places = nearbyPlaces.getFromWikidataQuery(curLatLng, Locale.getDefault().getLanguage(), returnClosestResult); + List 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 { diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyFragment.java b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyFragment.java index 9eba63cf4..5a1c48edf 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyFragment.java @@ -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 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 diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyListFragment.java b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyListFragment.java index 3cf60c31d..c6423f8ab 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyListFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyListFragment.java @@ -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 placeList) { + try { + adapterFactory.updateAdapterData(placeList, (RVRendererAdapter) recyclerView.getAdapter()); + } catch (NullPointerException e) { + Timber.e("Null pointer exception from calling recyclerView.getAdapter()"); + } + } + private List getPlaceListFromBundle(Bundle bundle) { List 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", diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyMapFragment.java b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyMapFragment.java index c8c1d3ebd..8d8f4989f 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyMapFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyMapFragment.java @@ -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 placeList) { + List 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 customNearbyBaseMarker) { + List 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", diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyNoificationCardView.java b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyNoificationCardView.java index 61798a95a..c9f0e0ff2 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyNoificationCardView.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyNoificationCardView.java @@ -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; } /** diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyPlaces.java b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyPlaces.java index 6ca4af318..74af35de5 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyPlaces.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyPlaces.java @@ -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 diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/PlaceRenderer.java b/app/src/main/java/fr/free/nrw/commons/nearby/PlaceRenderer.java index 9ea492e81..d709ae47d 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/PlaceRenderer.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/PlaceRenderer.java @@ -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 { @@ -193,6 +195,7 @@ public class PlaceRenderer extends Renderer { 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(); } diff --git a/app/src/main/java/fr/free/nrw/commons/notification/NotificationRenderer.java b/app/src/main/java/fr/free/nrw/commons/notification/NotificationRenderer.java index f843cda66..22bebb37a 100644 --- a/app/src/main/java/fr/free/nrw/commons/notification/NotificationRenderer.java +++ b/app/src/main/java/fr/free/nrw/commons/notification/NotificationRenderer.java @@ -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 { - @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 { 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{ diff --git a/app/src/main/java/fr/free/nrw/commons/upload/DescriptionsAdapter.java b/app/src/main/java/fr/free/nrw/commons/upload/DescriptionsAdapter.java index 5c5fe8877..7186a519f 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/DescriptionsAdapter.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/DescriptionsAdapter.java @@ -174,7 +174,10 @@ class DescriptionsAdapter extends RecyclerView.Adapter{ + descriptions.get(position - 1).setDescriptionText(descriptionText); + })); + descItemEditText.setOnFocusChangeListener((v, hasFocus) -> { if (!hasFocus) { ViewUtil.hideKeyboard(v); diff --git a/app/src/main/java/fr/free/nrw/commons/upload/FileProcessor.java b/app/src/main/java/fr/free/nrw/commons/upload/FileProcessor.java index c5b6df227..85205f079 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/FileProcessor.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/FileProcessor.java @@ -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"); diff --git a/app/src/main/java/fr/free/nrw/commons/upload/FileUtils.java b/app/src/main/java/fr/free/nrw/commons/upload/FileUtils.java index 9401c941e..3d9e8442c 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/FileUtils.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/FileUtils.java @@ -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); + } } \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/upload/FileUtilsWrapper.java b/app/src/main/java/fr/free/nrw/commons/upload/FileUtilsWrapper.java new file mode 100644 index 000000000..e5119c095 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/upload/FileUtilsWrapper.java @@ -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); + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/upload/GPSExtractor.java b/app/src/main/java/fr/free/nrw/commons/upload/GPSExtractor.java index a6b150c42..4559375c6 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/GPSExtractor.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/GPSExtractor.java @@ -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; } diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadActivity.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadActivity.java index d71bc0985..647920dc3 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/UploadActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadActivity.java @@ -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 = "" + getString(Utils.licenseNameFor(selectedLicense)) + "
"; 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); diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadController.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadController.java index fd0563ab3..680a16714 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/UploadController.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadController.java @@ -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) { diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadModel.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadModel.java index 46193e958..f4e17543e 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/UploadModel.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadModel.java @@ -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 licenses, @Named("default_preferences") SharedPreferences prefs, @Named("licenses_by_name") Map 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 descriptions) { + void setCurrentTitleAndDescriptions(Title title, List 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 getItems() { + return items; + } @SuppressWarnings("WeakerAccess") static class UploadItem { @@ -397,4 +418,4 @@ public class UploadModel { } } -} \ No newline at end of file +} diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadPresenter.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadPresenter.java index 74e3192bd..781c158bd 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/UploadPresenter.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadPresenter.java @@ -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()); } /** diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadService.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadService.java index e7920f317..0873d46d8 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/UploadService.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadService.java @@ -88,7 +88,7 @@ public class UploadService extends HandlerService { 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; diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadView.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadView.java index 410914446..a91574004 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/UploadView.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadView.java @@ -60,7 +60,7 @@ public interface UploadView { void updateLicenses(List licenses, String selectedLicense); - void updateLicenseSummary(String selectedLicense); + void updateLicenseSummary(String selectedLicense, int imageCount); void updateTopCardContent(); diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UrlLicense.java b/app/src/main/java/fr/free/nrw/commons/upload/UrlLicense.java deleted file mode 100644 index a28fde579..000000000 --- a/app/src/main/java/fr/free/nrw/commons/upload/UrlLicense.java +++ /dev/null @@ -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 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"); - } - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/upload/Zoom.java b/app/src/main/java/fr/free/nrw/commons/upload/Zoom.java deleted file mode 100644 index 438c7f77b..000000000 --- a/app/src/main/java/fr/free/nrw/commons/upload/Zoom.java +++ /dev/null @@ -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; - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/BitmapRegionDecoderWrapper.java b/app/src/main/java/fr/free/nrw/commons/utils/BitmapRegionDecoderWrapper.java new file mode 100644 index 000000000..21d411908 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/utils/BitmapRegionDecoderWrapper.java @@ -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); + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/ContributionUtils.java b/app/src/main/java/fr/free/nrw/commons/utils/ContributionUtils.java index 35c30a8f7..7d080f4fc 100644 --- a/app/src/main/java/fr/free/nrw/commons/utils/ContributionUtils.java +++ b/app/src/main/java/fr/free/nrw/commons/utils/ContributionUtils.java @@ -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(); + } } diff --git a/app/src/main/java/fr/free/nrw/commons/utils/FileUtils.java b/app/src/main/java/fr/free/nrw/commons/utils/FileUtils.java deleted file mode 100644 index 26ab5b2ca..000000000 --- a/app/src/main/java/fr/free/nrw/commons/utils/FileUtils.java +++ /dev/null @@ -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 - } - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/ImageUtils.java b/app/src/main/java/fr/free/nrw/commons/utils/ImageUtils.java index e6cc2fc5d..64bf033e6 100644 --- a/app/src/main/java/fr/free/nrw/commons/utils/ImageUtils.java +++ b/app/src/main/java/fr/free/nrw/commons/utils/ImageUtils.java @@ -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(); } } diff --git a/app/src/main/java/fr/free/nrw/commons/utils/ImageUtilsWrapper.java b/app/src/main/java/fr/free/nrw/commons/utils/ImageUtilsWrapper.java new file mode 100644 index 000000000..d5b905e9d --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/utils/ImageUtilsWrapper.java @@ -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); + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/LocationUtils.java b/app/src/main/java/fr/free/nrw/commons/utils/LocationUtils.java new file mode 100644 index 000000000..58cd138a0 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/utils/LocationUtils.java @@ -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()); + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/PlaceUtils.java b/app/src/main/java/fr/free/nrw/commons/utils/PlaceUtils.java new file mode 100644 index 000000000..4a4e153db --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/utils/PlaceUtils.java @@ -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); + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/SwipableCardView.java b/app/src/main/java/fr/free/nrw/commons/utils/SwipableCardView.java new file mode 100644 index 000000000..a65033d15 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/utils/SwipableCardView.java @@ -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); + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/upload/HeightLimitedRecyclerView.java b/app/src/main/java/fr/free/nrw/commons/widget/HeightLimitedRecyclerView.java similarity index 97% rename from app/src/main/java/fr/free/nrw/commons/upload/HeightLimitedRecyclerView.java rename to app/src/main/java/fr/free/nrw/commons/widget/HeightLimitedRecyclerView.java index ff100e16e..b1dc29736 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/HeightLimitedRecyclerView.java +++ b/app/src/main/java/fr/free/nrw/commons/widget/HeightLimitedRecyclerView.java @@ -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(); diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataConstants.java b/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataConstants.java index e7e929dac..bbab18300 100644 --- a/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataConstants.java +++ b/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataConstants.java @@ -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"; } diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditService.java b/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditService.java index 6b0c52cb3..3d52cf85c 100644 --- a/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditService.java +++ b/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditService.java @@ -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); } diff --git a/app/src/main/res/drawable-hdpi/ic_campaign.png b/app/src/main/res/drawable-hdpi/ic_campaign.png new file mode 100755 index 000000000..315ec45d3 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_campaign.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_campaign.png b/app/src/main/res/drawable-mdpi/ic_campaign.png new file mode 100755 index 000000000..b60884dd6 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_campaign.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_campaign.png b/app/src/main/res/drawable-xhdpi/ic_campaign.png new file mode 100755 index 000000000..8b93f7977 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_campaign.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_campaign.png b/app/src/main/res/drawable-xxhdpi/ic_campaign.png new file mode 100755 index 000000000..069ad8e1e Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_campaign.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_campaign.png b/app/src/main/res/drawable-xxxhdpi/ic_campaign.png new file mode 100755 index 000000000..cef03959d Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_campaign.png differ diff --git a/app/src/main/res/drawable/ic_app_logo.xml b/app/src/main/res/drawable/ic_app_logo.xml new file mode 100644 index 000000000..afbe0efda --- /dev/null +++ b/app/src/main/res/drawable/ic_app_logo.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_download_white_24dp.xml b/app/src/main/res/drawable/ic_download_white_24dp.xml new file mode 100644 index 000000000..b8e836142 --- /dev/null +++ b/app/src/main/res/drawable/ic_download_white_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 000000000..4c2e6cabf --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout-land/welcome_do_upload.xml b/app/src/main/res/layout-land/welcome_do_upload.xml index 01e5c2af3..5baceecec 100644 --- a/app/src/main/res/layout-land/welcome_do_upload.xml +++ b/app/src/main/res/layout-land/welcome_do_upload.xml @@ -9,21 +9,6 @@ android:gravity="center" android:orientation="horizontal"> - - - - - - - - + app:actualImageScaleType="fitCenter" /> + app:actualImageScaleType="fitCenter" /> - + android:text="@plurals/share_license_summary" /> + + - \ No newline at end of file + diff --git a/app/src/main/res/layout/fragment_contributions.xml b/app/src/main/res/layout/fragment_contributions.xml index dd1959178..5fc1a74dc 100644 --- a/app/src/main/res/layout/fragment_contributions.xml +++ b/app/src/main/res/layout/fragment_contributions.xml @@ -12,11 +12,21 @@ app:cardBackgroundColor="?attr/mainCardBackground" /> + + + android:id="@+id/root_frame" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_marginTop="2dp" + android:background="#000" + > \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_nearby.xml b/app/src/main/res/layout/fragment_nearby.xml index 4269f135b..8b50dfae5 100644 --- a/app/src/main/res/layout/fragment_nearby.xml +++ b/app/src/main/res/layout/fragment_nearby.xml @@ -19,7 +19,6 @@ android:gravity="center_vertical" android:orientation="horizontal"> - +