Merge branch 'master' into 2.9-release

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

View file

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

View file

@ -11,37 +11,13 @@ Initially started by the Wikimedia Foundation, this app is now maintained by gra
## Documentation ## 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] * [User Documentation][5]
* [Contributor Documentation][7] * [Contributor Documentation][6]
* [Volunteers Welcome!][9] * [Volunteers Welcome!][7]
* [Developer Documentation][8] * [Developer Documentation][8]
* [Libraries Used][9]
## 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]
## Contributors ## ## Contributors ##
@ -60,37 +36,18 @@ Thank you all for your work!
## License ## ## 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 [1]: https://play.google.com/store/apps/details?id=fr.free.nrw.commons
[2]: https://commons-app.github.io/ [2]: https://commons-app.github.io/
[3]: https://github.com/commons-app/apps-android-commons/issues [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 [4]: https://github.com/commons-app/apps-android-commons/wiki
[6]: https://github.com/commons-app/apps-android-commons/wiki#user-documentation [5]: https://github.com/commons-app/apps-android-commons/wiki#user-documentation
[7]: https://github.com/commons-app/apps-android-commons/wiki#contributor-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 [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 [9]: https://github.com/commons-app/apps-android-commons/wiki/Libraries-used
[10]: https://meta.wikimedia.org/wiki/Grants:Project/Improve_%27Upload_to_Commons%27_Android_App/Renewal
[11]: https://github.com/square/picasso [10]: https://www.apache.org/licenses/LICENSE-2.0
[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

View file

@ -5,83 +5,90 @@ apply plugin: 'kotlin-kapt'
apply plugin: 'jacoco-android' apply plugin: 'jacoco-android'
apply from: 'quality.gradle' apply from: 'quality.gradle'
def isRunningOnTravisAndIsNotPRBuild = System.getenv("CI") == "true" && file('../play.p12').exists()
if(isRunningOnTravisAndIsNotPRBuild) {
apply plugin: 'com.github.triplet.play'
}
dependencies { dependencies {
// Utils
implementation 'com.github.nicolas-raoul:Quadtree:ac16ea8035bf07' 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 '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.google.code.gson:gson:2.8.5'
implementation 'com.jakewharton.timber:timber:4.4.0'
implementation 'info.debatty:java-string-similarity:0.24' implementation 'info.debatty:java-string-similarity:0.24'
implementation 'com.borjabravo:readmoretextview:2.1.0' implementation 'in.yuvi:http.fluent:1.3'
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 'com.squareup.okhttp3:okhttp:3.10.0' implementation 'com.squareup.okhttp3:okhttp:3.10.0'
implementation 'com.squareup.okio:okio:1.14.0' implementation 'com.squareup.okio:okio:1.14.0'
implementation 'io.reactivex.rxjava2:rxandroid:2.1.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 'io.reactivex.rxjava2:rxjava:2.2.0'
implementation 'com.jakewharton.rxbinding2:rxbinding:2.1.1' implementation 'com.jakewharton.rxbinding2:rxbinding:2.1.1'
implementation 'com.jakewharton.rxbinding2:rxbinding-support-v4: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-appcompat-v7:2.1.1'
implementation 'com.jakewharton.rxbinding2:rxbinding-design:2.1.1' implementation 'com.jakewharton.rxbinding2:rxbinding-design:2.1.1'
implementation 'com.facebook.fresco:fresco:1.10.0' 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:$DAGGER_VERSION"
implementation "com.google.dagger:dagger-android-support:$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-android-processor:$DAGGER_VERSION"
kapt "com.google.dagger:dagger-compiler:$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-stdlib-jdk7:$kotlin_version"
testImplementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" testImplementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
testImplementation 'junit:junit:4.12' testImplementation 'junit:junit:4.12'
testImplementation 'org.robolectric:robolectric:3.7.1' testImplementation 'org.robolectric:robolectric:3.7.1'
testImplementation 'com.nhaarman:mockito-kotlin:1.5.0' testImplementation 'com.nhaarman:mockito-kotlin:1.5.0'
testImplementation 'com.squareup.okhttp3:mockwebserver:3.10.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 "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
androidTestImplementation 'com.squareup.okhttp3:mockwebserver:3.10.0' 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:rules:1.0.2'
androidTestImplementation 'com.android.support.test:runner: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 '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" debugImplementation "com.squareup.leakcanary:leakcanary-android:$LEAK_CANARY"
releaseImplementation "com.squareup.leakcanary:leakcanary-android-no-op:$LEAK_CANARY" releaseImplementation "com.squareup.leakcanary:leakcanary-android-no-op:$LEAK_CANARY"
testImplementation "com.squareup.leakcanary:leakcanary-android-no-op:$LEAK_CANARY" testImplementation "com.squareup.leakcanary:leakcanary-android-no-op:$LEAK_CANARY"
//For handling runtime permissions // Support libraries
implementation 'com.karumi:dexter:5.0.0' implementation "com.android.support:support-v4:$SUPPORT_LIB_VERSION"
implementation "com.android.support:appcompat-v7:$SUPPORT_LIB_VERSION"
implementation files('libs/simplemagic-1.9.jar') 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 { android {
@ -101,6 +108,8 @@ android {
} }
testOptions { testOptions {
unitTests.returnDefaultValues = true
unitTests.all { unitTests.all {
jvmArgs '-noverify' jvmArgs '-noverify'
} }
@ -115,11 +124,18 @@ android {
test.resources.srcDirs += 'src/main/resoures' test.resources.srcDirs += 'src/main/resoures'
} }
signingConfigs {
release
}
buildTypes { buildTypes {
release { release {
minifyEnabled true minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt' proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
testProguardFile 'test-proguard-rules.txt' testProguardFile 'test-proguard-rules.txt'
if(isRunningOnTravisAndIsNotPRBuild) {
signingConfig signingConfigs.release
}
} }
debug { debug {
minifyEnabled true minifyEnabled true
@ -130,6 +146,14 @@ android {
} }
} }
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' flavorDimensions 'tier'
productFlavors { productFlavors {
prod { prod {
@ -204,3 +228,17 @@ android {
buildToolsVersion buildToolsVersion buildToolsVersion buildToolsVersion
} }
if(isRunningOnTravisAndIsNotPRBuild) {
play {
track = "alpha"
userFraction = 1
serviceAccountEmail = System.getenv("SERVICE_ACCOUNT_NAME")
serviceAccountCredentials = file("../play.p12")
resolutionStrategy = "auto"
outputProcessor { // this: ApkVariantOutput
versionNameOverride = "$versionNameOverride.$versionCode"
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

View file

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

View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View file

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

View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View file

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

View file

@ -91,6 +91,7 @@ public class CommonsApplication extends Application {
@Override @Override
public void onCreate() { public void onCreate() {
super.onCreate(); super.onCreate();
ACRA.init(this);
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
//FIXME: Traceur should be disabled for release builds until error fixed //FIXME: Traceur should be disabled for release builds until error fixed
//See https://github.com/commons-app/apps-android-commons/issues/1877 //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. // Empty temp directory in case some temp files are created and never removed.
ContributionUtils.emptyTemporaryDirectory(); ContributionUtils.emptyTemporaryDirectory();
initAcra(); if (BuildConfig.DEBUG && !isRoboUnitTest()) {
if (BuildConfig.DEBUG) {
Stetho.initializeWithDefaults(this); Stetho.initializeWithDefaults(this);
} }
@ -152,14 +152,8 @@ public class CommonsApplication extends Application {
Timber.plant(new Timber.DebugTree()); Timber.plant(new Timber.DebugTree());
} }
/** public static boolean isRoboUnitTest() {
* Remove ACRA's UncaughtExceptionHandler return "robolectric".equals(Build.FINGERPRINT);
* 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);
} }
private ThreadPoolService getFileLoggingThreadPool() { private ThreadPoolService getFileLoggingThreadPool() {

View file

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

View file

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

View file

@ -14,7 +14,7 @@ import butterknife.OnClick;
import butterknife.Optional; import butterknife.Optional;
public class WelcomePagerAdapter extends PagerAdapter { 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_wikipedia,
R.layout.welcome_do_upload, R.layout.welcome_do_upload,
R.layout.welcome_dont_upload, R.layout.welcome_dont_upload,
@ -57,29 +57,31 @@ public class WelcomePagerAdapter extends PagerAdapter {
@Override @Override
public Object instantiateItem(ViewGroup container, int position) { public Object instantiateItem(ViewGroup container, int position) {
this.container=container; this.container = container;
LayoutInflater inflater = LayoutInflater.from(container.getContext()); LayoutInflater inflater = LayoutInflater.from(container.getContext());
ViewGroup layout = (ViewGroup) inflater.inflate(PAGE_LAYOUTS[position], container, false); 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){ // If final page
TextView moreInfo = layout.findViewById(R.id.welcomeInfo); if (position == PAGE_FINAL) {
moreInfo.setText(Html.fromHtml(WelcomeActivity.moreInformation)); // Add link to more information
ViewHolder holder1 = new ViewHolder(layout); TextView moreInfo = layout.findViewById(R.id.welcomeInfo);
layout.setTag(holder1); moreInfo.setText(Html.fromHtml(WelcomeActivity.moreInformation));
} moreInfo.setOnClickListener(view -> {
} else { try {
if (position == PAGE_FINAL) { Utils.handleWebUrl(
ViewHolder holder = new ViewHolder(layout); container.getContext(),
layout.setTag(holder); 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); container.addView(layout);
return layout; return layout;
} }
@ -96,33 +98,6 @@ public class WelcomePagerAdapter extends PagerAdapter {
} }
public interface Callback { public interface Callback {
void onYesClicked(); void finishTutorial();
}
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();
}
}
} }
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -14,6 +14,7 @@ import android.os.Bundle;
import android.os.IBinder; import android.os.IBinder;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager; import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentTransaction; 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.app.LoaderManager;
import android.support.v4.widget.CursorAdapter; import android.support.v4.widget.CursorAdapter;
import android.support.v7.app.AlertDialog; import android.support.v7.app.AlertDialog;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
@ -30,6 +33,14 @@ import android.widget.AdapterView;
import android.widget.CheckBox; import android.widget.CheckBox;
import android.widget.CompoundButton; 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.ArrayList;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
@ -60,6 +71,7 @@ import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable; import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.disposables.Disposable; import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers; import io.reactivex.schedulers.Schedulers;
import org.acra.util.ToastSender;
import timber.log.Timber; import timber.log.Timber;
import static fr.free.nrw.commons.contributions.Contribution.STATE_FAILED; import static fr.free.nrw.commons.contributions.Contribution.STATE_FAILED;
@ -76,7 +88,7 @@ public class ContributionsFragment
MediaDetailPagerFragment.MediaDetailProvider, MediaDetailPagerFragment.MediaDetailProvider,
FragmentManager.OnBackStackChangedListener, FragmentManager.OnBackStackChangedListener,
ContributionsListFragment.SourceRefresher, ContributionsListFragment.SourceRefresher,
LocationUpdateListener LocationUpdateListener,ICampaignsView
{ {
@Inject @Inject
@Named("default_preferences") @Named("default_preferences")
@ -112,6 +124,10 @@ public class ContributionsFragment
private boolean isFragmentAttachedBefore = false; private boolean isFragmentAttachedBefore = false;
private View checkBoxView; private View checkBoxView;
private CheckBox checkBox; 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 * Since we will need to use parent activity on onAuthCookieAcquired, we have to wait
@ -142,6 +158,10 @@ public class ContributionsFragment
@Override @Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_contributions, container, false); 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); nearbyNoificationCardView = view.findViewById(R.id.card_view_nearby);
checkBoxView = View.inflate(getActivity(), R.layout.nearby_permission_dialog, null); checkBoxView = View.inflate(getActivity(), R.layout.nearby_permission_dialog, null);
checkBox = (CheckBox) checkBoxView.findViewById(R.id.never_ask_again); checkBox = (CheckBox) checkBoxView.findViewById(R.id.never_ask_again);
@ -173,6 +193,27 @@ public class ContributionsFragment
setUploadCount(); 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; return view;
} }
@ -537,7 +578,7 @@ public class ContributionsFragment
nearbyNoificationCardView.setVisibility(View.GONE); nearbyNoificationCardView.setVisibility(View.GONE);
} }
fetchCampaigns();
} }
/** /**
@ -622,7 +663,7 @@ public class ContributionsFragment
curLatLng = locationManager.getLastLocation(); curLatLng = locationManager.getLastLocation();
placesDisposable = Observable.fromCallable(() -> nearbyController 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()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe(this::updateNearbyNotification, .subscribe(this::updateNearbyNotification,
@ -694,5 +735,38 @@ public class ContributionsFragment
// Update closest nearby card view if location changed more than 500 meters // Update closest nearby card view if location changed more than 500 meters
updateClosestNearbyCardViewInfo(); updateClosestNearbyCardViewInfo();
} }
@Override public void onViewCreated(@NonNull View view,
@Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
}
/**
* ask the presenter to fetch the campaigns only if user has not manually disabled it
*/
private void fetchCampaigns() {
if (prefs.getBoolean("displayCampaignsCardView", true)) {
presenter.getCampaigns();
}
}
@Override public void showMessage(String message) {
Toast.makeText(getContext(), message, Toast.LENGTH_SHORT).show();
}
@Override public MediaWikiApi getMediaWikiApi() {
return mediaWikiApi;
}
@Override public void showCampaigns(Campaign campaign) {
if (campaign != null) {
campaignView.setCampaign(campaign);
}
}
@Override public void onDestroyView() {
super.onDestroyView();
presenter.onDetachView();
}
} }

View file

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

View file

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

View file

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

View file

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

View file

@ -14,15 +14,18 @@ import android.text.Editable;
import android.text.Html; import android.text.Html;
import android.text.TextUtils; import android.text.TextUtils;
import android.text.TextWatcher; import android.text.TextWatcher;
import android.util.Log;
import android.util.TypedValue; import android.util.TypedValue;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.view.ViewTreeObserver; import android.view.ViewTreeObserver;
import android.widget.ArrayAdapter;
import android.widget.Button; import android.widget.Button;
import android.widget.EditText; import android.widget.EditText;
import android.widget.LinearLayout; import android.widget.LinearLayout;
import android.widget.ScrollView; import android.widget.ScrollView;
import android.widget.Spinner;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast; 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.MediaDataExtractor;
import fr.free.nrw.commons.MediaWikiImageView; import fr.free.nrw.commons.MediaWikiImageView;
import fr.free.nrw.commons.R; 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.category.CategoryDetailsActivity;
import fr.free.nrw.commons.contributions.ContributionsFragment; import fr.free.nrw.commons.contributions.ContributionsFragment;
import fr.free.nrw.commons.delete.DeleteTask; 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.di.CommonsDaggerSupportFragment;
import fr.free.nrw.commons.location.LatLng; import fr.free.nrw.commons.location.LatLng;
import fr.free.nrw.commons.mwapi.MediaWikiApi; import fr.free.nrw.commons.mwapi.MediaWikiApi;
@ -65,6 +70,8 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
private MediaDetailPagerFragment.MediaDetailProvider detailProvider; private MediaDetailPagerFragment.MediaDetailProvider detailProvider;
private int index; private int index;
private Locale locale; private Locale locale;
private boolean isDeleted = false;
public static MediaDetailFragment forMedia(int index, boolean editable, boolean isCategoryImage) { public static MediaDetailFragment forMedia(int index, boolean editable, boolean isCategoryImage) {
MediaDetailFragment mf = new MediaDetailFragment(); MediaDetailFragment mf = new MediaDetailFragment();
@ -85,6 +92,8 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
Provider<MediaDataExtractor> mediaDataExtractorProvider; Provider<MediaDataExtractor> mediaDataExtractorProvider;
@Inject @Inject
MediaWikiApi mwApi; MediaWikiApi mwApi;
@Inject
SessionManager sessionManager;
private int initialListTop = 0; 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 //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 Media media;
private ArrayList<String> reasonList;
@Override @Override
public void onSaveInstanceState(Bundle outState) { public void onSaveInstanceState(Bundle outState) {
@ -163,6 +174,13 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
initialListTop = 0; 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 = new ArrayList<>();
categoryNames.add(getString(R.string.detail_panel_cats_loading)); categoryNames.add(getString(R.string.detail_panel_cats_loading));
@ -379,48 +397,43 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
@OnClick(R.id.nominateDeletion) @OnClick(R.id.nominateDeletion)
public void onDeleteButtonClicked(){ public void onDeleteButtonClicked(){
//Reviewer correct me if i have misunderstood something over here final ArrayAdapter<String> languageAdapter = new ArrayAdapter<String>(getActivity(),
//But how does this if (delete.getVisibility() == View.VISIBLE) { R.layout.simple_spinner_dropdown_list, reasonList);
// enableDeleteButton(true); makes sense ? final Spinner spinner = new Spinner(getActivity());
AlertDialog.Builder alert = new AlertDialog.Builder(getActivity()); spinner.setLayoutParams(new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT));
alert.setMessage("Why should this file be deleted?"); spinner.setAdapter(languageAdapter);
final EditText input = new EditText(getActivity()); spinner.setGravity(17);
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);
}
}
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 @Override
public void afterTextChanged(Editable arg0) { public void onClick(DialogInterface dialog, int which) {
handleText(); dialog.dismiss();
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
} }
}); });
d.show(); AlertDialog dialog = builder.create();
d.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false); dialog.show();
if(isDeleted) {
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false);
}
} }
@OnClick(R.id.seeMore) @OnClick(R.id.seeMore)
@ -442,8 +455,19 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
private void rebuildCatList() { private void rebuildCatList() {
categoryContainer.removeAllViews(); categoryContainer.removeAllViews();
// @fixme add the category items // @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); categoryContainer.addView(catLabel);
} }
} }

View file

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

View file

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

View file

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

View file

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

View file

@ -14,6 +14,7 @@ import android.support.design.widget.BottomSheetBehavior;
import android.support.design.widget.Snackbar; import android.support.design.widget.Snackbar;
import android.support.v4.app.FragmentTransaction; import android.support.v4.app.FragmentTransaction;
import android.support.v7.app.AlertDialog; import android.support.v7.app.AlertDialog;
import android.util.Log;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
@ -64,8 +65,6 @@ public class NearbyFragment extends CommonsDaggerSupportFragment
LinearLayout bottomSheetDetails; LinearLayout bottomSheetDetails;
@BindView(R.id.transparentView) @BindView(R.id.transparentView)
View transparentView; View transparentView;
@BindView(R.id.fab_recenter)
View fabRecenter;
@Inject @Inject
LocationServiceManager locationManager; LocationServiceManager locationManager;
@ -87,16 +86,19 @@ public class NearbyFragment extends CommonsDaggerSupportFragment
private LatLng curLatLng; private LatLng curLatLng;
private Disposable placesDisposable; private Disposable placesDisposable;
private Disposable placesDisposableCustom;
private boolean lockNearbyView; //Determines if the nearby places needs to be refreshed private boolean lockNearbyView; //Determines if the nearby places needs to be refreshed
public View view; public View view;
private Snackbar snackbar; private Snackbar snackbar;
private LatLng lastKnownLocation; private LatLng lastKnownLocation;
private LatLng customLatLng;
private final String NETWORK_INTENT_ACTION = "android.net.conn.CONNECTIVITY_CHANGE"; private final String NETWORK_INTENT_ACTION = "android.net.conn.CONNECTIVITY_CHANGE";
private BroadcastReceiver broadcastReceiver; private BroadcastReceiver broadcastReceiver;
private boolean onOrientationChanged = false; private boolean onOrientationChanged = false;
private boolean populateForCurrentLocation = false;
@Override @Override
public void onCreate(@Nullable Bundle savedInstanceState) { public void onCreate(@Nullable Bundle savedInstanceState) {
@ -215,24 +217,27 @@ public class NearbyFragment extends CommonsDaggerSupportFragment
@Override @Override
public void onLocationChangedSignificantly(LatLng latLng) { public void onLocationChangedSignificantly(LatLng latLng) {
refreshView(LOCATION_SIGNIFICANTLY_CHANGED); refreshView(LOCATION_SIGNIFICANTLY_CHANGED);
} }
@Override @Override
public void onLocationChangedSlightly(LatLng latLng) { public void onLocationChangedSlightly(LatLng latLng) {
refreshView(LOCATION_SLIGHTLY_CHANGED); refreshView(LOCATION_SLIGHTLY_CHANGED);
} }
@Override @Override
public void onLocationChangedMedium(LatLng latLng) { public void onLocationChangedMedium(LatLng latLng) {
// For nearby map actions, there are no differences between 500 meter location change (aka medium change) and slight change // 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 @Override
public void onWikidataEditSuccessful() { 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 * @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"); Timber.d("Refreshing nearby places");
if (lockNearbyView) { if (lockNearbyView) {
return; return;
@ -256,9 +261,11 @@ public class NearbyFragment extends CommonsDaggerSupportFragment
if (curLatLng != null && curLatLng.equals(lastLocation) if (curLatLng != null && curLatLng.equals(lastLocation)
&& !locationChangeType.equals(MAP_UPDATED)) { //refresh view only if location has changed && !locationChangeType.equals(MAP_UPDATED)) { //refresh view only if location has changed
// Two exceptional cases to refresh nearby map manually.
if (!onOrientationChanged) { if (!onOrientationChanged) {
return; return;
} }
} }
curLatLng = lastLocation; curLatLng = lastLocation;
@ -291,7 +298,7 @@ public class NearbyFragment extends CommonsDaggerSupportFragment
bundle.putString("CurLatLng", gsonCurLatLng); bundle.putString("CurLatLng", gsonCurLatLng);
placesDisposable = Observable.fromCallable(() -> nearbyController placesDisposable = Observable.fromCallable(() -> nearbyController
.loadAttractionsFromLocation(curLatLng, false)) .loadAttractionsFromLocation(curLatLng, curLatLng, false, true))
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe(this::populatePlaces, .subscribe(this::populatePlaces,
@ -300,6 +307,7 @@ public class NearbyFragment extends CommonsDaggerSupportFragment
showErrorMessage(getString(R.string.error_fetching_nearby_places)); showErrorMessage(getString(R.string.error_fetching_nearby_places));
progressBar.setVisibility(View.GONE); progressBar.setVisibility(View.GONE);
}); });
} else if (locationChangeType } else if (locationChangeType
.equals(LOCATION_SLIGHTLY_CHANGED)) { .equals(LOCATION_SLIGHTLY_CHANGED)) {
Gson gson = new GsonBuilder() Gson gson = new GsonBuilder()
@ -307,7 +315,62 @@ public class NearbyFragment extends CommonsDaggerSupportFragment
.create(); .create();
String gsonCurLatLng = gson.toJson(curLatLng); String gsonCurLatLng = gson.toJson(curLatLng);
bundle.putString("CurLatLng", gsonCurLatLng); 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 { } else {
// There are fragments, just update the map and list // There are fragments, just update the map and list
Timber.d("Map fragment already exists, 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(); 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 Significant update means updating nearby place markers. Slightly update means only
updating current location marker and camera target. 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 * 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 * 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[0].getLatitude()
|| curLatLng.getLatitude() >= nearbyMapFragment.boundaryCoordinates[1].getLatitude() || curLatLng.getLatitude() >= nearbyMapFragment.boundaryCoordinates[1].getLatitude()
|| curLatLng.getLongitude() <= nearbyMapFragment.boundaryCoordinates[2].getLongitude() || curLatLng.getLongitude() <= nearbyMapFragment.boundaryCoordinates[2].getLongitude()
|| curLatLng.getLongitude() >= nearbyMapFragment.boundaryCoordinates[3].getLongitude())) { || curLatLng.getLongitude() >= nearbyMapFragment.boundaryCoordinates[3].getLongitude())) {
// populate places // populate places
placesDisposable = Observable.fromCallable(() -> nearbyController placesDisposable = Observable.fromCallable(() -> nearbyController
.loadAttractionsFromLocation(curLatLng, false)) .loadAttractionsFromLocation(curLatLng, curLatLng, false, updateViaButton))
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe(this::populatePlaces, .subscribe(this::populatePlaces,
@ -396,11 +463,16 @@ public class NearbyFragment extends CommonsDaggerSupportFragment
progressBar.setVisibility(View.GONE); progressBar.setVisibility(View.GONE);
}); });
nearbyMapFragment.setBundleForUpdtes(bundle); nearbyMapFragment.setBundleForUpdtes(bundle);
nearbyMapFragment.updateMapSignificantly(); nearbyMapFragment.updateMapSignificantlyForCurrentLocation();
updateListFragment(); updateListFragment();
return; 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 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 anymore. We want to significantly update map after each orientation change
@ -415,7 +487,7 @@ public class NearbyFragment extends CommonsDaggerSupportFragment
nearbyMapFragment.updateMapSlightly(); nearbyMapFragment.updateMapSlightly();
} else { } else {
nearbyMapFragment.setBundleForUpdtes(bundle); nearbyMapFragment.setBundleForUpdtes(bundle);
nearbyMapFragment.updateMapSignificantly(); nearbyMapFragment.updateMapSignificantlyForCurrentLocation();
updateListFragment(); updateListFragment();
} }
} else { } else {
@ -432,6 +504,15 @@ public class NearbyFragment extends CommonsDaggerSupportFragment
nearbyListFragment.updateNearbyListSignificantly(); nearbyListFragment.updateNearbyListSignificantly();
} }
/**
* Updates nearby list for custom location, will be used with search this area method. When you
* want to search for a place where you are not at.
* @param placeList List of places around your manually chosen target location from map.
*/
private void updateListFragmentForCustomLocation(List<Place> placeList) {
nearbyListFragment.updateNearbyListSignificantlyForCustomLocation(placeList);
}
/** /**
* Calls fragment for map view. * Calls fragment for map view.
*/ */
@ -658,6 +739,9 @@ public class NearbyFragment extends CommonsDaggerSupportFragment
placesDisposable.dispose(); placesDisposable.dispose();
} }
wikidataEditListener.setAuthenticationStateListener(null); wikidataEditListener.setAuthenticationStateListener(null);
if (placesDisposableCustom != null) {
placesDisposableCustom.dispose();
}
} }
@Override @Override

View file

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

View file

@ -23,8 +23,10 @@ import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.view.animation.Animation; import android.view.animation.Animation;
import android.view.animation.AnimationUtils; import android.view.animation.AnimationUtils;
import android.widget.Button;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.LinearLayout; import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.TextView; import android.widget.TextView;
import com.google.gson.Gson; 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.auth.LoginActivity;
import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao; import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao;
import fr.free.nrw.commons.contributions.ContributionController; 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.UriDeserializer;
import fr.free.nrw.commons.utils.ViewUtil; import fr.free.nrw.commons.utils.ViewUtil;
import timber.log.Timber; import timber.log.Timber;
import uk.co.deanwild.materialshowcaseview.MaterialShowcaseView;
import static android.app.Activity.RESULT_OK; import static android.app.Activity.RESULT_OK;
import static android.content.pm.PackageManager.PERMISSION_GRANTED; 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_ENTITY_ID_PREF;
import static fr.free.nrw.commons.wikidata.WikidataConstants.WIKIDATA_ITEM_LOCATION;
public class NearbyMapFragment extends DaggerFragment { public class NearbyMapFragment extends DaggerFragment {
@ -115,16 +120,21 @@ public class NearbyMapFragment extends DaggerFragment {
private Place place; private Place place;
private Marker selected; private Marker selected;
private Marker currentLocationMarker; private Marker currentLocationMarker;
private MapboxMap mapboxMap; public MapboxMap mapboxMap;
private PolygonOptions currentLocationPolygonOptions; private PolygonOptions currentLocationPolygonOptions;
public Button searchThisAreaButton;
public ProgressBar searchThisAreaButtonProgressBar;
private boolean isBottomListSheetExpanded; private boolean isBottomListSheetExpanded;
private final double CAMERA_TARGET_SHIFT_FACTOR_PORTRAIT = 0.06; private final double CAMERA_TARGET_SHIFT_FACTOR_PORTRAIT = 0.06;
private final double CAMERA_TARGET_SHIFT_FACTOR_LANDSCAPE = 0.04; private final double CAMERA_TARGET_SHIFT_FACTOR_LANDSCAPE = 0.04;
private boolean isMapReady; private boolean isMapReady;
public boolean searchThisAreaModeOn = false;
private Bundle bundleForUpdtes;// Carry information from activity about changed nearby places and current location private Bundle bundleForUpdtes;// Carry information from activity about changed nearby places and current location
private boolean searchedAroundCurrentLocation = true;
@Inject @Inject
@Named("prefs") @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 * called when user is out of boundaries (south, north, east or west) of markers drawn by
* previous nearby call. * previous nearby call.
*/ */
public void updateMapSignificantly() { public void updateMapSignificantlyForCurrentLocation() {
Timber.d("updateMapSignificantly called, bundle is:"+bundleForUpdtes); Timber.d("updateMapSignificantlyForCurrentLocation called, bundle is:"+bundleForUpdtes);
if (mapboxMap != null) { if (mapboxMap != null) {
if (bundleForUpdtes != null) { if (bundleForUpdtes != null) {
Gson gson = new GsonBuilder() Gson gson = new GsonBuilder()
@ -271,10 +281,32 @@ public class NearbyMapFragment extends DaggerFragment {
mapboxMap.clear(); mapboxMap.clear();
addCurrentLocationMarker(mapboxMap); addCurrentLocationMarker(mapboxMap);
updateMapToTrackPosition(); updateMapToTrackPosition();
addNearbyMarkerstoMapBoxMap(); // We are trying to find nearby places around our current location, thus custom parameter is nullified
addNearbyMarkerstoMapBoxMap(null);
} }
} }
/**
* Will be used for map vew updates for custom locations (ie. with search this area method).
* Clears the map, adds current location marker, adds nearby markers around custom location,
* re-enables map gestures which was locked during place load, remove progress bar.
* @param customLatLng custom location that we will search around
* @param placeList places around of custom location
*/
public void updateMapSignificantlyForCustomLocation(fr.free.nrw.commons.location.LatLng customLatLng, List<Place> placeList) {
List<NearbyBaseMarker> customBaseMarkerOptions = NearbyController
.loadAttractionsFromLocationToBaseMarkerOptions(curLatLng, // Curlatlang will be used to calculate distances
placeList,
getActivity());
mapboxMap.clear();
// We are trying to find nearby places around our custom searched area, thus custom parameter is nonnull
addNearbyMarkerstoMapBoxMap(customBaseMarkerOptions);
addCurrentLocationMarker(mapboxMap);
// Re-enable mapbox gestures on custom location markers load
mapboxMap.getUiSettings().setAllGesturesEnabled(true);
searchThisAreaButtonProgressBar.setVisibility(View.GONE);
}
// Only update current position marker and camera view // Only update current position marker and camera view
private void updateMapToTrackPosition() { private void updateMapToTrackPosition() {
@ -299,6 +331,9 @@ public class NearbyMapFragment extends DaggerFragment {
// Make camera to follow user on location change // Make camera to follow user on location change
CameraPosition position ; CameraPosition position ;
// Do not update camera position is search this area mode on
if (!searchThisAreaModeOn) {
if (ViewUtil.isPortrait(getActivity())){ if (ViewUtil.isPortrait(getActivity())){
position = new CameraPosition.Builder() position = new CameraPosition.Builder()
.target(isBottomListSheetExpanded ? .target(isBottomListSheetExpanded ?
@ -323,7 +358,7 @@ public class NearbyMapFragment extends DaggerFragment {
mapboxMap.animateCamera(CameraUpdateFactory mapboxMap.animateCamera(CameraUpdateFactory
.newCameraPosition(position), 1000); .newCameraPosition(position), 1000);
}
} }
} }
@ -366,6 +401,9 @@ public class NearbyMapFragment extends DaggerFragment {
bookmarkButton = getActivity().findViewById(R.id.bookmarkButton); bookmarkButton = getActivity().findViewById(R.id.bookmarkButton);
bookmarkButtonImage = getActivity().findViewById(R.id.bookmarkButtonImage); 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() { private void setListeners() {
@ -495,13 +533,77 @@ public class NearbyMapFragment extends DaggerFragment {
@Override @Override
public void onMapReady(MapboxMap mapboxMap) { public void onMapReady(MapboxMap mapboxMap) {
NearbyMapFragment.this.mapboxMap = mapboxMap; NearbyMapFragment.this.mapboxMap = mapboxMap;
updateMapSignificantly(); addMapMovementListeners();
updateMapSignificantlyForCurrentLocation();
} }
}); });
mapView.setStyleUrl("asset://mapstyle.json"); 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. * 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 * Adds markers for nearby places to mapbox map
*/ */
private void addNearbyMarkerstoMapBoxMap() { private void addNearbyMarkerstoMapBoxMap(@Nullable List<NearbyBaseMarker> customNearbyBaseMarker) {
List<NearbyBaseMarker> baseMarkerOptions;
Timber.d("addNearbyMarkerstoMapBoxMap is called"); 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.addMarkers(baseMarkerOptions);
mapboxMap.setOnInfoWindowCloseListener(marker -> { mapboxMap.setOnInfoWindowCloseListener(marker -> {
if (marker == selected) { if (marker == selected) {
bottomSheetDetailsBehavior.setState(BottomSheetBehavior.STATE_HIDDEN); bottomSheetDetailsBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
@ -781,6 +890,7 @@ public class NearbyMapFragment extends DaggerFragment {
editor.putString("Desc", place.getLongDescription()); editor.putString("Desc", place.getLongDescription());
editor.putString("Category", place.getCategory()); editor.putString("Category", place.getCategory());
editor.putString(WIKIDATA_ENTITY_ID_PREF, place.getWikiDataEntityId()); editor.putString(WIKIDATA_ENTITY_ID_PREF, place.getWikiDataEntityId());
editor.putString(WIKIDATA_ITEM_LOCATION, PlaceUtils.latLangToString(place.location));
editor.apply(); editor.apply();
} }
@ -817,13 +927,14 @@ public class NearbyMapFragment extends DaggerFragment {
if (resultCode == RESULT_OK) { if (resultCode == RESULT_OK) {
Timber.d("OnActivityResult() parameters: Req code: %d Result code: %d Data: %s", Timber.d("OnActivityResult() parameters: Req code: %d Result code: %d Data: %s",
requestCode, resultCode, data); 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 (requestCode == ContributionController.SELECT_FROM_CAMERA) {
// If coming from camera, pass null as uri. Because camera photos get saved to a // If coming from camera, pass null as uri. Because camera photos get saved to a
// fixed directory // fixed directory
controller.handleImagePicked(requestCode, null, true, wikidataEntityId); controller.handleImagePicked(requestCode, null, true, wikidataEntityId, wikidataItemLocation);
} else { } else {
controller.handleImagePicked(requestCode, data.getData(), true, wikidataEntityId); controller.handleImagePicked(requestCode, data.getData(), true, wikidataEntityId, wikidataItemLocation);
} }
} else { } else {
Timber.e("OnActivityResult() parameters: Req code: %d Result code: %d Data: %s", Timber.e("OnActivityResult() parameters: Req code: %d Result code: %d Data: %s",

View file

@ -6,12 +6,8 @@ import android.content.res.Resources;
import android.os.Handler; import android.os.Handler;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable; 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.app.AlertDialog;
import android.support.v7.widget.CardView;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent; import android.view.MotionEvent;
import android.view.View; import android.view.View;
import android.widget.Button; import android.widget.Button;
@ -20,19 +16,17 @@ import android.widget.ProgressBar;
import android.widget.RelativeLayout; import android.widget.RelativeLayout;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast;
import fr.free.nrw.commons.R; import fr.free.nrw.commons.R;
import fr.free.nrw.commons.contributions.MainActivity; import fr.free.nrw.commons.contributions.MainActivity;
import fr.free.nrw.commons.utils.SwipableCardView;
import fr.free.nrw.commons.utils.ViewUtil; import fr.free.nrw.commons.utils.ViewUtil;
import timber.log.Timber; import timber.log.Timber;
/** /**
* Custom card view for nearby notification card view on main screen, above contributions list * 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 Context context;
private Button permissionRequestButton; private Button permissionRequestButton;
@ -99,41 +93,15 @@ public class NearbyNoificationCardView extends CardView{
private void setActionListeners() { private void setActionListeners() {
this.setOnClickListener(view -> ((MainActivity)context).viewPager.setCurrentItem(1)); 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) { @Override public boolean onSwipe(View view) {
return (pixels / Resources.getSystem().getDisplayMetrics().density); view.setVisibility(GONE);
// Save shared preference for nearby card view accordingly
((MainActivity) context).prefs.edit().putBoolean("displayNearbyCardView", false).apply();
ViewUtil.showLongToast(context,
getResources().getString(R.string.nearby_notification_dismiss_message));
return true;
} }
/** /**

View file

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

View file

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

View file

@ -1,18 +1,12 @@
package fr.free.nrw.commons.notification; package fr.free.nrw.commons.notification;
import android.graphics.Color;
import android.text.Html; 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.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.TextView; import android.widget.TextView;
import com.borjabravo.readmoretextview.ReadMoreTextView;
import com.pedrogomez.renderers.Renderer; import com.pedrogomez.renderers.Renderer;
import butterknife.BindView; import butterknife.BindView;
@ -24,7 +18,7 @@ import fr.free.nrw.commons.R;
*/ */
public class NotificationRenderer extends Renderer<Notification> { public class NotificationRenderer extends Renderer<Notification> {
@BindView(R.id.title) ReadMoreTextView title; @BindView(R.id.title) TextView title;
@BindView(R.id.time) TextView time; @BindView(R.id.time) TextView time;
@BindView(R.id.icon) ImageView icon; @BindView(R.id.icon) ImageView icon;
private NotificationClicked listener; private NotificationClicked listener;
@ -64,26 +58,12 @@ public class NotificationRenderer extends Renderer<Notification> {
private void setTitle(String notificationText) { private void setTitle(String notificationText) {
notificationText = notificationText.trim().replaceAll("(^\\s*)|(\\s*$)", ""); notificationText = notificationText.trim().replaceAll("(^\\s*)|(\\s*$)", "");
notificationText = Html.fromHtml(notificationText).toString(); notificationText = Html.fromHtml(notificationText).toString();
if(notificationText.length()>280){
notificationText = notificationText.substring(0,279);
notificationText = notificationText.concat("...");
}
notificationText = notificationText.concat(" "); notificationText = notificationText.concat(" ");
title.setText(notificationText);
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);
} }
public interface NotificationClicked{ public interface NotificationClicked{

View file

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

View file

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

View file

@ -6,6 +6,7 @@ import android.content.ContentUris;
import android.content.Context; import android.content.Context;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.database.Cursor; import android.database.Cursor;
import android.media.ExifInterface;
import android.net.Uri; import android.net.Uri;
import android.os.Build; import android.os.Build;
import android.os.Environment; 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. * 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. * @return The value of the _data column, which is typically a file path.
*/ */
@Nullable @Nullable
public static String getDataColumn(Context context, Uri uri, String selection, private static String getDataColumn(Context context, Uri uri, String selection,
String[] selectionArgs) { String[] selectionArgs) {
Cursor cursor = null; Cursor cursor = null;
final String column = MediaStore.Images.ImageColumns.DATA; final String column = MediaStore.Images.ImageColumns.DATA;
@ -311,7 +331,7 @@ public class FileUtils {
* @param destination file path copied to * @param destination file path copied to
* @throws IOException thrown when failing to read source or opening destination file * @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 { throws IOException {
copy(new FileInputStream(source), new FileOutputStream(destination)); copy(new FileInputStream(source), new FileOutputStream(destination));
} }
@ -415,7 +435,7 @@ public class FileUtils {
return result; return result;
} }
public static String getFileExt(String fileName){ static String getFileExt(String fileName){
//Default file extension //Default file extension
String extension=".jpg"; String extension=".jpg";
@ -426,7 +446,11 @@ public class FileUtils {
return extension; return extension;
} }
public static String getFileExt(Uri uri, ContentResolver contentResolver) { private static String getFileExt(Uri uri, ContentResolver contentResolver) {
return getFileExt(getFilename(uri, contentResolver)); return getFileExt(getFilename(uri, contentResolver));
} }
public static FileInputStream getFileInputStream(String filePath) throws FileNotFoundException {
return new FileInputStream(filePath);
}
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -88,7 +88,7 @@ public class UploadService extends HandlerService<Contribution> {
String notificationProgressTitle; String notificationProgressTitle;
String notificationFinishingTitle; 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.notificationTag = notificationTag;
this.notificationProgressTitle = notificationProgressTitle; this.notificationProgressTitle = notificationProgressTitle;
this.notificationFinishingTitle = notificationFinishingTitle; this.notificationFinishingTitle = notificationFinishingTitle;

View file

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

View file

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

View file

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

View file

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

View file

@ -4,6 +4,11 @@ import android.content.Context;
import android.net.Uri; import android.net.Uri;
import java.io.File; 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 java.util.Random;
import timber.log.Timber; import timber.log.Timber;
@ -29,9 +34,9 @@ public class ContributionUtils {
// TODO add exceptions for Google Drive URİ is needed // TODO add exceptions for Google Drive URİ is needed
Uri result = null; Uri result = null;
if (FileUtils.checkIfDirectoryExists(TEMP_EXTERNAL_DIRECTORY)) { if (checkIfDirectoryExists(TEMP_EXTERNAL_DIRECTORY)) {
String destinationFilename = decideTempDestinationFileName(); 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 } else { // If directory doesn't exist, create it and recursive call current method to check again
File file = new File(TEMP_EXTERNAL_DIRECTORY); File file = new File(TEMP_EXTERNAL_DIRECTORY);
@ -53,29 +58,25 @@ public class ContributionUtils {
//TODO: do I have to notify file system about deletion? //TODO: do I have to notify file system about deletion?
File tempFile = new File(tempFileUri.getPath()); File tempFile = new File(tempFileUri.getPath());
if (tempFile.exists()) { if (tempFile.exists()) {
boolean isDeleted= tempFile.delete(); boolean isDeleted = tempFile.delete();
Timber.e("removeTemporaryFile() parameters: URI tempFileUri %s, deleted status %b", tempFileUri, isDeleted); Timber.e("removeTemporaryFile() parameters: URI tempFileUri %s, deleted status %b", tempFileUri, isDeleted);
} }
} }
private static String decideTempDestinationFileName() { private static String decideTempDestinationFileName() {
int i = 0; int i = 0;
while (true) { while (new File(TEMP_EXTERNAL_DIRECTORY + File.separatorChar + i + "_tmp").exists()) {
if (new File(TEMP_EXTERNAL_DIRECTORY +File.separatorChar+i+"_tmp").exists()) { i++;
// 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";
}
} }
// 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() { public static void emptyTemporaryDirectory() {
@ -91,4 +92,58 @@ public class ContributionUtils {
} }
} }
} }
/**
* Saves file from source URI to destination.
* @param sourceUri Uri which points to file to be saved
* @param destinationFilename where file will be located at
* @return Uri points to file saved
*/
private static Uri saveFileFromURI(Context context, Uri sourceUri, String destinationFilename) {
File file = new File(destinationFilename);
if (file.exists()) {
file.delete();
}
InputStream in = null;
OutputStream out = null;
try {
in = context.getContentResolver().openInputStream(sourceUri);
out = new FileOutputStream(new File(destinationFilename));
byte[] buf = new byte[1024];
int length;
while ((length = in.read(buf)) > 0) {
out.write(buf, 0, length);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (out != null) out.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
if (in != null) in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return Uri.parse("file://" + destinationFilename);
}
/**
* Checks if directory exists
* @param pathToCheck path of directory to check
* @return true if directory exists, false otherwise
*/
private static boolean checkIfDirectoryExists(String pathToCheck) {
File dir = new File(pathToCheck);
return dir.exists() && dir.isDirectory();
}
} }

View file

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

View file

@ -9,6 +9,7 @@ import android.graphics.Rect;
import android.net.Uri; import android.net.Uri;
import android.support.annotation.IntDef; import android.support.annotation.IntDef;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.util.Log;
import com.facebook.common.executors.CallerThreadExecutor; import com.facebook.common.executors.CallerThreadExecutor;
import com.facebook.common.references.CloseableReference; import com.facebook.common.references.CloseableReference;
@ -25,6 +26,7 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
import fr.free.nrw.commons.R; import fr.free.nrw.commons.R;
import fr.free.nrw.commons.location.LatLng;
import timber.log.Timber; import timber.log.Timber;
/** /**
@ -36,6 +38,7 @@ public class ImageUtils {
public static final int IMAGE_DARK = 1; public static final int IMAGE_DARK = 1;
public static final int IMAGE_BLURRY = 1 << 1; public static final int IMAGE_BLURRY = 1 << 1;
public static final int IMAGE_DUPLICATE = 1 << 2; 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_OK = 0;
public static final int IMAGE_KEEP = -1; public static final int IMAGE_KEEP = -1;
public static final int IMAGE_WAIT = -2; public static final int IMAGE_WAIT = -2;
@ -54,7 +57,8 @@ public class ImageUtils {
IMAGE_WAIT, IMAGE_WAIT,
EMPTY_TITLE, EMPTY_TITLE,
FILE_NAME_EXISTS, FILE_NAME_EXISTS,
NO_CATEGORY_SELECTED NO_CATEGORY_SELECTED,
IMAGE_GEOLOCATION_DIFFERENT
} }
) )
@Retention(RetentionPolicy.SOURCE) @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. * @param geolocationOfFileString Geolocation of image. If geotag doesn't exists, then this will be an empty string
* The calculation of brightness of each pixel is done by extracting the RGB constituents of the pixel * @param wikidataItemLocationString Location of wikidata item will be edited after upload
* and then applying the formula to calculate its "Luminance". * @return false if image is neither dark nor blurry or if the input bitmapRegionDecoder provided is null
* Pixels with luminance greater than 40% are considered to be bright pixels while the ones with luminance * true if geolocation of the image and wikidata item are different
* 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
*/ */
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) { private static boolean checkIfImageIsDark(Bitmap bitmap) {
if (bitmap == null) { if (bitmap == null) {
Timber.e("Expected bitmap was null"); Timber.e("Expected bitmap was null");
@ -206,24 +223,37 @@ public class ImageUtils {
} }
public static String getErrorMessageForResult(Context context, @Result int result) { public static String getErrorMessageForResult(Context context, @Result int result) {
String errorMessage; /**
if (result == ImageUtils.IMAGE_DARK) * Result variable is a result of an or operation of all possbile problems. Ie. if result
errorMessage = context.getString(R.string.upload_image_problem_dark); * is 0001 means IMAGE_DARK, if result is 1100 IMAGE_DUPLICATE and IMAGE_GEOLOCATION_DIFFERENT
else if (result == ImageUtils.IMAGE_BLURRY) */
errorMessage = context.getString(R.string.upload_image_problem_blurry); StringBuilder errorMessage = new StringBuilder();
else if (result == ImageUtils.IMAGE_DUPLICATE) if (((IMAGE_DARK | IMAGE_GEOLOCATION_DIFFERENT | IMAGE_BLURRY | IMAGE_DUPLICATE) & result) == 0 ) {
errorMessage = context.getString(R.string.upload_image_problem_duplicate); Timber.d("No issues to warn user is found");
else if (result == (ImageUtils.IMAGE_DARK|ImageUtils.IMAGE_BLURRY)) } else {
errorMessage = context.getString(R.string.upload_image_problem_dark_blurry); Timber.d("Issues found to warn user");
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 "";
return errorMessage; errorMessage.append(context.getResources().getString(R.string.upload_problem_exist));
if ((IMAGE_DARK & result) != 0 ) { // We are checking image dark bit to see if that bit is set or not
errorMessage.append("\n - ").append(context.getResources().getString(R.string.upload_problem_image_dark));
}
if ((IMAGE_BLURRY & result) != 0 ) {
errorMessage.append("\n - ").append(context.getResources().getString(R.string.upload_problem_image_blurry));
}
if ((IMAGE_DUPLICATE & result) != 0 ) {
errorMessage.append("\n - ").append(context.getResources().getString(R.string.upload_problem_image_duplicate));
}
if ((IMAGE_GEOLOCATION_DIFFERENT & result) != 0 ) {
errorMessage.append("\n - ").append(context.getResources().getString(R.string.upload_problem_different_geolocation));
}
errorMessage.append("\n\n").append(context.getResources().getString(R.string.upload_problem_do_you_continue));
}
return errorMessage.toString();
} }
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -58,6 +58,11 @@ public class WikidataEditService {
return; 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); editWikidataProperty(wikidataEntityId, fileName);
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 807 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 542 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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