Merge branch 'master' into 2.9-release
75
.travis.yml
|
|
@ -1,54 +1,55 @@
|
|||
language: android
|
||||
|
||||
addons:
|
||||
apt:
|
||||
packages:
|
||||
- w3m
|
||||
|
||||
- w3m
|
||||
env:
|
||||
global:
|
||||
- ANDROID_TARGET=android-22
|
||||
- ANDROID_ABI=armeabi-v7a
|
||||
- ADB_INSTALL_TIMEOUT=12 # in minutes
|
||||
|
||||
- ANDROID_TARGET=android-22
|
||||
- ANDROID_ABI=armeabi-v7a
|
||||
- ADB_INSTALL_TIMEOUT=12
|
||||
- secure: okdkna5DaH/2Fay9vI6Enrx7u9UwRm4/IJXvcaWJcvjF3JTsLQr0r+dlMT2X5E1GsNk4WcoGcfZJcVonULkaW4S96B43g3EyevWbLFWjii0cMUO00OshToKyboSvNUf+d5B6rghrbnxTIBNel2ZBFj8MXHdtz6Az20q8VywqPeBZupo7olyKKS1nYdvoo7ypNScVjDGEjEPonWplztYlSDT1w81Vww4kF9oiOPEzDOPw1lOiD8FTyKLXhK0WYlnc3cnyFjZwVMlKcomnFYPfe/J2zO6OP/XInxYSXRkZ6wiOC5gMPYAYanUAuzm91vsTBQMk6jMCglSM9Nl6dPusGgEqOyTwLVALlgvS3km9HNVsHuVJhU+bmJ6scFBWrAOhbsV2ioSEsQ8NgU0Zv1SC0wN9ZruF4ae03Re+k+eHgwA3taZXrT2pvkkSmfRex6oFZReypcPGFQYiHo31NsO39WPRYYxr4edYisVXw75x/BJyOcUULhG1YmwHYYeXOzbNp0Sf9ADtUDi0oip/BO2tkLxbE+z1GJSmC83fX2YpoK+IwDHNm+4w8OJAJBvdxA3Q4HrJBAbd8jnQYP+sBBaki8t5WuwJmfOucx0vgKJ7pzqRY/MOUVe/dACnjLgFDLuS7MMqr6xU/oMM6/rrt4209tL+GQbn/R98UKtmMRRq1hY=
|
||||
jdk:
|
||||
- oraclejdk8
|
||||
|
||||
- oraclejdk8
|
||||
android:
|
||||
components:
|
||||
- tools
|
||||
- platform-tools
|
||||
- build-tools-27.0.3
|
||||
- extra-google-m2repository
|
||||
- extra-android-m2repository
|
||||
- android-22
|
||||
- android-27
|
||||
- sys-img-${ANDROID_ABI}-${ANDROID_TARGET}
|
||||
- tools
|
||||
- platform-tools
|
||||
- build-tools-27.0.3
|
||||
- extra-google-m2repository
|
||||
- extra-android-m2repository
|
||||
- android-22
|
||||
- android-27
|
||||
- sys-img-${ANDROID_ABI}-${ANDROID_TARGET}
|
||||
licenses:
|
||||
- 'android-sdk-license-.+'
|
||||
|
||||
- android-sdk-license-.+
|
||||
before_script:
|
||||
- echo no | android create avd --force -n test -t $ANDROID_TARGET --abi $ANDROID_ABI
|
||||
- emulator -avd test -no-audio -no-window -no-boot-anim &
|
||||
- android-wait-for-emulator
|
||||
|
||||
- echo no | android create avd --force -n test -t $ANDROID_TARGET --abi $ANDROID_ABI
|
||||
- emulator -avd test -no-audio -no-window -no-boot-anim &
|
||||
- android-wait-for-emulator
|
||||
script:
|
||||
- ./gradlew clean check connectedCheck jacocoTestReport
|
||||
|
||||
- "./gradlew clean check connectedCheck jacocoTestReport"
|
||||
- if [ "$TRAVIS_PULL_REQUEST" == "false" ] && [ "$TRAVIS_BRANCH" == "master" ]; then
|
||||
./gradlew publishProdReleaseApk;
|
||||
fi
|
||||
after_success:
|
||||
- bash <(curl -s https://codecov.io/bash)
|
||||
|
||||
- bash <(curl -s https://codecov.io/bash)
|
||||
after_failure:
|
||||
- echo '*** Debug Unit Test Results ***'
|
||||
- w3m -dump ${TRAVIS_BUILD_DIR}/app/build/reports/tests/*/classes/*Test.html
|
||||
- echo '*** Connected Test Results ***'
|
||||
- w3m -dump ${TRAVIS_BUILD_DIR}/app/build/reports/androidTests/connected/flavors/*/*Test.html
|
||||
|
||||
- echo '*** Debug Unit Test Results ***'
|
||||
- w3m -dump ${TRAVIS_BUILD_DIR}/app/build/reports/tests/*/classes/*Test.html
|
||||
- echo '*** Connected Test Results ***'
|
||||
- w3m -dump ${TRAVIS_BUILD_DIR}/app/build/reports/androidTests/connected/flavors/*/*Test.html
|
||||
before_cache:
|
||||
- rm -f $HOME/.gradle/caches/modules-2/modules-2.lock
|
||||
- rm -fr $HOME/.gradle/caches/*/plugin-resolution/
|
||||
|
||||
- rm -f $HOME/.gradle/caches/modules-2/modules-2.lock
|
||||
- rm -fr $HOME/.gradle/caches/*/plugin-resolution/
|
||||
cache:
|
||||
directories:
|
||||
- $HOME/.gradle/caches/
|
||||
- $HOME/.gradle/wrapper/
|
||||
- "$HOME/.gradle/caches/"
|
||||
- "$HOME/.gradle/wrapper/"
|
||||
before_install:
|
||||
- if [ "$TRAVIS_PULL_REQUEST" == "false" ] && [ "$TRAVIS_BRANCH" == "master" ]; then
|
||||
openssl aes-256-cbc -K $encrypted_7b5c925cc32c_key -iv $encrypted_7b5c925cc32c_iv -in nr-commons.keystore.enc -out nr-commons.keystore -d;
|
||||
fi
|
||||
- if [ "$TRAVIS_PULL_REQUEST" == "false" ] && [ "$TRAVIS_BRANCH" == "master" ]; then
|
||||
openssl aes-256-cbc -K $encrypted_38ac1a5053f6_key -iv $encrypted_38ac1a5053f6_iv -in play.p12.enc -out play.p12 -d;
|
||||
fi
|
||||
|
|
|
|||
71
README.md
|
|
@ -11,37 +11,13 @@ Initially started by the Wikimedia Foundation, this app is now maintained by gra
|
|||
|
||||
## Documentation
|
||||
|
||||
We try to have an extensive documentation at [our wiki here at Github][5]:
|
||||
We try to have an extensive documentation at [our wiki here at Github][4]:
|
||||
|
||||
* [User Documentation][6]
|
||||
* [Contributor Documentation][7]
|
||||
* [Volunteers Welcome!][9]
|
||||
* [User Documentation][5]
|
||||
* [Contributor Documentation][6]
|
||||
* [Volunteers Welcome!][7]
|
||||
* [Developer Documentation][8]
|
||||
|
||||
## Libraries Used ##
|
||||
|
||||
* [Picasso][11]
|
||||
* [RSS-Parser][12]
|
||||
* [ViewPagerIndicator][13]
|
||||
* [PhotoView][14]
|
||||
* [Acra][15]
|
||||
* [Renderers][16]
|
||||
* [Gson][17]
|
||||
* [Timber][18]
|
||||
* [Java-String-Similarity][19]
|
||||
* [ReadMoreTextView][20]
|
||||
* [MaterialShowcaseView][21]
|
||||
* [Butterknife][22]
|
||||
* [OKHttp][23]
|
||||
* [Okio][24]
|
||||
* [RxJava][25]
|
||||
* [JSoup][26]
|
||||
* [Fresco][27]
|
||||
* [Stetho][28]
|
||||
* [Dagger][29]
|
||||
* [Java-HTTP-Fluent][30]
|
||||
* [CircleProgressBar][31]
|
||||
* [Leak Canary][32]
|
||||
* [Libraries Used][9]
|
||||
|
||||
## Contributors ##
|
||||
|
||||
|
|
@ -60,37 +36,18 @@ Thank you all for your work!
|
|||
|
||||
## License ##
|
||||
|
||||
This software is open source, licensed under the [Apache License 2.0][4].
|
||||
|
||||
This software is open source, licensed under the [Apache License 2.0][10].
|
||||
|
||||
|
||||
[1]: https://play.google.com/store/apps/details?id=fr.free.nrw.commons
|
||||
[2]: https://commons-app.github.io/
|
||||
[3]: https://github.com/commons-app/apps-android-commons/issues
|
||||
[4]: https://www.apache.org/licenses/LICENSE-2.0
|
||||
[5]: https://github.com/commons-app/apps-android-commons/wiki
|
||||
[6]: https://github.com/commons-app/apps-android-commons/wiki#user-documentation
|
||||
[7]: https://github.com/commons-app/apps-android-commons/wiki#contributor-documentation
|
||||
|
||||
[4]: https://github.com/commons-app/apps-android-commons/wiki
|
||||
[5]: https://github.com/commons-app/apps-android-commons/wiki#user-documentation
|
||||
[6]: https://github.com/commons-app/apps-android-commons/wiki#contributor-documentation
|
||||
[7]: https://github.com/commons-app/apps-android-commons/wiki/Volunteers-welcome%21
|
||||
[8]: https://github.com/commons-app/apps-android-commons/wiki#developer-documentation
|
||||
[9]: https://github.com/commons-app/apps-android-commons/wiki/Volunteers-welcome%21
|
||||
[10]: https://meta.wikimedia.org/wiki/Grants:Project/Improve_%27Upload_to_Commons%27_Android_App/Renewal
|
||||
[11]: https://github.com/square/picasso
|
||||
[13]: https://github.com/avianey/Android-ViewPagerIndicator
|
||||
[14]: https://github.com/chrisbanes/PhotoView
|
||||
[15]: https://github.com/ACRA/acra
|
||||
[16]: https://github.com/pedrovgs/Renderers
|
||||
[17]: https://github.com/google/gson
|
||||
[18]: https://github.com/JakeWharton/timber
|
||||
[19]: https://github.com/tdebatty/java-string-similarity
|
||||
[20]: https://github.com/bravoborja/ReadMoreTextView
|
||||
[21]: https://github.com/deano2390/MaterialShowcaseView
|
||||
[22]: https://github.com/JakeWharton/butterknife
|
||||
[23]: https://github.com/square/okhttp
|
||||
[24]: https://github.com/square/okio
|
||||
[25]: https://github.com/ReactiveX/RxJava
|
||||
[27]: https://github.com/facebook/fresco
|
||||
[28]: https://github.com/facebook/stetho
|
||||
[29]: https://github.com/google/dagger
|
||||
[30]: https://github.com/yuvipanda/java-http-fluent/blob/master/src/main/java/in/yuvi/http/fluent/Http.java
|
||||
[31]: https://github.com/dinuscxj/CircleProgressBar
|
||||
[32]: https://github.com/square/leakcanary
|
||||
[9]: https://github.com/commons-app/apps-android-commons/wiki/Libraries-used
|
||||
|
||||
[10]: https://www.apache.org/licenses/LICENSE-2.0
|
||||
|
|
|
|||
124
app/build.gradle
|
|
@ -5,83 +5,90 @@ apply plugin: 'kotlin-kapt'
|
|||
apply plugin: 'jacoco-android'
|
||||
apply from: 'quality.gradle'
|
||||
|
||||
def isRunningOnTravisAndIsNotPRBuild = System.getenv("CI") == "true" && file('../play.p12').exists()
|
||||
|
||||
if(isRunningOnTravisAndIsNotPRBuild) {
|
||||
apply plugin: 'com.github.triplet.play'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// Utils
|
||||
implementation 'com.github.nicolas-raoul:Quadtree:ac16ea8035bf07'
|
||||
implementation 'fr.avianey.com.viewpagerindicator:library:2.4.1.1@aar'
|
||||
implementation 'in.yuvi:http.fluent:1.3'
|
||||
implementation 'com.github.chrisbanes:PhotoView:2.0.0'
|
||||
|
||||
implementation 'ch.acra:acra:4.9.2'
|
||||
|
||||
implementation 'org.mediawiki:api:1.3'
|
||||
implementation 'commons-codec:commons-codec:1.10'
|
||||
implementation 'com.github.pedrovgs:renderers:3.3.3'
|
||||
implementation 'com.google.code.gson:gson:2.8.5'
|
||||
implementation 'com.jakewharton.timber:timber:4.4.0'
|
||||
implementation 'info.debatty:java-string-similarity:0.24'
|
||||
implementation 'com.borjabravo:readmoretextview:2.1.0'
|
||||
|
||||
implementation 'com.android.support.constraint:constraint-layout:1.1.3'
|
||||
|
||||
implementation 'org.slf4j:slf4j-api:1.7.25'
|
||||
api ("com.github.tony19:logback-android-classic:1.1.1-6") {
|
||||
exclude group: 'com.google.android', module: 'android'
|
||||
}
|
||||
|
||||
implementation('com.mapbox.mapboxsdk:mapbox-android-sdk:5.5.0@aar') {
|
||||
transitive = true
|
||||
}
|
||||
implementation 'com.github.deano2390:MaterialShowcaseView:1.2.0'
|
||||
|
||||
//noinspection GradleCompatible
|
||||
implementation "com.android.support:support-v4:$SUPPORT_LIB_VERSION"
|
||||
implementation "com.android.support:appcompat-v7:$SUPPORT_LIB_VERSION"
|
||||
implementation "com.android.support:design:$SUPPORT_LIB_VERSION"
|
||||
implementation "com.android.support:customtabs:$SUPPORT_LIB_VERSION"
|
||||
implementation "com.android.support:cardview-v7:$SUPPORT_LIB_VERSION"
|
||||
implementation "com.jakewharton:butterknife:$BUTTERKNIFE_VERSION"
|
||||
kapt "com.jakewharton:butterknife-compiler:$BUTTERKNIFE_VERSION"
|
||||
implementation 'in.yuvi:http.fluent:1.3'
|
||||
implementation 'com.squareup.okhttp3:okhttp:3.10.0'
|
||||
implementation 'com.squareup.okio:okio:1.14.0'
|
||||
implementation 'io.reactivex.rxjava2:rxandroid:2.1.0'
|
||||
// Because RxAndroid releases are few and far between, it is recommended you also
|
||||
|
||||
// explicitly depend on RxJava's latest version for bug fixes and new features.
|
||||
implementation 'io.reactivex.rxjava2:rxjava:2.2.0'
|
||||
implementation 'com.jakewharton.rxbinding2:rxbinding:2.1.1'
|
||||
implementation 'com.jakewharton.rxbinding2:rxbinding-support-v4:2.1.1'
|
||||
implementation 'com.jakewharton.rxbinding2:rxbinding-appcompat-v7:2.1.1'
|
||||
implementation 'com.jakewharton.rxbinding2:rxbinding-design:2.1.1'
|
||||
implementation 'com.facebook.fresco:fresco:1.10.0'
|
||||
implementation 'com.facebook.stetho:stetho:1.5.0'
|
||||
|
||||
// UI
|
||||
implementation 'fr.avianey.com.viewpagerindicator:library:2.4.1.1@aar'
|
||||
implementation 'com.github.chrisbanes:PhotoView:2.0.0'
|
||||
implementation 'com.github.pedrovgs:renderers:3.3.3'
|
||||
|
||||
implementation 'com.mapzen.android:lost:3.0.4'
|
||||
implementation('com.mapbox.mapboxsdk:mapbox-android-sdk:5.5.0@aar') {
|
||||
transitive = true
|
||||
}
|
||||
implementation 'com.github.deano2390:MaterialShowcaseView:1.2.0'
|
||||
implementation 'com.dinuscxj:circleprogressbar:1.1.1'
|
||||
implementation 'com.karumi:dexter:5.0.0'
|
||||
implementation files('libs/simplemagic-1.9.jar')
|
||||
implementation "com.jakewharton:butterknife:$BUTTERKNIFE_VERSION"
|
||||
kapt "com.jakewharton:butterknife-compiler:$BUTTERKNIFE_VERSION"
|
||||
|
||||
// Logging
|
||||
implementation 'ch.acra:acra:4.9.2'
|
||||
implementation 'com.jakewharton.timber:timber:4.4.0'
|
||||
implementation 'org.slf4j:slf4j-api:1.7.25'
|
||||
api ("com.github.tony19:logback-android-classic:1.1.1-6") {
|
||||
exclude group: 'com.google.android', module: 'android'
|
||||
}
|
||||
|
||||
// Dependency injector
|
||||
implementation "com.google.dagger:dagger:$DAGGER_VERSION"
|
||||
implementation "com.google.dagger:dagger-android-support:$DAGGER_VERSION"
|
||||
kapt "com.google.dagger:dagger-android-processor:$DAGGER_VERSION"
|
||||
kapt "com.google.dagger:dagger-compiler:$DAGGER_VERSION"
|
||||
|
||||
// Unit testing
|
||||
testImplementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
testImplementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
|
||||
testImplementation 'junit:junit:4.12'
|
||||
testImplementation 'org.robolectric:robolectric:3.7.1'
|
||||
testImplementation 'com.nhaarman:mockito-kotlin:1.5.0'
|
||||
testImplementation 'com.squareup.okhttp3:mockwebserver:3.10.0'
|
||||
implementation 'com.dinuscxj:circleprogressbar:1.1.1'
|
||||
|
||||
implementation 'com.tspoon.traceur:traceur:1.0.1'
|
||||
|
||||
// Android testing
|
||||
androidTestImplementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
androidTestImplementation 'com.squareup.okhttp3:mockwebserver:3.10.0'
|
||||
androidTestImplementation "com.android.support:support-annotations:$SUPPORT_LIB_VERSION"
|
||||
androidTestImplementation 'com.android.support.test:rules:1.0.2'
|
||||
androidTestImplementation 'com.android.support.test:runner:1.0.2'
|
||||
androidTestImplementation "com.android.support:support-annotations:$SUPPORT_LIB_VERSION"
|
||||
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
|
||||
androidTestImplementation "org.mockito:mockito-core:2.10.0"
|
||||
|
||||
// Debugging
|
||||
implementation 'com.tspoon.traceur:traceur:1.0.1'
|
||||
implementation 'com.facebook.stetho:stetho:1.5.0'
|
||||
debugImplementation "com.squareup.leakcanary:leakcanary-android:$LEAK_CANARY"
|
||||
releaseImplementation "com.squareup.leakcanary:leakcanary-android-no-op:$LEAK_CANARY"
|
||||
testImplementation "com.squareup.leakcanary:leakcanary-android-no-op:$LEAK_CANARY"
|
||||
|
||||
//For handling runtime permissions
|
||||
implementation 'com.karumi:dexter:5.0.0'
|
||||
|
||||
implementation files('libs/simplemagic-1.9.jar')
|
||||
// Support libraries
|
||||
implementation "com.android.support:support-v4:$SUPPORT_LIB_VERSION"
|
||||
implementation "com.android.support:appcompat-v7:$SUPPORT_LIB_VERSION"
|
||||
implementation "com.android.support:design:$SUPPORT_LIB_VERSION"
|
||||
implementation "com.android.support:customtabs:$SUPPORT_LIB_VERSION"
|
||||
implementation "com.android.support:cardview-v7:$SUPPORT_LIB_VERSION"
|
||||
implementation 'com.android.support.constraint:constraint-layout:1.1.3'
|
||||
}
|
||||
|
||||
android {
|
||||
|
|
@ -101,6 +108,8 @@ android {
|
|||
}
|
||||
|
||||
testOptions {
|
||||
unitTests.returnDefaultValues = true
|
||||
|
||||
unitTests.all {
|
||||
jvmArgs '-noverify'
|
||||
}
|
||||
|
|
@ -115,11 +124,18 @@ android {
|
|||
test.resources.srcDirs += 'src/main/resoures'
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
release
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled true
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
|
||||
testProguardFile 'test-proguard-rules.txt'
|
||||
if(isRunningOnTravisAndIsNotPRBuild) {
|
||||
signingConfig signingConfigs.release
|
||||
}
|
||||
}
|
||||
debug {
|
||||
minifyEnabled true
|
||||
|
|
@ -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'
|
||||
productFlavors {
|
||||
prod {
|
||||
|
|
@ -204,3 +228,17 @@ android {
|
|||
|
||||
buildToolsVersion buildToolsVersion
|
||||
}
|
||||
|
||||
if(isRunningOnTravisAndIsNotPRBuild) {
|
||||
play {
|
||||
track = "alpha"
|
||||
userFraction = 1
|
||||
serviceAccountEmail = System.getenv("SERVICE_ACCOUNT_NAME")
|
||||
serviceAccountCredentials = file("../play.p12")
|
||||
|
||||
resolutionStrategy = "auto"
|
||||
outputProcessor { // this: ApkVariantOutput
|
||||
versionNameOverride = "$versionNameOverride.$versionCode"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
BIN
app/src/betaDebug/ic_launcher-web.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 5.5 KiB |
5
app/src/betaDebug/res/mipmap-anydpi-v26/ic_launcher.xml
Normal 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>
|
||||
|
|
@ -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>
|
||||
BIN
app/src/betaDebug/res/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 2 KiB |
BIN
app/src/betaDebug/res/mipmap-hdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
app/src/betaDebug/res/mipmap-hdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
app/src/betaDebug/res/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
app/src/betaDebug/res/mipmap-mdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
app/src/betaDebug/res/mipmap-mdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
app/src/betaDebug/res/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
app/src/betaDebug/res/mipmap-xhdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
app/src/betaDebug/res/mipmap-xhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 5.6 KiB |
BIN
app/src/betaDebug/res/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 4.3 KiB |
BIN
app/src/betaDebug/res/mipmap-xxhdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 4.9 KiB |
BIN
app/src/betaDebug/res/mipmap-xxhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 8.8 KiB |
BIN
app/src/betaDebug/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 6 KiB |
BIN
app/src/betaDebug/res/mipmap-xxxhdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 6.6 KiB |
BIN
app/src/betaDebug/res/mipmap-xxxhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
4
app/src/betaDebug/res/values/ic_launcher_background.xml
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#FFFFFF</color>
|
||||
</resources>
|
||||
|
|
@ -24,7 +24,7 @@
|
|||
|
||||
<application
|
||||
android:name=".CommonsApplication"
|
||||
android:icon="@drawable/ic_launcher"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/LightAppTheme"
|
||||
android:supportsRtl="true" >
|
||||
|
|
@ -44,7 +44,7 @@
|
|||
<activity android:name=".WelcomeActivity" />
|
||||
|
||||
<activity android:name=".upload.UploadActivity"
|
||||
android:icon="@drawable/ic_launcher"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name">
|
||||
<intent-filter android:label="@string/intent_share_upload_label">
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
|
|
@ -65,7 +65,7 @@
|
|||
</activity>
|
||||
<activity
|
||||
android:name=".contributions.MainActivity"
|
||||
android:icon="@drawable/ic_launcher"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name" />
|
||||
<activity
|
||||
android:name=".settings.SettingsActivity"
|
||||
|
|
@ -102,7 +102,7 @@
|
|||
<activity
|
||||
android:name=".explore.SearchActivity"
|
||||
android:label="@string/title_activity_search"
|
||||
android:configChanges="orientation|keyboardHidden"
|
||||
android:configChanges="orientation|keyboardHidden|screenSize"
|
||||
android:parentActivityName=".contributions.MainActivity"
|
||||
/>
|
||||
|
||||
|
|
|
|||
BIN
app/src/main/ic_launcher-web.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
16
app/src/main/java/fr/free/nrw/commons/BasePresenter.java
Normal 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();
|
||||
}
|
||||
|
|
@ -91,6 +91,7 @@ public class CommonsApplication extends Application {
|
|||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
ACRA.init(this);
|
||||
if (BuildConfig.DEBUG) {
|
||||
//FIXME: Traceur should be disabled for release builds until error fixed
|
||||
//See https://github.com/commons-app/apps-android-commons/issues/1877
|
||||
|
|
@ -118,8 +119,7 @@ public class CommonsApplication extends Application {
|
|||
// Empty temp directory in case some temp files are created and never removed.
|
||||
ContributionUtils.emptyTemporaryDirectory();
|
||||
|
||||
initAcra();
|
||||
if (BuildConfig.DEBUG) {
|
||||
if (BuildConfig.DEBUG && !isRoboUnitTest()) {
|
||||
Stetho.initializeWithDefaults(this);
|
||||
}
|
||||
|
||||
|
|
@ -152,14 +152,8 @@ public class CommonsApplication extends Application {
|
|||
Timber.plant(new Timber.DebugTree());
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove ACRA's UncaughtExceptionHandler
|
||||
* We do this because ACRA's handler spawns a new process possibly screwing up with a few things
|
||||
*/
|
||||
private void initAcra() {
|
||||
Thread.UncaughtExceptionHandler exceptionHandler = Thread.getDefaultUncaughtExceptionHandler();
|
||||
ACRA.init(this);
|
||||
Thread.setDefaultUncaughtExceptionHandler(exceptionHandler);
|
||||
public static boolean isRoboUnitTest() {
|
||||
return "robolectric".equals(Build.FINGERPRINT);
|
||||
}
|
||||
|
||||
private ThreadPoolService getFileLoggingThreadPool() {
|
||||
|
|
|
|||
8
app/src/main/java/fr/free/nrw/commons/MvpView.java
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
package fr.free.nrw.commons;
|
||||
|
||||
/**
|
||||
* Base interface for all the views
|
||||
*/
|
||||
public interface MvpView {
|
||||
void showMessage(String message);
|
||||
}
|
||||
|
|
@ -2,20 +2,33 @@ package fr.free.nrw.commons;
|
|||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.view.ViewPager;
|
||||
import android.view.View;
|
||||
|
||||
import com.viewpagerindicator.CirclePageIndicator;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
|
||||
import butterknife.BindView;
|
||||
import butterknife.ButterKnife;
|
||||
import butterknife.OnClick;
|
||||
import butterknife.Optional;
|
||||
import fr.free.nrw.commons.quiz.QuizActivity;
|
||||
import fr.free.nrw.commons.theme.BaseActivity;
|
||||
|
||||
public class WelcomeActivity extends BaseActivity {
|
||||
|
||||
@BindView(R.id.welcomePager) ViewPager pager;
|
||||
@BindView(R.id.welcomePagerIndicator) CirclePageIndicator indicator;
|
||||
@Inject
|
||||
@Named("application_preferences")
|
||||
SharedPreferences prefs;
|
||||
|
||||
@BindView(R.id.welcomePager)
|
||||
ViewPager pager;
|
||||
@BindView(R.id.welcomePagerIndicator)
|
||||
CirclePageIndicator indicator;
|
||||
|
||||
private WelcomePagerAdapter adapter = new WelcomePagerAdapter();
|
||||
private boolean isQuiz;
|
||||
|
|
@ -38,15 +51,20 @@ public class WelcomeActivity extends BaseActivity {
|
|||
if (bundle != null) {
|
||||
isQuiz = bundle.getBoolean("isQuiz");
|
||||
}
|
||||
} else{
|
||||
} else {
|
||||
isQuiz = false;
|
||||
}
|
||||
|
||||
// Enable skip button if beta flavor
|
||||
if (BuildConfig.FLAVOR == "beta") {
|
||||
findViewById(R.id.finishTutorialButton).setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
ButterKnife.bind(this);
|
||||
|
||||
pager.setAdapter(adapter);
|
||||
indicator.setViewPager(pager);
|
||||
adapter.setCallback(this::finish);
|
||||
adapter.setCallback(this::finishTutorial);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -54,7 +72,7 @@ public class WelcomeActivity extends BaseActivity {
|
|||
*/
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
if (isQuiz){
|
||||
if (isQuiz) {
|
||||
Intent i = new Intent(WelcomeActivity.this, QuizActivity.class);
|
||||
startActivity(i);
|
||||
}
|
||||
|
|
@ -71,4 +89,22 @@ public class WelcomeActivity extends BaseActivity {
|
|||
Intent welcomeIntent = new Intent(context, WelcomeActivity.class);
|
||||
context.startActivity(welcomeIntent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Override onBackPressed() to go to previous tutorial 'pages' if not on first page
|
||||
*/
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
if (pager.getCurrentItem() != 0) {
|
||||
pager.setCurrentItem(pager.getCurrentItem() - 1, true);
|
||||
} else {
|
||||
finish();
|
||||
}
|
||||
}
|
||||
|
||||
@OnClick(R.id.finishTutorialButton)
|
||||
public void finishTutorial() {
|
||||
prefs.edit().putBoolean("firstrun", false).apply();
|
||||
finish();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import butterknife.OnClick;
|
|||
import butterknife.Optional;
|
||||
|
||||
public class WelcomePagerAdapter extends PagerAdapter {
|
||||
static final int[] PAGE_LAYOUTS = new int[]{
|
||||
private static final int[] PAGE_LAYOUTS = new int[]{
|
||||
R.layout.welcome_wikipedia,
|
||||
R.layout.welcome_do_upload,
|
||||
R.layout.welcome_dont_upload,
|
||||
|
|
@ -57,29 +57,31 @@ public class WelcomePagerAdapter extends PagerAdapter {
|
|||
|
||||
@Override
|
||||
public Object instantiateItem(ViewGroup container, int position) {
|
||||
this.container=container;
|
||||
this.container = container;
|
||||
LayoutInflater inflater = LayoutInflater.from(container.getContext());
|
||||
ViewGroup layout = (ViewGroup) inflater.inflate(PAGE_LAYOUTS[position], container, false);
|
||||
if (BuildConfig.FLAVOR == "beta") {
|
||||
TextView textView = layout.findViewById(R.id.welcomeYesButton);
|
||||
if (textView.getVisibility() != View.VISIBLE) {
|
||||
textView.setVisibility(View.VISIBLE);
|
||||
}
|
||||
ViewHolder holder = new ViewHolder(layout);
|
||||
layout.setTag(holder);
|
||||
|
||||
if (position == PAGE_FINAL){
|
||||
TextView moreInfo = layout.findViewById(R.id.welcomeInfo);
|
||||
moreInfo.setText(Html.fromHtml(WelcomeActivity.moreInformation));
|
||||
ViewHolder holder1 = new ViewHolder(layout);
|
||||
layout.setTag(holder1);
|
||||
}
|
||||
} else {
|
||||
if (position == PAGE_FINAL) {
|
||||
ViewHolder holder = new ViewHolder(layout);
|
||||
layout.setTag(holder);
|
||||
}
|
||||
// If final page
|
||||
if (position == PAGE_FINAL) {
|
||||
// Add link to more information
|
||||
TextView moreInfo = layout.findViewById(R.id.welcomeInfo);
|
||||
moreInfo.setText(Html.fromHtml(WelcomeActivity.moreInformation));
|
||||
moreInfo.setOnClickListener(view -> {
|
||||
try {
|
||||
Utils.handleWebUrl(
|
||||
container.getContext(),
|
||||
Uri.parse("https://commons.wikimedia.org/wiki/Help:Contents")
|
||||
);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
});
|
||||
|
||||
// Handle click of finishTutorialButton ("YES!" button) inside layout
|
||||
layout.findViewById(R.id.finishTutorialButton)
|
||||
.setOnClickListener(view -> callback.finishTutorial());
|
||||
}
|
||||
|
||||
container.addView(layout);
|
||||
return layout;
|
||||
}
|
||||
|
|
@ -96,33 +98,6 @@ public class WelcomePagerAdapter extends PagerAdapter {
|
|||
}
|
||||
|
||||
public interface Callback {
|
||||
void onYesClicked();
|
||||
}
|
||||
|
||||
class ViewHolder {
|
||||
ViewHolder(View view) {
|
||||
ButterKnife.bind(this, view);
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers on click callback on button click
|
||||
*/
|
||||
@OnClick(R.id.welcomeYesButton)
|
||||
void onClicked() {
|
||||
if (callback != null) {
|
||||
callback.onYesClicked();
|
||||
}
|
||||
}
|
||||
|
||||
@Optional
|
||||
@OnClick(R.id.welcomeInfo)
|
||||
void onHelpClicked () {
|
||||
try {
|
||||
Utils.handleWebUrl(container.getContext(),Uri.parse("https://commons.wikimedia.org/wiki/Help:Contents" ));
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
void finishTutorial();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -177,7 +177,6 @@ public class LoginActivity extends AccountAuthenticatorActivity {
|
|||
super.onResume();
|
||||
if (prefs.getBoolean("firstrun", true)) {
|
||||
WelcomeActivity.startYourself(this);
|
||||
prefs.edit().putBoolean("firstrun", false).apply();
|
||||
}
|
||||
|
||||
if (sessionManager.getCurrentAccount() != null
|
||||
|
|
@ -215,6 +214,7 @@ public class LoginActivity extends AccountAuthenticatorActivity {
|
|||
loginCurrentlyInProgress = true;
|
||||
Timber.d("Login to start!");
|
||||
final String username = canonicializeUsername(usernameEdit.getText().toString());
|
||||
final String rawUsername = Utils.capitalize(usernameEdit.getText().toString().trim());
|
||||
final String password = passwordEdit.getText().toString();
|
||||
String twoFactorCode = twoFactorEdit.getText().toString();
|
||||
|
||||
|
|
@ -222,7 +222,7 @@ public class LoginActivity extends AccountAuthenticatorActivity {
|
|||
Observable.fromCallable(() -> login(username, password, twoFactorCode))
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(result -> handleLogin(username, password, result));
|
||||
.subscribe(result -> handleLogin(username, rawUsername, password, result));
|
||||
}
|
||||
|
||||
private String login(String username, String password, String twoFactorCode) {
|
||||
|
|
@ -238,10 +238,10 @@ public class LoginActivity extends AccountAuthenticatorActivity {
|
|||
}
|
||||
}
|
||||
|
||||
private void handleLogin(String username, String password, String result) {
|
||||
private void handleLogin(String username, String rawUsername, String password, String result) {
|
||||
Timber.d("Login done!");
|
||||
if (result.equals("PASS")) {
|
||||
handlePassResult(username, password);
|
||||
handlePassResult(username, rawUsername , password);
|
||||
} else {
|
||||
loginCurrentlyInProgress = false;
|
||||
errorMessageShown = true;
|
||||
|
|
@ -259,7 +259,7 @@ public class LoginActivity extends AccountAuthenticatorActivity {
|
|||
progressDialog.show();
|
||||
}
|
||||
|
||||
private void handlePassResult(String username, String password) {
|
||||
private void handlePassResult(String username, String rawUsername, String password) {
|
||||
showSuccessAndDismissDialog();
|
||||
requestAuthToken();
|
||||
AccountAuthenticatorResponse response = null;
|
||||
|
|
@ -276,7 +276,7 @@ public class LoginActivity extends AccountAuthenticatorActivity {
|
|||
}
|
||||
}
|
||||
|
||||
sessionManager.createAccount(response, username, password);
|
||||
sessionManager.createAccount(response, username, rawUsername, password);
|
||||
startMainActivity();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -28,7 +28,8 @@ public class SessionManager {
|
|||
private final MediaWikiApi mediaWikiApi;
|
||||
private Account currentAccount; // Unlike a savings account... ;-)
|
||||
private SharedPreferences sharedPreferences;
|
||||
|
||||
private static final String KEY_RAWUSERNAME = "rawusername";
|
||||
private Bundle userdata = new Bundle();
|
||||
|
||||
public SessionManager(Context context,
|
||||
MediaWikiApi mediaWikiApi,
|
||||
|
|
@ -44,13 +45,15 @@ public class SessionManager {
|
|||
*
|
||||
* @param response
|
||||
* @param username
|
||||
* @param rawusername
|
||||
* @param password
|
||||
*/
|
||||
public void createAccount(@Nullable AccountAuthenticatorResponse response,
|
||||
String username, String password) {
|
||||
String username, String rawusername, String password) {
|
||||
|
||||
Account account = new Account(username, BuildConfig.ACCOUNT_TYPE);
|
||||
boolean created = accountManager().addAccountExplicitly(account, password, null);
|
||||
userdata.putString(KEY_RAWUSERNAME, rawusername);
|
||||
boolean created = accountManager().addAccountExplicitly(account, password, userdata);
|
||||
|
||||
Timber.d("account creation " + (created ? "successful" : "failure"));
|
||||
|
||||
|
|
@ -97,6 +100,17 @@ public class SessionManager {
|
|||
return account == null ? null : account.name;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getRawUserName() {
|
||||
Account account = getCurrentAccount();
|
||||
return account == null ? null : accountManager().getUserData(account, KEY_RAWUSERNAME);
|
||||
}
|
||||
|
||||
public String getAuthorName(){
|
||||
return getRawUserName() == null ? getUserName() : getRawUserName();
|
||||
}
|
||||
|
||||
|
||||
@Nullable
|
||||
public String getPassword() {
|
||||
Account account = getCurrentAccount();
|
||||
|
|
|
|||
|
|
@ -34,6 +34,8 @@ import timber.log.Timber;
|
|||
|
||||
import static android.app.Activity.RESULT_OK;
|
||||
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
|
||||
import static fr.free.nrw.commons.wikidata.WikidataConstants.WIKIDATA_ENTITY_ID_PREF;
|
||||
import static fr.free.nrw.commons.wikidata.WikidataConstants.WIKIDATA_ITEM_LOCATION;
|
||||
|
||||
public class BookmarkLocationsFragment extends DaggerFragment {
|
||||
|
||||
|
|
@ -136,13 +138,14 @@ public class BookmarkLocationsFragment extends DaggerFragment {
|
|||
if (resultCode == RESULT_OK) {
|
||||
Timber.d("OnActivityResult() parameters: Req code: %d Result code: %d Data: %s",
|
||||
requestCode, resultCode, data);
|
||||
String wikidataEntityId = directPrefs.getString("WikiDataEntityId", null);
|
||||
String wikidataEntityId = directPrefs.getString(WIKIDATA_ENTITY_ID_PREF, null);
|
||||
String wikidataItemLocation = directPrefs.getString(WIKIDATA_ITEM_LOCATION, null);
|
||||
if (requestCode == ContributionController.SELECT_FROM_CAMERA) {
|
||||
// If coming from camera, pass null as uri. Because camera photos get saved to a
|
||||
// fixed directory
|
||||
contributionController.handleImagePicked(requestCode, null, true, wikidataEntityId);
|
||||
contributionController.handleImagePicked(requestCode, null, true, wikidataEntityId, wikidataItemLocation);
|
||||
} else {
|
||||
contributionController.handleImagePicked(requestCode, data.getData(), true, wikidataEntityId);
|
||||
contributionController.handleImagePicked(requestCode, data.getData(), true, wikidataEntityId, wikidataItemLocation);
|
||||
}
|
||||
} else {
|
||||
Timber.e("OnActivityResult() parameters: Req code: %d Result code: %d Data: %s",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -26,9 +26,11 @@ public class CategoryImageUtils {
|
|||
*/
|
||||
public static List<Media> getMediaList(NodeList childNodes) {
|
||||
List<Media> categoryImages = new ArrayList<>();
|
||||
|
||||
for (int i = 0; i < childNodes.getLength(); i++) {
|
||||
Node node = childNodes.item(i);
|
||||
if (getMediaFromPage(node).getFilename().substring(0,5).equals("File:")){
|
||||
|
||||
if (getFileName(node).substring(0, 5).equals("File:")) {
|
||||
categoryImages.add(getMediaFromPage(node));
|
||||
}
|
||||
}
|
||||
|
|
@ -46,7 +48,7 @@ public class CategoryImageUtils {
|
|||
List<String> subCategories = new ArrayList<>();
|
||||
for (int i = 0; i < childNodes.getLength(); i++) {
|
||||
Node node = childNodes.item(i);
|
||||
subCategories.add(getMediaFromPage(node).getFilename());
|
||||
subCategories.add(getFileName(node));
|
||||
}
|
||||
Collections.sort(subCategories);
|
||||
return subCategories;
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ import static fr.free.nrw.commons.contributions.Contribution.SOURCE_CAMERA;
|
|||
import static fr.free.nrw.commons.contributions.Contribution.SOURCE_GALLERY;
|
||||
import static fr.free.nrw.commons.upload.UploadService.EXTRA_SOURCE;
|
||||
import static fr.free.nrw.commons.wikidata.WikidataConstants.WIKIDATA_ENTITY_ID_PREF;
|
||||
import static fr.free.nrw.commons.wikidata.WikidataConstants.WIKIDATA_ITEM_LOCATION;
|
||||
|
||||
public class ContributionController {
|
||||
|
||||
|
|
@ -131,7 +132,7 @@ public class ContributionController {
|
|||
}
|
||||
}
|
||||
|
||||
public void handleImagePicked(int requestCode, @Nullable Uri uri, boolean isDirectUpload, String wikiDataEntityId) {
|
||||
public void handleImagePicked(int requestCode, @Nullable Uri uri, boolean isDirectUpload, String wikiDataEntityId, String wikidateItemLocation) {
|
||||
FragmentActivity activity = fragment.getActivity();
|
||||
Timber.d("handleImagePicked() called with onActivityResult(). Boolean isDirectUpload: " + isDirectUpload + "String wikiDataEntityId: " + wikiDataEntityId);
|
||||
Intent shareIntent = new Intent(activity, UploadActivity.class);
|
||||
|
|
@ -163,6 +164,7 @@ public class ContributionController {
|
|||
try {
|
||||
if (wikiDataEntityId != null && !wikiDataEntityId.equals("")) {
|
||||
shareIntent.putExtra(WIKIDATA_ENTITY_ID_PREF, wikiDataEntityId);
|
||||
shareIntent.putExtra(WIKIDATA_ITEM_LOCATION, wikidateItemLocation);
|
||||
}
|
||||
} catch (SecurityException e) {
|
||||
Timber.e(e, "Security Exception");
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import android.os.Bundle;
|
|||
import android.os.IBinder;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v4.app.FragmentManager;
|
||||
import android.support.v4.app.FragmentTransaction;
|
||||
|
||||
|
|
@ -22,6 +23,8 @@ import android.support.v4.content.Loader;
|
|||
import android.support.v4.app.LoaderManager;
|
||||
import android.support.v4.widget.CursorAdapter;
|
||||
import android.support.v7.app.AlertDialog;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
|
@ -30,6 +33,14 @@ import android.widget.AdapterView;
|
|||
import android.widget.CheckBox;
|
||||
import android.widget.CompoundButton;
|
||||
|
||||
import android.widget.Toast;
|
||||
import butterknife.BindView;
|
||||
import butterknife.ButterKnife;
|
||||
import fr.free.nrw.commons.campaigns.Campaign;
|
||||
import fr.free.nrw.commons.campaigns.CampaignResponseDTO;
|
||||
import fr.free.nrw.commons.campaigns.CampaignView;
|
||||
import fr.free.nrw.commons.campaigns.CampaignsPresenter;
|
||||
import fr.free.nrw.commons.campaigns.ICampaignsView;
|
||||
import java.util.ArrayList;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
|
||||
|
|
@ -60,6 +71,7 @@ import io.reactivex.android.schedulers.AndroidSchedulers;
|
|||
import io.reactivex.disposables.CompositeDisposable;
|
||||
import io.reactivex.disposables.Disposable;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import org.acra.util.ToastSender;
|
||||
import timber.log.Timber;
|
||||
|
||||
import static fr.free.nrw.commons.contributions.Contribution.STATE_FAILED;
|
||||
|
|
@ -76,7 +88,7 @@ public class ContributionsFragment
|
|||
MediaDetailPagerFragment.MediaDetailProvider,
|
||||
FragmentManager.OnBackStackChangedListener,
|
||||
ContributionsListFragment.SourceRefresher,
|
||||
LocationUpdateListener
|
||||
LocationUpdateListener,ICampaignsView
|
||||
{
|
||||
@Inject
|
||||
@Named("default_preferences")
|
||||
|
|
@ -112,6 +124,10 @@ public class ContributionsFragment
|
|||
private boolean isFragmentAttachedBefore = false;
|
||||
private View checkBoxView;
|
||||
private CheckBox checkBox;
|
||||
private CampaignsPresenter presenter;
|
||||
|
||||
|
||||
@BindView(R.id.campaigns_view) CampaignView campaignView;
|
||||
|
||||
/**
|
||||
* Since we will need to use parent activity on onAuthCookieAcquired, we have to wait
|
||||
|
|
@ -142,6 +158,10 @@ public class ContributionsFragment
|
|||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
View view = inflater.inflate(R.layout.fragment_contributions, container, false);
|
||||
ButterKnife.bind(this, view);
|
||||
presenter = new CampaignsPresenter();
|
||||
presenter.onAttachView(this);
|
||||
campaignView.setVisibility(View.GONE);
|
||||
nearbyNoificationCardView = view.findViewById(R.id.card_view_nearby);
|
||||
checkBoxView = View.inflate(getActivity(), R.layout.nearby_permission_dialog, null);
|
||||
checkBox = (CheckBox) checkBoxView.findViewById(R.id.never_ask_again);
|
||||
|
|
@ -173,6 +193,27 @@ public class ContributionsFragment
|
|||
setUploadCount();
|
||||
}
|
||||
|
||||
getChildFragmentManager().registerFragmentLifecycleCallbacks(
|
||||
new FragmentManager.FragmentLifecycleCallbacks() {
|
||||
@Override public void onFragmentResumed(FragmentManager fm, Fragment f) {
|
||||
super.onFragmentResumed(fm, f);
|
||||
//If media detail pager fragment is visible, hide the campaigns view [might not be the best way to do, this but yeah, this proves to work for now]
|
||||
Log.e("#CF#", "onFragmentResumed" + f.getClass().getName());
|
||||
if (f instanceof MediaDetailPagerFragment) {
|
||||
campaignView.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override public void onFragmentDetached(FragmentManager fm, Fragment f) {
|
||||
super.onFragmentDetached(fm, f);
|
||||
Log.e("#CF#", "onFragmentDetached" + f.getClass().getName());
|
||||
//If media detail pager fragment is detached, ContributionsList fragment is gonna be visible, [becomes tightly coupled though]
|
||||
if (f instanceof MediaDetailPagerFragment) {
|
||||
fetchCampaigns();
|
||||
}
|
||||
}
|
||||
}, true);
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
|
|
@ -537,7 +578,7 @@ public class ContributionsFragment
|
|||
nearbyNoificationCardView.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
|
||||
fetchCampaigns();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -622,7 +663,7 @@ public class ContributionsFragment
|
|||
curLatLng = locationManager.getLastLocation();
|
||||
|
||||
placesDisposable = Observable.fromCallable(() -> nearbyController
|
||||
.loadAttractionsFromLocation(curLatLng, true)) // thanks to boolean, it will only return closest result
|
||||
.loadAttractionsFromLocation(curLatLng, curLatLng, true, false)) // thanks to boolean, it will only return closest result
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(this::updateNearbyNotification,
|
||||
|
|
@ -694,5 +735,38 @@ public class ContributionsFragment
|
|||
// Update closest nearby card view if location changed more than 500 meters
|
||||
updateClosestNearbyCardViewInfo();
|
||||
}
|
||||
|
||||
@Override public void onViewCreated(@NonNull View view,
|
||||
@Nullable Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
}
|
||||
|
||||
/**
|
||||
* ask the presenter to fetch the campaigns only if user has not manually disabled it
|
||||
*/
|
||||
private void fetchCampaigns() {
|
||||
if (prefs.getBoolean("displayCampaignsCardView", true)) {
|
||||
presenter.getCampaigns();
|
||||
}
|
||||
}
|
||||
|
||||
@Override public void showMessage(String message) {
|
||||
Toast.makeText(getContext(), message, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
|
||||
@Override public MediaWikiApi getMediaWikiApi() {
|
||||
return mediaWikiApi;
|
||||
}
|
||||
|
||||
@Override public void showCampaigns(Campaign campaign) {
|
||||
if (campaign != null) {
|
||||
campaignView.setCampaign(campaign);
|
||||
}
|
||||
}
|
||||
|
||||
@Override public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
presenter.onDetachView();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -255,11 +255,11 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment {
|
|||
if (requestCode == ContributionController.SELECT_FROM_CAMERA) {
|
||||
// If coming from camera, pass null as uri. Because camera photos get saved to a
|
||||
// fixed directory
|
||||
controller.handleImagePicked(requestCode, null, false, null);
|
||||
controller.handleImagePicked(requestCode, null, false, null, null);
|
||||
} else if (requestCode == ContributionController.PICK_IMAGE_MULTIPLE) {
|
||||
handleMultipleImages(requestCode, data);
|
||||
} else if (requestCode == ContributionController.SELECT_FROM_GALLERY){
|
||||
controller.handleImagePicked(requestCode, data.getData(), false, null);
|
||||
controller.handleImagePicked(requestCode, data.getData(), false, null, null);
|
||||
}
|
||||
} else {
|
||||
Timber.e("OnActivityResult() parameters: Req code: %d Result code: %d Data: %s",
|
||||
|
|
@ -319,7 +319,7 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment {
|
|||
Log.v("LOG_TAG", "Selected Images" + mArrayUri.size());
|
||||
controller.handleImagesPicked(requestCode, mArrayUri);
|
||||
} else if(data.getData() != null) {
|
||||
controller.handleImagePicked(SELECT_FROM_GALLERY, data.getData(), false, null);
|
||||
controller.handleImagePicked(SELECT_FROM_GALLERY, data.getData(), false, null, null);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
101
app/src/main/java/fr/free/nrw/commons/delete/ReasonBuilder.java
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ package fr.free.nrw.commons.explore;
|
|||
import android.content.res.Configuration;
|
||||
import android.database.DataSetObserver;
|
||||
import android.os.Bundle;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.design.widget.TabLayout;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v4.app.FragmentManager;
|
||||
|
|
@ -57,9 +58,16 @@ public class SearchActivity extends NavigationBaseActivity implements MediaDetai
|
|||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
boolean currentThemeIsDark = PreferenceManager.getDefaultSharedPreferences(this).getBoolean("theme", false);
|
||||
setContentView(R.layout.activity_search);
|
||||
ButterKnife.bind(this);
|
||||
initDrawer();
|
||||
if (currentThemeIsDark) {
|
||||
searchView.setBackgroundResource(R.color.vpi__bright_foreground_disabled_holo_dark);
|
||||
tabLayout.setBackgroundResource(R.color.vpi__bright_foreground_disabled_holo_dark);
|
||||
toolbar.setBackgroundResource(R.color.vpi__bright_foreground_disabled_holo_dark);
|
||||
viewPager.setBackgroundResource(R.color.vpi__bright_foreground_disabled_holo_dark);
|
||||
}
|
||||
setTitle(getString(R.string.title_activity_search));
|
||||
toolbar.setNavigationOnClickListener(v->onBackPressed());
|
||||
supportFragmentManager = getSupportFragmentManager();
|
||||
|
|
@ -93,9 +101,9 @@ public class SearchActivity extends NavigationBaseActivity implements MediaDetai
|
|||
searchImageFragment = new SearchImageFragment();
|
||||
searchCategoryFragment= new SearchCategoryFragment();
|
||||
fragmentList.add(searchImageFragment);
|
||||
titleList.add("MEDIA");
|
||||
titleList.add(getResources().getString(R.string.search_tab_title_media));
|
||||
fragmentList.add(searchCategoryFragment);
|
||||
titleList.add("CATEGORIES");
|
||||
titleList.add(getResources().getString(R.string.search_tab_title_categories));
|
||||
|
||||
viewPagerAdapter.setTabData(fragmentList, titleList);
|
||||
viewPagerAdapter.notifyDataSetChanged();
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
package fr.free.nrw.commons.explore.recentsearches;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.v7.app.AlertDialog;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
|
|
@ -8,6 +10,7 @@ import android.view.ViewGroup;
|
|||
import android.widget.ArrayAdapter;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.ListView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import java.util.List;
|
||||
|
|
@ -31,6 +34,9 @@ public class RecentSearchesFragment extends CommonsDaggerSupportFragment {
|
|||
ArrayAdapter adapter;
|
||||
@BindView(R.id.recent_searches_delete_button)
|
||||
ImageView recent_searches_delete_button;
|
||||
boolean currentThemeIsDark = false;
|
||||
@BindView(R.id.recent_searches_text_view)
|
||||
TextView recent_searches_text_view;
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
|
|
@ -38,21 +44,33 @@ public class RecentSearchesFragment extends CommonsDaggerSupportFragment {
|
|||
View rootView = inflater.inflate(R.layout.fragment_search_history, container, false);
|
||||
ButterKnife.bind(this, rootView);
|
||||
recentSearches = recentSearchesDao.recentSearches(10);
|
||||
recent_searches_delete_button.setOnClickListener(v -> new AlertDialog.Builder(getContext())
|
||||
.setMessage(getString(R.string.delete_recent_searches_dialog))
|
||||
.setPositiveButton(android.R.string.yes, (dialog, which) -> {
|
||||
recentSearchesDao.deleteAll(recentSearches);
|
||||
Toast.makeText(getContext(),getString(R.string.search_history_deleted),Toast.LENGTH_SHORT).show();
|
||||
recentSearches = recentSearchesDao.recentSearches(10);
|
||||
adapter = new ArrayAdapter<String>(getContext(),R.layout.item_recent_searches, recentSearches);
|
||||
recentSearchesList.setAdapter(adapter);
|
||||
adapter.notifyDataSetChanged();
|
||||
dialog.dismiss();
|
||||
})
|
||||
.setNegativeButton(android.R.string.no, null)
|
||||
.create()
|
||||
.show());
|
||||
adapter = new ArrayAdapter<String>(getContext(),R.layout.item_recent_searches, recentSearches);
|
||||
|
||||
if(recentSearches.isEmpty()) {
|
||||
recent_searches_delete_button.setVisibility(View.GONE);
|
||||
recent_searches_text_view.setText(R.string.no_recent_searches);
|
||||
}
|
||||
|
||||
recent_searches_delete_button.setOnClickListener(v -> {
|
||||
new AlertDialog.Builder(getContext())
|
||||
.setMessage(getString(R.string.delete_recent_searches_dialog))
|
||||
.setPositiveButton(android.R.string.yes, (dialog, which) -> {
|
||||
recentSearchesDao.deleteAll(recentSearches);
|
||||
recent_searches_delete_button.setVisibility(View.GONE);
|
||||
recent_searches_text_view.setText(R.string.no_recent_searches);
|
||||
Toast.makeText(getContext(),getString(R.string.search_history_deleted),Toast.LENGTH_SHORT).show();
|
||||
recentSearches = recentSearchesDao.recentSearches(10);
|
||||
adapter = new ArrayAdapter<String>(getContext(),R.layout.item_recent_searches, recentSearches);
|
||||
recentSearchesList.setAdapter(adapter);
|
||||
adapter.notifyDataSetChanged();
|
||||
dialog.dismiss();
|
||||
})
|
||||
.setNegativeButton(android.R.string.no, null)
|
||||
.create()
|
||||
.show();
|
||||
});
|
||||
currentThemeIsDark = PreferenceManager.getDefaultSharedPreferences(getContext()).getBoolean("theme", false);
|
||||
setAdapterForThemes(getContext(), currentThemeIsDark);
|
||||
|
||||
recentSearchesList.setAdapter(adapter);
|
||||
recentSearchesList.setOnItemClickListener((parent, view, position, id) -> (
|
||||
(SearchActivity)getContext()).updateText(recentSearches.get(position)));
|
||||
|
|
@ -76,8 +94,21 @@ public class RecentSearchesFragment extends CommonsDaggerSupportFragment {
|
|||
*/
|
||||
public void updateRecentSearches() {
|
||||
recentSearches = recentSearchesDao.recentSearches(10);
|
||||
adapter = new ArrayAdapter<String>(getContext(),R.layout.item_recent_searches, recentSearches);
|
||||
setAdapterForThemes(getContext(), currentThemeIsDark);
|
||||
recentSearchesList.setAdapter(adapter);
|
||||
adapter.notifyDataSetChanged();
|
||||
|
||||
if(!recentSearches.isEmpty()) {
|
||||
recent_searches_delete_button.setVisibility(View.VISIBLE);
|
||||
recent_searches_text_view.setText(R.string.search_recent_header);
|
||||
}
|
||||
}
|
||||
|
||||
private void setAdapterForThemes(Context context, boolean currentThemeIsDark) {
|
||||
if (currentThemeIsDark) {
|
||||
adapter = new ArrayAdapter<String>(context, R.layout.item_recent_searches_dark_theme, recentSearches);
|
||||
} else {
|
||||
adapter = new ArrayAdapter<String>(context, R.layout.item_recent_searches, recentSearches);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,15 +14,18 @@ import android.text.Editable;
|
|||
import android.text.Html;
|
||||
import android.text.TextUtils;
|
||||
import android.text.TextWatcher;
|
||||
import android.util.Log;
|
||||
import android.util.TypedValue;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewTreeObserver;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.ScrollView;
|
||||
import android.widget.Spinner;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
|
|
@ -44,9 +47,11 @@ import fr.free.nrw.commons.Media;
|
|||
import fr.free.nrw.commons.MediaDataExtractor;
|
||||
import fr.free.nrw.commons.MediaWikiImageView;
|
||||
import fr.free.nrw.commons.R;
|
||||
import fr.free.nrw.commons.auth.SessionManager;
|
||||
import fr.free.nrw.commons.category.CategoryDetailsActivity;
|
||||
import fr.free.nrw.commons.contributions.ContributionsFragment;
|
||||
import fr.free.nrw.commons.delete.DeleteTask;
|
||||
import fr.free.nrw.commons.delete.ReasonBuilder;
|
||||
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
|
||||
import fr.free.nrw.commons.location.LatLng;
|
||||
import fr.free.nrw.commons.mwapi.MediaWikiApi;
|
||||
|
|
@ -65,6 +70,8 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
|
|||
private MediaDetailPagerFragment.MediaDetailProvider detailProvider;
|
||||
private int index;
|
||||
private Locale locale;
|
||||
private boolean isDeleted = false;
|
||||
|
||||
|
||||
public static MediaDetailFragment forMedia(int index, boolean editable, boolean isCategoryImage) {
|
||||
MediaDetailFragment mf = new MediaDetailFragment();
|
||||
|
|
@ -85,6 +92,8 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
|
|||
Provider<MediaDataExtractor> mediaDataExtractorProvider;
|
||||
@Inject
|
||||
MediaWikiApi mwApi;
|
||||
@Inject
|
||||
SessionManager sessionManager;
|
||||
|
||||
private int initialListTop = 0;
|
||||
|
||||
|
|
@ -128,6 +137,8 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
|
|||
|
||||
//Had to make this class variable, to implement various onClicks, which access the media, also I fell why make separate variables when one can serve the purpose
|
||||
private Media media;
|
||||
private ArrayList<String> reasonList;
|
||||
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(Bundle outState) {
|
||||
|
|
@ -163,6 +174,13 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
|
|||
initialListTop = 0;
|
||||
}
|
||||
|
||||
reasonList = new ArrayList<>();
|
||||
reasonList.add(getString(R.string.deletion_reason_uploaded_by_mistake));
|
||||
reasonList.add(getString(R.string.deletion_reason_publicly_visible));
|
||||
reasonList.add(getString(R.string.deletion_reason_not_interesting));
|
||||
reasonList.add(getString(R.string.deletion_reason_no_longer_want_public));
|
||||
reasonList.add(getString(R.string.deletion_reason_bad_for_my_privacy));
|
||||
|
||||
categoryNames = new ArrayList<>();
|
||||
categoryNames.add(getString(R.string.detail_panel_cats_loading));
|
||||
|
||||
|
|
@ -379,48 +397,43 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
|
|||
|
||||
@OnClick(R.id.nominateDeletion)
|
||||
public void onDeleteButtonClicked(){
|
||||
//Reviewer correct me if i have misunderstood something over here
|
||||
//But how does this if (delete.getVisibility() == View.VISIBLE) {
|
||||
// enableDeleteButton(true); makes sense ?
|
||||
AlertDialog.Builder alert = new AlertDialog.Builder(getActivity());
|
||||
alert.setMessage("Why should this file be deleted?");
|
||||
final EditText input = new EditText(getActivity());
|
||||
alert.setView(input);
|
||||
input.requestFocus();
|
||||
alert.setPositiveButton(R.string.ok, (dialog, whichButton) -> {
|
||||
String reason = input.getText().toString();
|
||||
DeleteTask deleteTask = new DeleteTask(getActivity(), media, reason);
|
||||
deleteTask.execute();
|
||||
enableDeleteButton(false);
|
||||
});
|
||||
alert.setNegativeButton(R.string.cancel, (dialog, whichButton) -> {
|
||||
});
|
||||
AlertDialog d = alert.create();
|
||||
input.addTextChangedListener(new TextWatcher() {
|
||||
private void handleText() {
|
||||
final Button okButton = d.getButton(AlertDialog.BUTTON_POSITIVE);
|
||||
if (input.getText().length() == 0) {
|
||||
okButton.setEnabled(false);
|
||||
} else {
|
||||
okButton.setEnabled(true);
|
||||
}
|
||||
}
|
||||
final ArrayAdapter<String> languageAdapter = new ArrayAdapter<String>(getActivity(),
|
||||
R.layout.simple_spinner_dropdown_list, reasonList);
|
||||
final Spinner spinner = new Spinner(getActivity());
|
||||
spinner.setLayoutParams(new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT));
|
||||
spinner.setAdapter(languageAdapter);
|
||||
spinner.setGravity(17);
|
||||
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
|
||||
builder.setView(spinner);
|
||||
builder.setTitle(R.string.nominate_delete)
|
||||
.setPositiveButton(R.string.about_translate_proceed, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
String reason = spinner.getSelectedItem().toString();
|
||||
ReasonBuilder reasonBuilder = new ReasonBuilder(reason,
|
||||
getActivity(),
|
||||
media,
|
||||
sessionManager,
|
||||
mwApi);
|
||||
reason = reasonBuilder.getReason();
|
||||
DeleteTask deleteTask = new DeleteTask(getActivity(), media, reason);
|
||||
deleteTask.execute();
|
||||
isDeleted = true;
|
||||
enableDeleteButton(false);
|
||||
}
|
||||
});
|
||||
builder.setNegativeButton(R.string.about_translate_cancel, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void afterTextChanged(Editable arg0) {
|
||||
handleText();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
dialog.dismiss();
|
||||
}
|
||||
});
|
||||
d.show();
|
||||
d.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false);
|
||||
AlertDialog dialog = builder.create();
|
||||
dialog.show();
|
||||
if(isDeleted) {
|
||||
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false);
|
||||
}
|
||||
}
|
||||
|
||||
@OnClick(R.id.seeMore)
|
||||
|
|
@ -442,8 +455,19 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
|
|||
private void rebuildCatList() {
|
||||
categoryContainer.removeAllViews();
|
||||
// @fixme add the category items
|
||||
for (String cat : categoryNames) {
|
||||
View catLabel = buildCatLabel(cat, categoryContainer);
|
||||
|
||||
//As per issue #1826(see https://github.com/commons-app/apps-android-commons/issues/1826), some categories come suffixed with strings prefixed with |. As per the discussion
|
||||
//that was meant for alphabetical sorting of the categories and can be safely removed.
|
||||
for (int i = 0; i < categoryNames.size(); i++) {
|
||||
String categoryName = categoryNames.get(i);
|
||||
//Removed everything after '|'
|
||||
int indexOfPipe = categoryName.indexOf('|');
|
||||
if (indexOfPipe != -1) {
|
||||
categoryName = categoryName.substring(0, indexOfPipe);
|
||||
//Set the updated category to the list as well
|
||||
categoryNames.set(i, categoryName);
|
||||
}
|
||||
View catLabel = buildCatLabel(categoryName, categoryContainer);
|
||||
categoryContainer.addView(catLabel);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -343,7 +343,7 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple
|
|||
|
||||
@Override
|
||||
public void onPageScrolled(int i, float v, int i2) {
|
||||
if (getActivity() == null) {
|
||||
if(getActivity() == null) {
|
||||
Timber.d("Returning as activity is destroyed!");
|
||||
return;
|
||||
}
|
||||
|
|
@ -398,7 +398,7 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple
|
|||
public Fragment getItem(int i) {
|
||||
if (i == 0) {
|
||||
// See bug https://code.google.com/p/android/issues/detail?id=27526
|
||||
if (getActivity() == null) {
|
||||
if(getActivity() == null) {
|
||||
Timber.d("Skipping getItem. Returning as activity is destroyed!");
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import android.text.TextUtils;
|
|||
|
||||
import com.google.gson.Gson;
|
||||
|
||||
import fr.free.nrw.commons.campaigns.CampaignResponseDTO;
|
||||
import org.apache.http.HttpResponse;
|
||||
import org.apache.http.conn.ClientConnectionManager;
|
||||
import org.apache.http.conn.scheme.PlainSocketFactory;
|
||||
|
|
@ -77,6 +78,8 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
|
|||
private SharedPreferences categoryPreferences;
|
||||
private Gson gson;
|
||||
private final OkHttpClient okHttpClient;
|
||||
private final String WIKIMEDIA_CAMPAIGNS_BASE_URL =
|
||||
"https://raw.githubusercontent.com/commons-app/campaigns/master/campaigns.json";
|
||||
|
||||
public ApacheHttpClientMediaWikiApi(Context context,
|
||||
String apiURL,
|
||||
|
|
@ -1056,4 +1059,18 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
|
|||
}
|
||||
}
|
||||
|
||||
@Override public Single<CampaignResponseDTO> getCampaigns() {
|
||||
return Single.fromCallable(() -> {
|
||||
Request request = new Request.Builder().url(WIKIMEDIA_CAMPAIGNS_BASE_URL).build();
|
||||
Response response = okHttpClient.newCall(request).execute();
|
||||
if (response != null && response.body() != null && response.isSuccessful()) {
|
||||
String json = response.body().string();
|
||||
if (json == null) {
|
||||
return null;
|
||||
}
|
||||
return gson.fromJson(json, CampaignResponseDTO.class);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import android.net.Uri;
|
|||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import fr.free.nrw.commons.campaigns.CampaignResponseDTO;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.List;
|
||||
|
|
@ -105,6 +106,8 @@ public interface MediaWikiApi {
|
|||
|
||||
void logout();
|
||||
|
||||
Single<CampaignResponseDTO> getCampaigns();
|
||||
|
||||
interface ProgressListener {
|
||||
void onProgress(long transferred, long total);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import android.content.SharedPreferences;
|
|||
import android.content.res.Resources;
|
||||
import android.graphics.Bitmap;
|
||||
import android.support.graphics.drawable.VectorDrawableCompat;
|
||||
import android.util.Log;
|
||||
|
||||
import com.mapbox.mapboxsdk.annotations.IconFactory;
|
||||
|
||||
|
|
@ -31,6 +32,8 @@ public class NearbyController {
|
|||
private static final int MAX_RESULTS = 1000;
|
||||
private final NearbyPlaces nearbyPlaces;
|
||||
private final SharedPreferences prefs;
|
||||
public static double searchedRadius = 10.0; //in kilometers
|
||||
public static LatLng currentLocation;
|
||||
|
||||
@Inject
|
||||
public NearbyController(NearbyPlaces nearbyPlaces,
|
||||
|
|
@ -44,18 +47,21 @@ public class NearbyController {
|
|||
* Prepares Place list to make their distance information update later.
|
||||
*
|
||||
* @param curLatLng current location for user
|
||||
* @param latLangToSearchAround the location user wants to search around
|
||||
* @param returnClosestResult if this search is done to find closest result or all results
|
||||
* @return NearbyPlacesInfo a variable holds Place list without distance information
|
||||
* and boundary coordinates of current Place List
|
||||
*/
|
||||
public NearbyPlacesInfo loadAttractionsFromLocation(LatLng curLatLng, boolean returnClosestResult) throws IOException {
|
||||
Timber.d("Loading attractions near %s", curLatLng);
|
||||
public NearbyPlacesInfo loadAttractionsFromLocation(LatLng curLatLng, LatLng latLangToSearchAround, boolean returnClosestResult, boolean checkingAroundCurrentLocation) throws IOException {
|
||||
|
||||
Timber.d("Loading attractions near %s", latLangToSearchAround);
|
||||
NearbyPlacesInfo nearbyPlacesInfo = new NearbyPlacesInfo();
|
||||
|
||||
if (curLatLng == null) {
|
||||
if (latLangToSearchAround == null) {
|
||||
Timber.d("Loading attractions neari, but curLatLng is null");
|
||||
return null;
|
||||
}
|
||||
List<Place> places = nearbyPlaces.getFromWikidataQuery(curLatLng, Locale.getDefault().getLanguage(), returnClosestResult);
|
||||
List<Place> places = nearbyPlaces.getFromWikidataQuery(latLangToSearchAround, Locale.getDefault().getLanguage(), returnClosestResult);
|
||||
|
||||
if (null != places && places.size() > 0) {
|
||||
LatLng[] boundaryCoordinates = {places.get(0).location, // south
|
||||
|
|
@ -93,6 +99,11 @@ public class NearbyController {
|
|||
}
|
||||
nearbyPlacesInfo.placeList = places;
|
||||
nearbyPlacesInfo.boundaryCoordinates = boundaryCoordinates;
|
||||
if (!returnClosestResult && checkingAroundCurrentLocation) {
|
||||
// Do not update searched radius, if controller is used for nearby card notification
|
||||
searchedRadius = nearbyPlaces.radius;
|
||||
currentLocation = curLatLng;
|
||||
}
|
||||
return nearbyPlacesInfo;
|
||||
}
|
||||
else {
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import android.support.design.widget.BottomSheetBehavior;
|
|||
import android.support.design.widget.Snackbar;
|
||||
import android.support.v4.app.FragmentTransaction;
|
||||
import android.support.v7.app.AlertDialog;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
|
@ -64,8 +65,6 @@ public class NearbyFragment extends CommonsDaggerSupportFragment
|
|||
LinearLayout bottomSheetDetails;
|
||||
@BindView(R.id.transparentView)
|
||||
View transparentView;
|
||||
@BindView(R.id.fab_recenter)
|
||||
View fabRecenter;
|
||||
|
||||
@Inject
|
||||
LocationServiceManager locationManager;
|
||||
|
|
@ -87,16 +86,19 @@ public class NearbyFragment extends CommonsDaggerSupportFragment
|
|||
|
||||
private LatLng curLatLng;
|
||||
private Disposable placesDisposable;
|
||||
private Disposable placesDisposableCustom;
|
||||
private boolean lockNearbyView; //Determines if the nearby places needs to be refreshed
|
||||
public View view;
|
||||
private Snackbar snackbar;
|
||||
|
||||
private LatLng lastKnownLocation;
|
||||
private LatLng customLatLng;
|
||||
|
||||
private final String NETWORK_INTENT_ACTION = "android.net.conn.CONNECTIVITY_CHANGE";
|
||||
private BroadcastReceiver broadcastReceiver;
|
||||
|
||||
private boolean onOrientationChanged = false;
|
||||
private boolean populateForCurrentLocation = false;
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
|
|
@ -215,24 +217,27 @@ public class NearbyFragment extends CommonsDaggerSupportFragment
|
|||
|
||||
@Override
|
||||
public void onLocationChangedSignificantly(LatLng latLng) {
|
||||
refreshView(LOCATION_SIGNIFICANTLY_CHANGED);
|
||||
refreshView(LOCATION_SIGNIFICANTLY_CHANGED);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLocationChangedSlightly(LatLng latLng) {
|
||||
refreshView(LOCATION_SLIGHTLY_CHANGED);
|
||||
refreshView(LOCATION_SLIGHTLY_CHANGED);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onLocationChangedMedium(LatLng latLng) {
|
||||
// For nearby map actions, there are no differences between 500 meter location change (aka medium change) and slight change
|
||||
refreshView(LOCATION_SLIGHTLY_CHANGED);
|
||||
refreshView(LOCATION_SLIGHTLY_CHANGED);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWikidataEditSuccessful() {
|
||||
refreshView(MAP_UPDATED);
|
||||
// Do not refresh nearby map if we are checking other areas with search this area button
|
||||
if (!nearbyMapFragment.searchThisAreaModeOn) {
|
||||
refreshView(MAP_UPDATED);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -240,7 +245,7 @@ public class NearbyFragment extends CommonsDaggerSupportFragment
|
|||
*
|
||||
* @param locationChangeType defines if location shanged significantly or slightly
|
||||
*/
|
||||
private void refreshView(LocationServiceManager.LocationChangeType locationChangeType) {
|
||||
public void refreshView(LocationServiceManager.LocationChangeType locationChangeType) {
|
||||
Timber.d("Refreshing nearby places");
|
||||
if (lockNearbyView) {
|
||||
return;
|
||||
|
|
@ -256,9 +261,11 @@ public class NearbyFragment extends CommonsDaggerSupportFragment
|
|||
|
||||
if (curLatLng != null && curLatLng.equals(lastLocation)
|
||||
&& !locationChangeType.equals(MAP_UPDATED)) { //refresh view only if location has changed
|
||||
// Two exceptional cases to refresh nearby map manually.
|
||||
if (!onOrientationChanged) {
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
curLatLng = lastLocation;
|
||||
|
||||
|
|
@ -291,7 +298,7 @@ public class NearbyFragment extends CommonsDaggerSupportFragment
|
|||
bundle.putString("CurLatLng", gsonCurLatLng);
|
||||
|
||||
placesDisposable = Observable.fromCallable(() -> nearbyController
|
||||
.loadAttractionsFromLocation(curLatLng, false))
|
||||
.loadAttractionsFromLocation(curLatLng, curLatLng, false, true))
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(this::populatePlaces,
|
||||
|
|
@ -300,6 +307,7 @@ public class NearbyFragment extends CommonsDaggerSupportFragment
|
|||
showErrorMessage(getString(R.string.error_fetching_nearby_places));
|
||||
progressBar.setVisibility(View.GONE);
|
||||
});
|
||||
|
||||
} else if (locationChangeType
|
||||
.equals(LOCATION_SLIGHTLY_CHANGED)) {
|
||||
Gson gson = new GsonBuilder()
|
||||
|
|
@ -307,7 +315,62 @@ public class NearbyFragment extends CommonsDaggerSupportFragment
|
|||
.create();
|
||||
String gsonCurLatLng = gson.toJson(curLatLng);
|
||||
bundle.putString("CurLatLng", gsonCurLatLng);
|
||||
updateMapFragment(true);
|
||||
updateMapFragment(false,true, null, null);
|
||||
}
|
||||
|
||||
if (nearbyMapFragment != null) {
|
||||
nearbyMapFragment.searchThisAreaButton.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method should be used with "Search this are button". This method will search nearby
|
||||
* points around any custom location (target location when user clicked on search this area)
|
||||
* button. It populates places for custom location.
|
||||
* @param customLatLng Custom area which we will search around
|
||||
*/
|
||||
public void refreshViewForCustomLocation(LatLng customLatLng, boolean refreshForCurrentLocation) {
|
||||
|
||||
if (customLatLng == null) {
|
||||
// If null, return
|
||||
return;
|
||||
}
|
||||
|
||||
populateForCurrentLocation = refreshForCurrentLocation;
|
||||
this.customLatLng = customLatLng;
|
||||
placesDisposableCustom = Observable.fromCallable(() -> nearbyController
|
||||
.loadAttractionsFromLocation(curLatLng, customLatLng, false, populateForCurrentLocation))
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(this::populatePlacesFromCustomLocation,
|
||||
throwable -> {
|
||||
Timber.d(throwable);
|
||||
showErrorMessage(getString(R.string.error_fetching_nearby_places));
|
||||
});
|
||||
|
||||
if (nearbyMapFragment != null) {
|
||||
nearbyMapFragment.searchThisAreaButton.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Populates places for custom location, should be used for finding nearby places around a
|
||||
* location where you are not at.
|
||||
* @param nearbyPlacesInfo This variable has place list information and distances.
|
||||
*/
|
||||
private void populatePlacesFromCustomLocation(NearbyController.NearbyPlacesInfo nearbyPlacesInfo) {
|
||||
//NearbyMapFragment nearbyMapFragment = getMapFragment();
|
||||
if (nearbyMapFragment != null) {
|
||||
nearbyMapFragment.searchThisAreaButtonProgressBar.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
if (nearbyMapFragment != null && curLatLng != null) {
|
||||
if (!populateForCurrentLocation) {
|
||||
nearbyMapFragment.updateMapSignificantlyForCustomLocation(customLatLng, nearbyPlacesInfo.placeList);
|
||||
} else {
|
||||
updateMapFragment(true,true, customLatLng, nearbyPlacesInfo);
|
||||
}
|
||||
updateListFragmentForCustomLocation(nearbyPlacesInfo.placeList);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -341,7 +404,7 @@ public class NearbyFragment extends CommonsDaggerSupportFragment
|
|||
} else {
|
||||
// There are fragments, just update the map and list
|
||||
Timber.d("Map fragment already exists, just update the map and list");
|
||||
updateMapFragment(false);
|
||||
updateMapFragment(false,false, null, null);
|
||||
updateListFragment();
|
||||
}
|
||||
}
|
||||
|
|
@ -363,7 +426,11 @@ public class NearbyFragment extends CommonsDaggerSupportFragment
|
|||
}
|
||||
}
|
||||
|
||||
private void updateMapFragment(boolean isSlightUpdate) {
|
||||
private void updateMapFragment(boolean updateViaButton, boolean isSlightUpdate, @Nullable LatLng customLatLng, @Nullable NearbyController.NearbyPlacesInfo nearbyPlacesInfo) {
|
||||
|
||||
if (nearbyMapFragment.searchThisAreaModeOn) {
|
||||
return;
|
||||
}
|
||||
/*
|
||||
Significant update means updating nearby place markers. Slightly update means only
|
||||
updating current location marker and camera target.
|
||||
|
|
@ -379,14 +446,14 @@ public class NearbyFragment extends CommonsDaggerSupportFragment
|
|||
* If we are close to nearby places boundaries, we need a significant update to
|
||||
* get new nearby places. Check order is south, north, west, east
|
||||
* */
|
||||
if (nearbyMapFragment.boundaryCoordinates != null
|
||||
if (nearbyMapFragment.boundaryCoordinates != null && !nearbyMapFragment.searchThisAreaModeOn
|
||||
&& (curLatLng.getLatitude() <= nearbyMapFragment.boundaryCoordinates[0].getLatitude()
|
||||
|| curLatLng.getLatitude() >= nearbyMapFragment.boundaryCoordinates[1].getLatitude()
|
||||
|| curLatLng.getLongitude() <= nearbyMapFragment.boundaryCoordinates[2].getLongitude()
|
||||
|| curLatLng.getLongitude() >= nearbyMapFragment.boundaryCoordinates[3].getLongitude())) {
|
||||
// populate places
|
||||
placesDisposable = Observable.fromCallable(() -> nearbyController
|
||||
.loadAttractionsFromLocation(curLatLng, false))
|
||||
.loadAttractionsFromLocation(curLatLng, curLatLng, false, updateViaButton))
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(this::populatePlaces,
|
||||
|
|
@ -396,11 +463,16 @@ public class NearbyFragment extends CommonsDaggerSupportFragment
|
|||
progressBar.setVisibility(View.GONE);
|
||||
});
|
||||
nearbyMapFragment.setBundleForUpdtes(bundle);
|
||||
nearbyMapFragment.updateMapSignificantly();
|
||||
nearbyMapFragment.updateMapSignificantlyForCurrentLocation();
|
||||
updateListFragment();
|
||||
return;
|
||||
}
|
||||
|
||||
if (updateViaButton) {
|
||||
nearbyMapFragment.updateMapSignificantlyForCustomLocation(customLatLng, nearbyPlacesInfo.placeList);
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
If this is the map update just after orientation change, then it is not a slight update
|
||||
anymore. We want to significantly update map after each orientation change
|
||||
|
|
@ -415,7 +487,7 @@ public class NearbyFragment extends CommonsDaggerSupportFragment
|
|||
nearbyMapFragment.updateMapSlightly();
|
||||
} else {
|
||||
nearbyMapFragment.setBundleForUpdtes(bundle);
|
||||
nearbyMapFragment.updateMapSignificantly();
|
||||
nearbyMapFragment.updateMapSignificantlyForCurrentLocation();
|
||||
updateListFragment();
|
||||
}
|
||||
} else {
|
||||
|
|
@ -432,6 +504,15 @@ public class NearbyFragment extends CommonsDaggerSupportFragment
|
|||
nearbyListFragment.updateNearbyListSignificantly();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates nearby list for custom location, will be used with search this area method. When you
|
||||
* want to search for a place where you are not at.
|
||||
* @param placeList List of places around your manually chosen target location from map.
|
||||
*/
|
||||
private void updateListFragmentForCustomLocation(List<Place> placeList) {
|
||||
nearbyListFragment.updateNearbyListSignificantlyForCustomLocation(placeList);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls fragment for map view.
|
||||
*/
|
||||
|
|
@ -658,6 +739,9 @@ public class NearbyFragment extends CommonsDaggerSupportFragment
|
|||
placesDisposable.dispose();
|
||||
}
|
||||
wikidataEditListener.setAuthenticationStateListener(null);
|
||||
if (placesDisposableCustom != null) {
|
||||
placesDisposableCustom.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -35,6 +35,8 @@ import timber.log.Timber;
|
|||
|
||||
import static android.app.Activity.RESULT_OK;
|
||||
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
|
||||
import static fr.free.nrw.commons.wikidata.WikidataConstants.WIKIDATA_ENTITY_ID_PREF;
|
||||
import static fr.free.nrw.commons.wikidata.WikidataConstants.WIKIDATA_ITEM_LOCATION;
|
||||
|
||||
public class NearbyListFragment extends DaggerFragment {
|
||||
private Bundle bundleForUpdates; // Carry information from activity about changed nearby places and current location
|
||||
|
|
@ -98,6 +100,19 @@ public class NearbyListFragment extends DaggerFragment {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* While nearby updates for current location held with bundle, automatically, custom updates are
|
||||
* done by calling this methods, triddered by search this are button input from user.
|
||||
* @param placeList
|
||||
*/
|
||||
public void updateNearbyListSignificantlyForCustomLocation(List<Place> placeList) {
|
||||
try {
|
||||
adapterFactory.updateAdapterData(placeList, (RVRendererAdapter<Place>) recyclerView.getAdapter());
|
||||
} catch (NullPointerException e) {
|
||||
Timber.e("Null pointer exception from calling recyclerView.getAdapter()");
|
||||
}
|
||||
}
|
||||
|
||||
private List<Place> getPlaceListFromBundle(Bundle bundle) {
|
||||
List<Place> placeList = Collections.emptyList();
|
||||
|
||||
|
|
@ -146,13 +161,14 @@ public class NearbyListFragment extends DaggerFragment {
|
|||
if (resultCode == RESULT_OK) {
|
||||
Timber.d("OnActivityResult() parameters: Req code: %d Result code: %d Data: %s",
|
||||
requestCode, resultCode, data);
|
||||
String wikidataEntityId = directPrefs.getString("WikiDataEntityId", null);
|
||||
String wikidataEntityId = directPrefs.getString(WIKIDATA_ENTITY_ID_PREF, null);
|
||||
String wikidataItemLocation = directPrefs.getString(WIKIDATA_ITEM_LOCATION, null);
|
||||
if (requestCode == ContributionController.SELECT_FROM_CAMERA) {
|
||||
// If coming from camera, pass null as uri. Because camera photos get saved to a
|
||||
// fixed directory
|
||||
controller.handleImagePicked(requestCode, null, true, wikidataEntityId);
|
||||
controller.handleImagePicked(requestCode, null, true, wikidataEntityId, wikidataItemLocation);
|
||||
} else {
|
||||
controller.handleImagePicked(requestCode, data.getData(), true, wikidataEntityId);
|
||||
controller.handleImagePicked(requestCode, data.getData(), true, wikidataEntityId, wikidataItemLocation);
|
||||
}
|
||||
} else {
|
||||
Timber.e("OnActivityResult() parameters: Req code: %d Result code: %d Data: %s",
|
||||
|
|
|
|||
|
|
@ -23,8 +23,10 @@ import android.view.View;
|
|||
import android.view.ViewGroup;
|
||||
import android.view.animation.Animation;
|
||||
import android.view.animation.AnimationUtils;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
|
|
@ -60,14 +62,17 @@ import fr.free.nrw.commons.Utils;
|
|||
import fr.free.nrw.commons.auth.LoginActivity;
|
||||
import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao;
|
||||
import fr.free.nrw.commons.contributions.ContributionController;
|
||||
import fr.free.nrw.commons.location.LocationServiceManager;
|
||||
import fr.free.nrw.commons.utils.LocationUtils;
|
||||
import fr.free.nrw.commons.utils.PlaceUtils;
|
||||
import fr.free.nrw.commons.utils.UriDeserializer;
|
||||
import fr.free.nrw.commons.utils.ViewUtil;
|
||||
import timber.log.Timber;
|
||||
import uk.co.deanwild.materialshowcaseview.MaterialShowcaseView;
|
||||
|
||||
import static android.app.Activity.RESULT_OK;
|
||||
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
|
||||
import static fr.free.nrw.commons.wikidata.WikidataConstants.WIKIDATA_ENTITY_ID_PREF;
|
||||
import static fr.free.nrw.commons.wikidata.WikidataConstants.WIKIDATA_ITEM_LOCATION;
|
||||
|
||||
public class NearbyMapFragment extends DaggerFragment {
|
||||
|
||||
|
|
@ -115,16 +120,21 @@ public class NearbyMapFragment extends DaggerFragment {
|
|||
private Place place;
|
||||
private Marker selected;
|
||||
private Marker currentLocationMarker;
|
||||
private MapboxMap mapboxMap;
|
||||
public MapboxMap mapboxMap;
|
||||
private PolygonOptions currentLocationPolygonOptions;
|
||||
|
||||
public Button searchThisAreaButton;
|
||||
public ProgressBar searchThisAreaButtonProgressBar;
|
||||
|
||||
private boolean isBottomListSheetExpanded;
|
||||
private final double CAMERA_TARGET_SHIFT_FACTOR_PORTRAIT = 0.06;
|
||||
private final double CAMERA_TARGET_SHIFT_FACTOR_LANDSCAPE = 0.04;
|
||||
|
||||
private boolean isMapReady;
|
||||
public boolean searchThisAreaModeOn = false;
|
||||
|
||||
private Bundle bundleForUpdtes;// Carry information from activity about changed nearby places and current location
|
||||
private boolean searchedAroundCurrentLocation = true;
|
||||
|
||||
@Inject
|
||||
@Named("prefs")
|
||||
|
|
@ -246,8 +256,8 @@ public class NearbyMapFragment extends DaggerFragment {
|
|||
* called when user is out of boundaries (south, north, east or west) of markers drawn by
|
||||
* previous nearby call.
|
||||
*/
|
||||
public void updateMapSignificantly() {
|
||||
Timber.d("updateMapSignificantly called, bundle is:"+bundleForUpdtes);
|
||||
public void updateMapSignificantlyForCurrentLocation() {
|
||||
Timber.d("updateMapSignificantlyForCurrentLocation called, bundle is:"+bundleForUpdtes);
|
||||
if (mapboxMap != null) {
|
||||
if (bundleForUpdtes != null) {
|
||||
Gson gson = new GsonBuilder()
|
||||
|
|
@ -271,10 +281,32 @@ public class NearbyMapFragment extends DaggerFragment {
|
|||
mapboxMap.clear();
|
||||
addCurrentLocationMarker(mapboxMap);
|
||||
updateMapToTrackPosition();
|
||||
addNearbyMarkerstoMapBoxMap();
|
||||
// We are trying to find nearby places around our current location, thus custom parameter is nullified
|
||||
addNearbyMarkerstoMapBoxMap(null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Will be used for map vew updates for custom locations (ie. with search this area method).
|
||||
* Clears the map, adds current location marker, adds nearby markers around custom location,
|
||||
* re-enables map gestures which was locked during place load, remove progress bar.
|
||||
* @param customLatLng custom location that we will search around
|
||||
* @param placeList places around of custom location
|
||||
*/
|
||||
public void updateMapSignificantlyForCustomLocation(fr.free.nrw.commons.location.LatLng customLatLng, List<Place> placeList) {
|
||||
List<NearbyBaseMarker> customBaseMarkerOptions = NearbyController
|
||||
.loadAttractionsFromLocationToBaseMarkerOptions(curLatLng, // Curlatlang will be used to calculate distances
|
||||
placeList,
|
||||
getActivity());
|
||||
mapboxMap.clear();
|
||||
// We are trying to find nearby places around our custom searched area, thus custom parameter is nonnull
|
||||
addNearbyMarkerstoMapBoxMap(customBaseMarkerOptions);
|
||||
addCurrentLocationMarker(mapboxMap);
|
||||
// Re-enable mapbox gestures on custom location markers load
|
||||
mapboxMap.getUiSettings().setAllGesturesEnabled(true);
|
||||
searchThisAreaButtonProgressBar.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
// Only update current position marker and camera view
|
||||
private void updateMapToTrackPosition() {
|
||||
|
||||
|
|
@ -299,6 +331,9 @@ public class NearbyMapFragment extends DaggerFragment {
|
|||
|
||||
// Make camera to follow user on location change
|
||||
CameraPosition position ;
|
||||
|
||||
// Do not update camera position is search this area mode on
|
||||
if (!searchThisAreaModeOn) {
|
||||
if (ViewUtil.isPortrait(getActivity())){
|
||||
position = new CameraPosition.Builder()
|
||||
.target(isBottomListSheetExpanded ?
|
||||
|
|
@ -323,7 +358,7 @@ public class NearbyMapFragment extends DaggerFragment {
|
|||
|
||||
mapboxMap.animateCamera(CameraUpdateFactory
|
||||
.newCameraPosition(position), 1000);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -366,6 +401,9 @@ public class NearbyMapFragment extends DaggerFragment {
|
|||
bookmarkButton = getActivity().findViewById(R.id.bookmarkButton);
|
||||
bookmarkButtonImage = getActivity().findViewById(R.id.bookmarkButtonImage);
|
||||
|
||||
searchThisAreaButton = ((NearbyFragment)getParentFragment()).view.findViewById(R.id.search_this_area_button);
|
||||
searchThisAreaButtonProgressBar = ((NearbyFragment)getParentFragment()).view.findViewById(R.id.search_this_area_button_progres_bar);
|
||||
|
||||
}
|
||||
|
||||
private void setListeners() {
|
||||
|
|
@ -495,13 +533,77 @@ public class NearbyMapFragment extends DaggerFragment {
|
|||
@Override
|
||||
public void onMapReady(MapboxMap mapboxMap) {
|
||||
NearbyMapFragment.this.mapboxMap = mapboxMap;
|
||||
updateMapSignificantly();
|
||||
addMapMovementListeners();
|
||||
updateMapSignificantlyForCurrentLocation();
|
||||
}
|
||||
});
|
||||
mapView.setStyleUrl("asset://mapstyle.json");
|
||||
}
|
||||
}
|
||||
|
||||
private void addMapMovementListeners() {
|
||||
|
||||
mapboxMap.addOnCameraMoveListener(new MapboxMap.OnCameraMoveListener() {
|
||||
|
||||
@Override
|
||||
public void onCameraMove() {
|
||||
|
||||
if (NearbyController.currentLocation != null) { // If our nearby markers are calculated at least once
|
||||
|
||||
if (searchThisAreaButton.getVisibility() == View.GONE) {
|
||||
searchThisAreaButton.setVisibility(View.VISIBLE);
|
||||
}
|
||||
double distance = mapboxMap.getCameraPosition().target
|
||||
.distanceTo(new LatLng(NearbyController.currentLocation.getLatitude()
|
||||
, NearbyController.currentLocation.getLongitude()));
|
||||
|
||||
if (distance > NearbyController.searchedRadius*1000*3/4) { //Convert to meter, and compare if our distance is bigger than 3/4 or our searched area
|
||||
if (!searchThisAreaModeOn) { // If we are changing mode, then change click action
|
||||
searchThisAreaModeOn = true;
|
||||
searchThisAreaButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
searchThisAreaModeOn = true;
|
||||
// Lock map operations during search this area operation
|
||||
mapboxMap.getUiSettings().setAllGesturesEnabled(false);
|
||||
searchThisAreaButtonProgressBar.setVisibility(View.VISIBLE);
|
||||
searchThisAreaButton.setVisibility(View.GONE);
|
||||
searchedAroundCurrentLocation = false;
|
||||
((NearbyFragment)getParentFragment())
|
||||
.refreshViewForCustomLocation(LocationUtils
|
||||
.mapBoxLatLngToCommonsLatLng(mapboxMap.getCameraPosition().target), false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
} else {
|
||||
if (searchThisAreaModeOn) {
|
||||
searchThisAreaModeOn = false; // This flag will help us to understand should we folor users location or not
|
||||
searchThisAreaButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
searchThisAreaModeOn = true;
|
||||
// Lock map operations during search this area operation
|
||||
mapboxMap.getUiSettings().setAllGesturesEnabled(false);
|
||||
searchThisAreaButtonProgressBar.setVisibility(View.VISIBLE);
|
||||
fabRecenter.callOnClick();
|
||||
searchThisAreaButton.setVisibility(View.GONE);
|
||||
searchedAroundCurrentLocation = true;
|
||||
((NearbyFragment)getParentFragment())
|
||||
.refreshViewForCustomLocation(LocationUtils
|
||||
.mapBoxLatLngToCommonsLatLng(mapboxMap.getCameraPosition().target), true);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (searchedAroundCurrentLocation) {
|
||||
searchThisAreaButton.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* onLogoutComplete is called after shared preferences and data stored in local database are cleared.
|
||||
*/
|
||||
|
|
@ -554,10 +656,17 @@ public class NearbyMapFragment extends DaggerFragment {
|
|||
/**
|
||||
* Adds markers for nearby places to mapbox map
|
||||
*/
|
||||
private void addNearbyMarkerstoMapBoxMap() {
|
||||
private void addNearbyMarkerstoMapBoxMap(@Nullable List<NearbyBaseMarker> customNearbyBaseMarker) {
|
||||
List<NearbyBaseMarker> baseMarkerOptions;
|
||||
Timber.d("addNearbyMarkerstoMapBoxMap is called");
|
||||
if (customNearbyBaseMarker != null) {
|
||||
// If we try to update nearby points for a custom location choosen from map (we are not there)
|
||||
baseMarkerOptions = customNearbyBaseMarker;
|
||||
} else {
|
||||
// If we try to display nearby markers around our curret location
|
||||
baseMarkerOptions = this.baseMarkerOptions;
|
||||
}
|
||||
mapboxMap.addMarkers(baseMarkerOptions);
|
||||
|
||||
mapboxMap.setOnInfoWindowCloseListener(marker -> {
|
||||
if (marker == selected) {
|
||||
bottomSheetDetailsBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
|
||||
|
|
@ -781,6 +890,7 @@ public class NearbyMapFragment extends DaggerFragment {
|
|||
editor.putString("Desc", place.getLongDescription());
|
||||
editor.putString("Category", place.getCategory());
|
||||
editor.putString(WIKIDATA_ENTITY_ID_PREF, place.getWikiDataEntityId());
|
||||
editor.putString(WIKIDATA_ITEM_LOCATION, PlaceUtils.latLangToString(place.location));
|
||||
editor.apply();
|
||||
}
|
||||
|
||||
|
|
@ -817,13 +927,14 @@ public class NearbyMapFragment extends DaggerFragment {
|
|||
if (resultCode == RESULT_OK) {
|
||||
Timber.d("OnActivityResult() parameters: Req code: %d Result code: %d Data: %s",
|
||||
requestCode, resultCode, data);
|
||||
String wikidataEntityId = directPrefs.getString("WikiDataEntityId", null);
|
||||
String wikidataEntityId = directPrefs.getString(WIKIDATA_ENTITY_ID_PREF, null);
|
||||
String wikidataItemLocation = directPrefs.getString(WIKIDATA_ITEM_LOCATION, null);
|
||||
if (requestCode == ContributionController.SELECT_FROM_CAMERA) {
|
||||
// If coming from camera, pass null as uri. Because camera photos get saved to a
|
||||
// fixed directory
|
||||
controller.handleImagePicked(requestCode, null, true, wikidataEntityId);
|
||||
controller.handleImagePicked(requestCode, null, true, wikidataEntityId, wikidataItemLocation);
|
||||
} else {
|
||||
controller.handleImagePicked(requestCode, data.getData(), true, wikidataEntityId);
|
||||
controller.handleImagePicked(requestCode, data.getData(), true, wikidataEntityId, wikidataItemLocation);
|
||||
}
|
||||
} else {
|
||||
Timber.e("OnActivityResult() parameters: Req code: %d Result code: %d Data: %s",
|
||||
|
|
|
|||
|
|
@ -6,12 +6,8 @@ import android.content.res.Resources;
|
|||
import android.os.Handler;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.design.widget.CoordinatorLayout;
|
||||
import android.support.design.widget.SwipeDismissBehavior;
|
||||
import android.support.v7.app.AlertDialog;
|
||||
import android.support.v7.widget.CardView;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Log;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
|
|
@ -20,19 +16,17 @@ import android.widget.ProgressBar;
|
|||
import android.widget.RelativeLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import android.widget.Toast;
|
||||
|
||||
import fr.free.nrw.commons.R;
|
||||
import fr.free.nrw.commons.contributions.MainActivity;
|
||||
import fr.free.nrw.commons.utils.SwipableCardView;
|
||||
import fr.free.nrw.commons.utils.ViewUtil;
|
||||
import timber.log.Timber;
|
||||
|
||||
/**
|
||||
* Custom card view for nearby notification card view on main screen, above contributions list
|
||||
*/
|
||||
public class NearbyNoificationCardView extends CardView{
|
||||
public class NearbyNoificationCardView extends SwipableCardView {
|
||||
|
||||
private static final float MINIMUM_THRESHOLD_FOR_SWIPE = 100;
|
||||
private Context context;
|
||||
|
||||
private Button permissionRequestButton;
|
||||
|
|
@ -99,41 +93,15 @@ public class NearbyNoificationCardView extends CardView{
|
|||
|
||||
private void setActionListeners() {
|
||||
this.setOnClickListener(view -> ((MainActivity)context).viewPager.setCurrentItem(1));
|
||||
|
||||
this.setOnTouchListener(
|
||||
(v, event) -> {
|
||||
boolean isSwipe = false;
|
||||
float deltaX=0.0f;
|
||||
switch (event.getAction()) {
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
x1 = event.getX();
|
||||
break;
|
||||
case MotionEvent.ACTION_UP:
|
||||
x2 = event.getX();
|
||||
deltaX = x2 - x1;
|
||||
if (deltaX < 0) {
|
||||
//Right to left swipe
|
||||
isSwipe = true;
|
||||
} else if (deltaX > 0) {
|
||||
//Left to right swipe
|
||||
isSwipe = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (isSwipe && (pixelToDp(Math.abs(deltaX)) > MINIMUM_THRESHOLD_FOR_SWIPE)) {
|
||||
v.setVisibility(GONE);
|
||||
// Save shared preference for nearby card view accordingly
|
||||
((MainActivity) context).prefs.edit()
|
||||
.putBoolean("displayNearbyCardView", false).apply();
|
||||
ViewUtil.showLongToast(context, getResources().getString(R.string.nearby_notification_dismiss_message));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
private float pixelToDp(float pixels) {
|
||||
return (pixels / Resources.getSystem().getDisplayMetrics().density);
|
||||
@Override public boolean onSwipe(View view) {
|
||||
view.setVisibility(GONE);
|
||||
// Save shared preference for nearby card view accordingly
|
||||
((MainActivity) context).prefs.edit().putBoolean("displayNearbyCardView", false).apply();
|
||||
ViewUtil.showLongToast(context,
|
||||
getResources().getString(R.string.nearby_notification_dismiss_message));
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ public class NearbyPlaces {
|
|||
private static final Uri WIKIDATA_QUERY_URL = Uri.parse("https://query.wikidata.org/sparql");
|
||||
private static final Uri WIKIDATA_QUERY_UI_URL = Uri.parse("https://query.wikidata.org/");
|
||||
private final String wikidataQuery;
|
||||
private double radius = INITIAL_RADIUS;
|
||||
public double radius = INITIAL_RADIUS;
|
||||
|
||||
public NearbyPlaces() {
|
||||
try {
|
||||
|
|
@ -55,6 +55,7 @@ public class NearbyPlaces {
|
|||
} else {
|
||||
MIN_RESULTS = 40;
|
||||
MAX_RADIUS = 300.0; // in kilometers
|
||||
radius = INITIAL_RADIUS;
|
||||
}
|
||||
|
||||
// increase the radius gradually to find a satisfactory number of nearby places
|
||||
|
|
|
|||
|
|
@ -30,10 +30,12 @@ import fr.free.nrw.commons.auth.LoginActivity;
|
|||
import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao;
|
||||
import fr.free.nrw.commons.contributions.ContributionController;
|
||||
import fr.free.nrw.commons.di.ApplicationlessInjection;
|
||||
import fr.free.nrw.commons.utils.PlaceUtils;
|
||||
import timber.log.Timber;
|
||||
|
||||
import static fr.free.nrw.commons.theme.NavigationBaseActivity.startActivityWithFlags;
|
||||
import static fr.free.nrw.commons.wikidata.WikidataConstants.WIKIDATA_ENTITY_ID_PREF;
|
||||
import static fr.free.nrw.commons.wikidata.WikidataConstants.WIKIDATA_ITEM_LOCATION;
|
||||
|
||||
public class PlaceRenderer extends Renderer<Place> {
|
||||
|
||||
|
|
@ -193,6 +195,7 @@ public class PlaceRenderer extends Renderer<Place> {
|
|||
editor.putString("Desc", place.getLongDescription());
|
||||
editor.putString("Category", place.getCategory());
|
||||
editor.putString(WIKIDATA_ENTITY_ID_PREF, place.getWikiDataEntityId());
|
||||
editor.putString(WIKIDATA_ITEM_LOCATION, PlaceUtils.latLangToString(place.location));
|
||||
editor.apply();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,18 +1,12 @@
|
|||
package fr.free.nrw.commons.notification;
|
||||
|
||||
import android.graphics.Color;
|
||||
import android.text.Html;
|
||||
import android.text.SpannableString;
|
||||
import android.text.Spanned;
|
||||
import android.text.TextPaint;
|
||||
import android.text.style.ClickableSpan;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.borjabravo.readmoretextview.ReadMoreTextView;
|
||||
import com.pedrogomez.renderers.Renderer;
|
||||
|
||||
import butterknife.BindView;
|
||||
|
|
@ -24,7 +18,7 @@ import fr.free.nrw.commons.R;
|
|||
*/
|
||||
|
||||
public class NotificationRenderer extends Renderer<Notification> {
|
||||
@BindView(R.id.title) ReadMoreTextView title;
|
||||
@BindView(R.id.title) TextView title;
|
||||
@BindView(R.id.time) TextView time;
|
||||
@BindView(R.id.icon) ImageView icon;
|
||||
private NotificationClicked listener;
|
||||
|
|
@ -64,26 +58,12 @@ public class NotificationRenderer extends Renderer<Notification> {
|
|||
private void setTitle(String notificationText) {
|
||||
notificationText = notificationText.trim().replaceAll("(^\\s*)|(\\s*$)", "");
|
||||
notificationText = Html.fromHtml(notificationText).toString();
|
||||
if(notificationText.length()>280){
|
||||
notificationText = notificationText.substring(0,279);
|
||||
notificationText = notificationText.concat("...");
|
||||
}
|
||||
notificationText = notificationText.concat(" ");
|
||||
|
||||
SpannableString ss = new SpannableString(notificationText);
|
||||
ClickableSpan clickableSpan = new ClickableSpan() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
listener.notificationClicked(getContent());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateDrawState(TextPaint ds) {
|
||||
super.updateDrawState(ds);
|
||||
ds.setUnderlineText(false);
|
||||
ds.setColor(Color.BLACK);
|
||||
}
|
||||
};
|
||||
|
||||
// Attach a ClickableSpan to the range (start:0, end:notificationText.length()) of the String
|
||||
ss.setSpan(clickableSpan, 0, notificationText.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
title.setText(ss, TextView.BufferType.SPANNABLE);
|
||||
title.setText(notificationText);
|
||||
}
|
||||
|
||||
public interface NotificationClicked{
|
||||
|
|
|
|||
|
|
@ -174,7 +174,10 @@ class DescriptionsAdapter extends RecyclerView.Adapter<DescriptionsAdapter.ViewH
|
|||
descItemEditText.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null);
|
||||
}
|
||||
|
||||
descItemEditText.addTextChangedListener(new AbstractTextWatcher(description::setDescriptionText));
|
||||
descItemEditText.addTextChangedListener(new AbstractTextWatcher(descriptionText->{
|
||||
descriptions.get(position - 1).setDescriptionText(descriptionText);
|
||||
}));
|
||||
|
||||
descItemEditText.setOnFocusChangeListener((v, hasFocus) -> {
|
||||
if (!hasFocus) {
|
||||
ViewUtil.hideKeyboard(v);
|
||||
|
|
|
|||
|
|
@ -2,37 +2,31 @@ package fr.free.nrw.commons.upload;
|
|||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.media.ExifInterface;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
import javax.inject.Singleton;
|
||||
|
||||
import fr.free.nrw.commons.caching.CacheController;
|
||||
import fr.free.nrw.commons.di.ApplicationlessInjection;
|
||||
import fr.free.nrw.commons.mwapi.CategoryApi;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import timber.log.Timber;
|
||||
|
||||
import static com.mapbox.mapboxsdk.Mapbox.getApplicationContext;
|
||||
|
||||
/**
|
||||
* Processing of the image file that is about to be uploaded via ShareActivity is done here
|
||||
*/
|
||||
@Singleton
|
||||
public class FileProcessor implements SimilarImageDialogFragment.onResponse {
|
||||
|
||||
@Inject
|
||||
|
|
@ -47,24 +41,23 @@ public class FileProcessor implements SimilarImageDialogFragment.onResponse {
|
|||
private String filePath;
|
||||
private ContentResolver contentResolver;
|
||||
private GPSExtractor imageObj;
|
||||
private Context context;
|
||||
private String decimalCoords;
|
||||
private ExifInterface exifInterface;
|
||||
private boolean useExtStorage;
|
||||
private boolean haveCheckedForOtherImages = false;
|
||||
private GPSExtractor tempImageObj;
|
||||
|
||||
FileProcessor(@NonNull String filePath, ContentResolver contentResolver, Context context) {
|
||||
@Inject
|
||||
FileProcessor() {
|
||||
}
|
||||
|
||||
void initFileDetails(@NonNull String filePath, ContentResolver contentResolver) {
|
||||
this.filePath = filePath;
|
||||
this.contentResolver = contentResolver;
|
||||
this.context = context;
|
||||
ApplicationlessInjection.getInstance(context.getApplicationContext()).getCommonsApplicationComponent().inject(this);
|
||||
try {
|
||||
exifInterface=new ExifInterface(filePath);
|
||||
exifInterface = new ExifInterface(filePath);
|
||||
} catch (IOException e) {
|
||||
Timber.e(e);
|
||||
}
|
||||
useExtStorage = prefs.getBoolean("useExternalStorage", true);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -85,10 +78,6 @@ public class FileProcessor implements SimilarImageDialogFragment.onResponse {
|
|||
return imageObj;
|
||||
}
|
||||
|
||||
String getDecimalCoords() {
|
||||
return decimalCoords;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find other images around the same location that were taken within the last 20 sec
|
||||
* @param similarImageInterface
|
||||
|
|
@ -142,7 +131,7 @@ public class FileProcessor implements SimilarImageDialogFragment.onResponse {
|
|||
* Then initiates the calls to MediaWiki API through an instance of CategoryApi.
|
||||
*/
|
||||
@SuppressLint("CheckResult")
|
||||
public void useImageCoords() {
|
||||
private void useImageCoords() {
|
||||
if (decimalCoords != null) {
|
||||
Timber.d("Decimal coords of image: %s", decimalCoords);
|
||||
Timber.d("is EXIF data present:" + imageObj.imageCoordsExists + " from findOther image");
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import android.content.ContentUris;
|
|||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.database.Cursor;
|
||||
import android.media.ExifInterface;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Environment;
|
||||
|
|
@ -78,6 +79,25 @@ public class FileUtils {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Geolocation of file from input file path
|
||||
*/
|
||||
static String getGeolocationOfFile(String filePath) {
|
||||
|
||||
try {
|
||||
ExifInterface exifInterface=new ExifInterface(filePath);
|
||||
GPSExtractor imageObj = new GPSExtractor(exifInterface);
|
||||
if (imageObj.imageCoordsExists) { // If image has geolocation information in its EXIF
|
||||
return imageObj.getCoords();
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* In older devices getPath() may fail depending on the source URI. Creating and using a copy of the file seems to work instead.
|
||||
*
|
||||
|
|
@ -234,8 +254,8 @@ public class FileUtils {
|
|||
* @return The value of the _data column, which is typically a file path.
|
||||
*/
|
||||
@Nullable
|
||||
public static String getDataColumn(Context context, Uri uri, String selection,
|
||||
String[] selectionArgs) {
|
||||
private static String getDataColumn(Context context, Uri uri, String selection,
|
||||
String[] selectionArgs) {
|
||||
|
||||
Cursor cursor = null;
|
||||
final String column = MediaStore.Images.ImageColumns.DATA;
|
||||
|
|
@ -311,7 +331,7 @@ public class FileUtils {
|
|||
* @param destination file path copied to
|
||||
* @throws IOException thrown when failing to read source or opening destination file
|
||||
*/
|
||||
public static void copy(@NonNull FileDescriptor source, @NonNull String destination)
|
||||
private static void copy(@NonNull FileDescriptor source, @NonNull String destination)
|
||||
throws IOException {
|
||||
copy(new FileInputStream(source), new FileOutputStream(destination));
|
||||
}
|
||||
|
|
@ -415,7 +435,7 @@ public class FileUtils {
|
|||
return result;
|
||||
}
|
||||
|
||||
public static String getFileExt(String fileName){
|
||||
static String getFileExt(String fileName){
|
||||
//Default file extension
|
||||
String extension=".jpg";
|
||||
|
||||
|
|
@ -426,7 +446,11 @@ public class FileUtils {
|
|||
return extension;
|
||||
}
|
||||
|
||||
public static String getFileExt(Uri uri, ContentResolver contentResolver) {
|
||||
private static String getFileExt(Uri uri, ContentResolver contentResolver) {
|
||||
return getFileExt(getFilename(uri, contentResolver));
|
||||
}
|
||||
|
||||
public static FileInputStream getFileInputStream(String filePath) throws FileNotFoundException {
|
||||
return new FileInputStream(filePath);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -14,12 +14,12 @@ import timber.log.Timber;
|
|||
* Extracts geolocation to be passed to API for category suggestions. If a picture with geolocation
|
||||
* is uploaded, extract latitude and longitude from EXIF data of image.
|
||||
*/
|
||||
public class GPSExtractor {
|
||||
class GPSExtractor {
|
||||
|
||||
public static final GPSExtractor DUMMY= new GPSExtractor();
|
||||
static final GPSExtractor DUMMY= new GPSExtractor();
|
||||
private double decLatitude;
|
||||
private double decLongitude;
|
||||
public boolean imageCoordsExists;
|
||||
boolean imageCoordsExists;
|
||||
private String latitude;
|
||||
private String longitude;
|
||||
private String latitudeRef;
|
||||
|
|
@ -37,7 +37,7 @@ public class GPSExtractor {
|
|||
* @param fileDescriptor the file descriptor of the image
|
||||
*/
|
||||
@RequiresApi(24)
|
||||
public GPSExtractor(@NonNull FileDescriptor fileDescriptor) {
|
||||
GPSExtractor(@NonNull FileDescriptor fileDescriptor) {
|
||||
try {
|
||||
ExifInterface exif = new ExifInterface(fileDescriptor);
|
||||
processCoords(exif);
|
||||
|
|
@ -51,7 +51,7 @@ public class GPSExtractor {
|
|||
* @param path file path of the image
|
||||
*
|
||||
*/
|
||||
public GPSExtractor(@NonNull String path) {
|
||||
GPSExtractor(@NonNull String path) {
|
||||
try {
|
||||
ExifInterface exif = new ExifInterface(path);
|
||||
processCoords(exif);
|
||||
|
|
@ -65,7 +65,7 @@ public class GPSExtractor {
|
|||
* @param exif exif interface of the image
|
||||
*
|
||||
*/
|
||||
public GPSExtractor(@NonNull ExifInterface exif){
|
||||
GPSExtractor(@NonNull ExifInterface exif){
|
||||
processCoords(exif);
|
||||
}
|
||||
|
||||
|
|
@ -89,7 +89,7 @@ public class GPSExtractor {
|
|||
* @return coordinates as string (needs to be passed as a String in API query)
|
||||
*/
|
||||
@Nullable
|
||||
public String getCoords() {
|
||||
String getCoords() {
|
||||
if(decimalCoords!=null){
|
||||
return decimalCoords;
|
||||
}else if (latitude!=null && latitudeRef!=null && longitude!=null && longitudeRef!=null) {
|
||||
|
|
@ -103,11 +103,11 @@ public class GPSExtractor {
|
|||
}
|
||||
}
|
||||
|
||||
public double getDecLatitude() {
|
||||
double getDecLatitude() {
|
||||
return decLatitude;
|
||||
}
|
||||
|
||||
public double getDecLongitude() {
|
||||
double getDecLongitude() {
|
||||
return decLongitude;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -65,6 +65,7 @@ import timber.log.Timber;
|
|||
import static fr.free.nrw.commons.utils.ImageUtils.Result;
|
||||
import static fr.free.nrw.commons.utils.ImageUtils.getErrorMessageForResult;
|
||||
import static fr.free.nrw.commons.wikidata.WikidataConstants.WIKIDATA_ENTITY_ID_PREF;
|
||||
import static fr.free.nrw.commons.wikidata.WikidataConstants.WIKIDATA_ITEM_LOCATION;
|
||||
|
||||
public class UploadActivity extends AuthenticatedActivity implements UploadView, SimilarImageInterface {
|
||||
@Inject InputMethodManager inputMethodManager;
|
||||
|
|
@ -251,13 +252,14 @@ public class UploadActivity extends AuthenticatedActivity implements UploadView,
|
|||
|
||||
@SuppressLint("StringFormatInvalid")
|
||||
@Override
|
||||
public void updateLicenseSummary(String selectedLicense) {
|
||||
public void updateLicenseSummary(String selectedLicense, int imageCount) {
|
||||
String licenseHyperLink = "<a href='" + Utils.licenseUrlFor(selectedLicense)+"'>" +
|
||||
getString(Utils.licenseNameFor(selectedLicense)) + "</a><br>";
|
||||
licenseSummary.setMovementMethod(LinkMovementMethod.getInstance());
|
||||
licenseSummary.setText(
|
||||
Html.fromHtml(
|
||||
getString(R.string.share_license_summary, licenseHyperLink)));
|
||||
getResources().getQuantityString(R.plurals.share_license_summary,
|
||||
imageCount, licenseHyperLink)));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -350,6 +352,9 @@ public class UploadActivity extends AuthenticatedActivity implements UploadView,
|
|||
|
||||
@Override
|
||||
public void showBadPicturePopup(@Result int result) {
|
||||
if (result >= 8 ) { // If location of image and nearby does not match, then set shared preferences to disable wikidata edits
|
||||
directPrefs.edit().putBoolean("Picture_Has_Correct_Location",false);
|
||||
}
|
||||
String errorMessageForResult = getErrorMessageForResult(this, result);
|
||||
if (StringUtils.isNullOrWhiteSpace(errorMessageForResult)) {
|
||||
return;
|
||||
|
|
@ -553,7 +558,8 @@ public class UploadActivity extends AuthenticatedActivity implements UploadView,
|
|||
String imageDesc = directPrefs.getString("Desc", "");
|
||||
Timber.i("Received direct upload with title %s and description %s", imageTitle, imageDesc);
|
||||
String wikidataEntityIdPref = intent.getStringExtra(WIKIDATA_ENTITY_ID_PREF);
|
||||
presenter.receiveDirect(mediaUri, mimeType, source, wikidataEntityIdPref, imageTitle, imageDesc);
|
||||
String wikidataItemLocation = intent.getStringExtra(WIKIDATA_ITEM_LOCATION);
|
||||
presenter.receiveDirect(mediaUri, mimeType, source, wikidataEntityIdPref, imageTitle, imageDesc, wikidataItemLocation);
|
||||
} else {
|
||||
Timber.i("Received single upload");
|
||||
presenter.receive(mediaUri, mimeType, source);
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ public class UploadController {
|
|||
}
|
||||
|
||||
private boolean isUploadServiceConnected;
|
||||
private ServiceConnection uploadServiceConnection = new ServiceConnection() {
|
||||
public ServiceConnection uploadServiceConnection = new ServiceConnection() {
|
||||
@Override
|
||||
public void onServiceConnected(ComponentName componentName, IBinder binder) {
|
||||
uploadService = (UploadService) ((HandlerService.HandlerServiceLocalBinder) binder).getService();
|
||||
|
|
@ -61,6 +61,7 @@ public class UploadController {
|
|||
@Override
|
||||
public void onServiceDisconnected(ComponentName componentName) {
|
||||
// this should never happen
|
||||
isUploadServiceConnected = false;
|
||||
Timber.e(new RuntimeException("UploadService died but the rest of the process did not!"));
|
||||
}
|
||||
};
|
||||
|
|
@ -68,7 +69,7 @@ public class UploadController {
|
|||
/**
|
||||
* Prepares the upload service.
|
||||
*/
|
||||
public void prepareService() {
|
||||
void prepareService() {
|
||||
Intent uploadServiceIntent = new Intent(context, UploadService.class);
|
||||
uploadServiceIntent.setAction(UploadService.ACTION_START_SERVICE);
|
||||
context.startService(uploadServiceIntent);
|
||||
|
|
@ -78,7 +79,7 @@ public class UploadController {
|
|||
/**
|
||||
* Disconnects the upload service.
|
||||
*/
|
||||
public void cleanup() {
|
||||
void cleanup() {
|
||||
if (isUploadServiceConnected) {
|
||||
context.unbindService(uploadServiceConnection);
|
||||
}
|
||||
|
|
@ -89,7 +90,7 @@ public class UploadController {
|
|||
*
|
||||
* @param contribution the contribution object
|
||||
*/
|
||||
public void startUpload(Contribution contribution) {
|
||||
void startUpload(Contribution contribution) {
|
||||
startUpload(contribution, c -> {});
|
||||
}
|
||||
|
||||
|
|
@ -100,7 +101,7 @@ public class UploadController {
|
|||
* @param onComplete the progress tracker
|
||||
*/
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
public void startUpload(final Contribution contribution, final ContributionUploadProgress onComplete) {
|
||||
private void startUpload(final Contribution contribution, final ContributionUploadProgress onComplete) {
|
||||
//Set creator, desc, and license
|
||||
if (TextUtils.isEmpty(contribution.getCreator())) {
|
||||
Account currentAccount = sessionManager.getCurrentAccount();
|
||||
|
|
@ -110,7 +111,7 @@ public class UploadController {
|
|||
sessionManager.forceLogin(context);
|
||||
return;
|
||||
}
|
||||
contribution.setCreator(currentAccount.name);
|
||||
contribution.setCreator(sessionManager.getAuthorName());
|
||||
}
|
||||
|
||||
if (contribution.getDescription() == null) {
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ import android.net.Uri;
|
|||
import android.support.annotation.Nullable;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
|
|
@ -25,7 +24,9 @@ import fr.free.nrw.commons.auth.SessionManager;
|
|||
import fr.free.nrw.commons.contributions.Contribution;
|
||||
import fr.free.nrw.commons.mwapi.MediaWikiApi;
|
||||
import fr.free.nrw.commons.settings.Prefs;
|
||||
import fr.free.nrw.commons.utils.BitmapRegionDecoderWrapper;
|
||||
import fr.free.nrw.commons.utils.ImageUtils;
|
||||
import fr.free.nrw.commons.utils.ImageUtilsWrapper;
|
||||
import io.reactivex.Observable;
|
||||
import io.reactivex.Single;
|
||||
import io.reactivex.disposables.Disposable;
|
||||
|
|
@ -53,23 +54,36 @@ public class UploadModel {
|
|||
private boolean useExtStorage;
|
||||
private Disposable badImageSubscription;
|
||||
|
||||
@Inject
|
||||
SessionManager sessionManager;
|
||||
private SessionManager sessionManager;
|
||||
private Uri currentMediaUri;
|
||||
private FileUtilsWrapper fileUtilsWrapper;
|
||||
private ImageUtilsWrapper imageUtilsWrapper;
|
||||
private BitmapRegionDecoderWrapper bitmapRegionDecoderWrapper;
|
||||
private FileProcessor fileProcessor;
|
||||
|
||||
@Inject
|
||||
UploadModel(@Named("licenses") List<String> licenses,
|
||||
@Named("default_preferences") SharedPreferences prefs,
|
||||
@Named("licenses_by_name") Map<String, String> licensesByName,
|
||||
Context context,
|
||||
MediaWikiApi mwApi) {
|
||||
MediaWikiApi mwApi,
|
||||
SessionManager sessionManager,
|
||||
FileUtilsWrapper fileUtilsWrapper,
|
||||
ImageUtilsWrapper imageUtilsWrapper,
|
||||
BitmapRegionDecoderWrapper bitmapRegionDecoderWrapper,
|
||||
FileProcessor fileProcessor) {
|
||||
this.licenses = licenses;
|
||||
this.prefs = prefs;
|
||||
this.bitmapRegionDecoderWrapper = bitmapRegionDecoderWrapper;
|
||||
this.license = Prefs.Licenses.CC_BY_SA_3;
|
||||
this.licensesByName = licensesByName;
|
||||
this.context = context;
|
||||
this.mwApi = mwApi;
|
||||
this.contentResolver = context.getContentResolver();
|
||||
this.sessionManager = sessionManager;
|
||||
this.fileUtilsWrapper = fileUtilsWrapper;
|
||||
this.fileProcessor = fileProcessor;
|
||||
this.imageUtilsWrapper = imageUtilsWrapper;
|
||||
useExtStorage = this.prefs.getBoolean("useExternalStorage", false);
|
||||
}
|
||||
|
||||
|
|
@ -84,19 +98,19 @@ public class UploadModel {
|
|||
.map(filePath -> {
|
||||
long fileCreatedDate = getFileCreatedDate(currentMediaUri);
|
||||
Uri uri = Uri.fromFile(new File(filePath));
|
||||
FileProcessor fp = new FileProcessor(filePath, context.getContentResolver(), context);
|
||||
UploadItem item = new UploadItem(uri, mimeType, source, fp.processFileCoordinates(similarImageInterface),
|
||||
FileUtils.getFileExt(filePath), null,fileCreatedDate);
|
||||
fileProcessor.initFileDetails(filePath, context.getContentResolver());
|
||||
UploadItem item = new UploadItem(uri, mimeType, source, fileProcessor.processFileCoordinates(similarImageInterface),
|
||||
fileUtilsWrapper.getFileExt(filePath), null,fileCreatedDate);
|
||||
Single.zip(
|
||||
Single.fromCallable(() ->
|
||||
new FileInputStream(filePath))
|
||||
.map(FileUtils::getSHA1)
|
||||
fileUtilsWrapper.getFileInputStream(filePath))
|
||||
.map(fileUtilsWrapper::getSHA1)
|
||||
.map(mwApi::existingFile)
|
||||
.map(b -> b ? ImageUtils.IMAGE_DUPLICATE : ImageUtils.IMAGE_OK),
|
||||
Single.fromCallable(() ->
|
||||
new FileInputStream(filePath))
|
||||
.map(file -> BitmapRegionDecoder.newInstance(file, false))
|
||||
.map(ImageUtils::checkIfImageIsTooDark), //Returns IMAGE_DARK or IMAGE_OK
|
||||
fileUtilsWrapper.getFileInputStream(filePath))
|
||||
.map(file -> bitmapRegionDecoderWrapper.newInstance(file, false))
|
||||
.map(imageUtilsWrapper::checkIfImageIsTooDark), //Returns IMAGE_DARK or IMAGE_OK
|
||||
(dupe, dark) -> dupe | dark)
|
||||
.observeOn(Schedulers.io())
|
||||
.subscribe(item.imageQuality::onNext, Timber::e);
|
||||
|
|
@ -108,29 +122,33 @@ public class UploadModel {
|
|||
}
|
||||
|
||||
@SuppressLint("CheckResult")
|
||||
void receiveDirect(Uri media, String mimeType, String source, String wikidataEntityIdPref, String title, String desc, SimilarImageInterface similarImageInterface) {
|
||||
void receiveDirect(Uri media, String mimeType, String source, String wikidataEntityIdPref, String title, String desc, SimilarImageInterface similarImageInterface, String wikidataItemLocation) {
|
||||
initDefaultValues();
|
||||
long fileCreatedDate = getFileCreatedDate(media);
|
||||
String filePath = this.cacheFileUpload(media);
|
||||
Uri uri = Uri.fromFile(new File(filePath));
|
||||
FileProcessor fp = new FileProcessor(filePath, context.getContentResolver(), context);
|
||||
UploadItem item = new UploadItem(uri, mimeType, source, fp.processFileCoordinates(similarImageInterface),
|
||||
FileUtils.getFileExt(filePath), wikidataEntityIdPref,fileCreatedDate);
|
||||
fileProcessor.initFileDetails(filePath, context.getContentResolver());
|
||||
UploadItem item = new UploadItem(uri, mimeType, source, fileProcessor.processFileCoordinates(similarImageInterface),
|
||||
fileUtilsWrapper.getFileExt(filePath), wikidataEntityIdPref,fileCreatedDate);
|
||||
item.title.setTitleText(title);
|
||||
item.descriptions.get(0).setDescriptionText(desc);
|
||||
//TODO figure out if default descriptions in other languages exist
|
||||
item.descriptions.get(0).setLanguageCode("en");
|
||||
Single.zip(
|
||||
Single.fromCallable(() ->
|
||||
new FileInputStream(filePath))
|
||||
.map(FileUtils::getSHA1)
|
||||
fileUtilsWrapper.getFileInputStream(filePath))
|
||||
.map(fileUtilsWrapper::getSHA1)
|
||||
.map(mwApi::existingFile)
|
||||
.map(b -> b ? ImageUtils.IMAGE_DUPLICATE : ImageUtils.IMAGE_OK),
|
||||
Single.fromCallable(() -> filePath)
|
||||
.map(fileUtilsWrapper::getGeolocationOfFile)
|
||||
.map(geoLocation -> imageUtilsWrapper.checkImageGeolocationIsDifferent(geoLocation,wikidataItemLocation))
|
||||
.map(r -> r ? ImageUtils.IMAGE_GEOLOCATION_DIFFERENT : ImageUtils.IMAGE_OK),
|
||||
Single.fromCallable(() ->
|
||||
new FileInputStream(filePath))
|
||||
.map(file -> BitmapRegionDecoder.newInstance(file, false))
|
||||
.map(ImageUtils::checkIfImageIsTooDark), //Returns IMAGE_DARK or IMAGE_OK
|
||||
(dupe, dark) -> dupe | dark).subscribe(item.imageQuality::onNext);
|
||||
fileUtilsWrapper.getFileInputStream(filePath))
|
||||
.map(file -> bitmapRegionDecoderWrapper.newInstance(file, false))
|
||||
.map(imageUtilsWrapper::checkIfImageIsTooDark), //Returns IMAGE_DARK or IMAGE_OK
|
||||
(dupe, wrongGeo, dark) -> dupe | wrongGeo | dark).subscribe(item.imageQuality::onNext);
|
||||
items.add(item);
|
||||
items.get(0).selected = true;
|
||||
items.get(0).first = true;
|
||||
|
|
@ -239,7 +257,7 @@ public class UploadModel {
|
|||
updateItemState();
|
||||
}
|
||||
|
||||
public void setCurrentTitleAndDescriptions(Title title, List<Description> descriptions) {
|
||||
void setCurrentTitleAndDescriptions(Title title, List<Description> descriptions) {
|
||||
setCurrentUploadTitle(title);
|
||||
setCurrentUploadDescriptions(descriptions);
|
||||
}
|
||||
|
|
@ -312,7 +330,7 @@ public class UploadModel {
|
|||
{
|
||||
Contribution contribution = new Contribution(item.mediaUri, null, item.title + "." + item.fileExt,
|
||||
Description.formatList(item.descriptions), -1,
|
||||
null, null, sessionManager.getUserName(),
|
||||
null, null, sessionManager.getAuthorName(),
|
||||
CommonsApplication.DEFAULT_EDIT_SUMMARY, item.gpsCoords.getCoords());
|
||||
contribution.setWikiDataEntityId(item.wikidataEntityId);
|
||||
contribution.setCategories(categoryStringList);
|
||||
|
|
@ -337,9 +355,9 @@ public class UploadModel {
|
|||
try {
|
||||
String copyPath;
|
||||
if (useExtStorage)
|
||||
copyPath = FileUtils.createExternalCopyPathAndCopy(media, contentResolver);
|
||||
copyPath = fileUtilsWrapper.createExternalCopyPathAndCopy(media, contentResolver);
|
||||
else
|
||||
copyPath = FileUtils.createCopyPathAndCopy(media, context);
|
||||
copyPath = fileUtilsWrapper.createCopyPathAndCopy(media, context);
|
||||
Timber.i("File path is " + copyPath);
|
||||
return copyPath;
|
||||
} catch (IOException e) {
|
||||
|
|
@ -362,6 +380,9 @@ public class UploadModel {
|
|||
badImageSubscription = getCurrentItem().imageQuality.subscribe(consumer, Timber::e);
|
||||
}
|
||||
|
||||
public List<UploadItem> getItems() {
|
||||
return items;
|
||||
}
|
||||
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
static class UploadItem {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package fr.free.nrw.commons.upload;
|
|||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.net.Uri;
|
||||
import android.util.Log;
|
||||
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.util.ArrayList;
|
||||
|
|
@ -92,8 +93,8 @@ public class UploadPresenter {
|
|||
* @param source File source from {@link Contribution.FileSource}
|
||||
*/
|
||||
@SuppressLint("CheckResult")
|
||||
void receiveDirect(Uri media, String mimeType, @Contribution.FileSource String source, String wikidataEntityIdPref, String title, String desc) {
|
||||
Completable.fromRunnable(() -> uploadModel.receiveDirect(media, mimeType, source, wikidataEntityIdPref, title, desc, similarImageInterface))
|
||||
void receiveDirect(Uri media, String mimeType, @Contribution.FileSource String source, String wikidataEntityIdPref, String title, String desc, String wikidataItemLocation) {
|
||||
Completable.fromRunnable(() -> uploadModel.receiveDirect(media, mimeType, source, wikidataEntityIdPref, title, desc, similarImageInterface, wikidataItemLocation))
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(() -> {
|
||||
|
|
@ -111,7 +112,7 @@ public class UploadPresenter {
|
|||
*/
|
||||
void selectLicense(String licenseName) {
|
||||
uploadModel.setSelectedLicense(licenseName);
|
||||
view.updateLicenseSummary(uploadModel.getSelectedLicense());
|
||||
view.updateLicenseSummary(uploadModel.getSelectedLicense(), uploadModel.getCount());
|
||||
}
|
||||
|
||||
//region Wizard step management
|
||||
|
|
@ -356,7 +357,7 @@ public class UploadPresenter {
|
|||
private void updateLicenses() {
|
||||
String selectedLicense = uploadModel.getSelectedLicense();
|
||||
view.updateLicenses(uploadModel.getLicenses(), selectedLicense);
|
||||
view.updateLicenseSummary(selectedLicense);
|
||||
view.updateLicenseSummary(selectedLicense, uploadModel.getCount());
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -88,7 +88,7 @@ public class UploadService extends HandlerService<Contribution> {
|
|||
String notificationProgressTitle;
|
||||
String notificationFinishingTitle;
|
||||
|
||||
public NotificationUpdateProgressListener(String notificationTag, String notificationProgressTitle, String notificationFinishingTitle, Contribution contribution) {
|
||||
NotificationUpdateProgressListener(String notificationTag, String notificationProgressTitle, String notificationFinishingTitle, Contribution contribution) {
|
||||
this.notificationTag = notificationTag;
|
||||
this.notificationProgressTitle = notificationProgressTitle;
|
||||
this.notificationFinishingTitle = notificationFinishingTitle;
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ public interface UploadView {
|
|||
|
||||
void updateLicenses(List<String> licenses, String selectedLicense);
|
||||
|
||||
void updateLicenseSummary(String selectedLicense);
|
||||
void updateLicenseSummary(String selectedLicense, int imageCount);
|
||||
|
||||
void updateTopCardContent();
|
||||
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,11 @@ import android.content.Context;
|
|||
import android.net.Uri;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.Random;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
|
@ -29,9 +34,9 @@ public class ContributionUtils {
|
|||
// TODO add exceptions for Google Drive URİ is needed
|
||||
Uri result = null;
|
||||
|
||||
if (FileUtils.checkIfDirectoryExists(TEMP_EXTERNAL_DIRECTORY)) {
|
||||
if (checkIfDirectoryExists(TEMP_EXTERNAL_DIRECTORY)) {
|
||||
String destinationFilename = decideTempDestinationFileName();
|
||||
result = FileUtils.saveFileFromURI(context, URIfromContentProvider, destinationFilename);
|
||||
result = saveFileFromURI(context, URIfromContentProvider, destinationFilename);
|
||||
} else { // If directory doesn't exist, create it and recursive call current method to check again
|
||||
|
||||
File file = new File(TEMP_EXTERNAL_DIRECTORY);
|
||||
|
|
@ -53,29 +58,25 @@ public class ContributionUtils {
|
|||
//TODO: do I have to notify file system about deletion?
|
||||
File tempFile = new File(tempFileUri.getPath());
|
||||
if (tempFile.exists()) {
|
||||
boolean isDeleted= tempFile.delete();
|
||||
boolean isDeleted = tempFile.delete();
|
||||
Timber.e("removeTemporaryFile() parameters: URI tempFileUri %s, deleted status %b", tempFileUri, isDeleted);
|
||||
}
|
||||
}
|
||||
|
||||
private static String decideTempDestinationFileName() {
|
||||
int i = 0;
|
||||
while (true) {
|
||||
if (new File(TEMP_EXTERNAL_DIRECTORY +File.separatorChar+i+"_tmp").exists()) {
|
||||
// This file is in use, try enother file
|
||||
i++;
|
||||
} else {
|
||||
// Use time stamp for file name, so that two temporary file never has same file name
|
||||
// to prevent previous file reference bug
|
||||
Long tsLong = System.currentTimeMillis()/1000;
|
||||
String ts = tsLong.toString();
|
||||
|
||||
// For multiple uploads, time randomisation should be combined with another random
|
||||
// parameter, since they created at same time
|
||||
int multipleUploadRandomParameter = new Random().nextInt(100);
|
||||
return TEMP_EXTERNAL_DIRECTORY +File.separatorChar+ts+multipleUploadRandomParameter+"_tmp";
|
||||
}
|
||||
while (new File(TEMP_EXTERNAL_DIRECTORY + File.separatorChar + i + "_tmp").exists()) {
|
||||
i++;
|
||||
}
|
||||
|
||||
// Use time stamp for file name, so that two temporary file never has same file name
|
||||
// to prevent previous file reference bug
|
||||
String timeStamp = String.valueOf(System.currentTimeMillis() / 1000);
|
||||
|
||||
// For multiple uploads, time randomisation should be combined with another random
|
||||
// parameter, since they created at same time
|
||||
int multipleUploadRandomParameter = new Random().nextInt(100);
|
||||
return TEMP_EXTERNAL_DIRECTORY + File.separatorChar + timeStamp + multipleUploadRandomParameter + "_tmp";
|
||||
}
|
||||
|
||||
public static void emptyTemporaryDirectory() {
|
||||
|
|
@ -91,4 +92,58 @@ public class ContributionUtils {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves file from source URI to destination.
|
||||
* @param sourceUri Uri which points to file to be saved
|
||||
* @param destinationFilename where file will be located at
|
||||
* @return Uri points to file saved
|
||||
*/
|
||||
private static Uri saveFileFromURI(Context context, Uri sourceUri, String destinationFilename) {
|
||||
File file = new File(destinationFilename);
|
||||
if (file.exists()) {
|
||||
file.delete();
|
||||
}
|
||||
|
||||
InputStream in = null;
|
||||
OutputStream out = null;
|
||||
try {
|
||||
in = context.getContentResolver().openInputStream(sourceUri);
|
||||
out = new FileOutputStream(new File(destinationFilename));
|
||||
|
||||
byte[] buf = new byte[1024];
|
||||
int length;
|
||||
while ((length = in.read(buf)) > 0) {
|
||||
out.write(buf, 0, length);
|
||||
}
|
||||
} catch (FileNotFoundException e) {
|
||||
e.printStackTrace();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
try {
|
||||
if (out != null) out.close();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
try {
|
||||
if (in != null) in.close();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
return Uri.parse("file://" + destinationFilename);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if directory exists
|
||||
* @param pathToCheck path of directory to check
|
||||
* @return true if directory exists, false otherwise
|
||||
*/
|
||||
private static boolean checkIfDirectoryExists(String pathToCheck) {
|
||||
File dir = new File(pathToCheck);
|
||||
return dir.exists() && dir.isDirectory();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -9,6 +9,7 @@ import android.graphics.Rect;
|
|||
import android.net.Uri;
|
||||
import android.support.annotation.IntDef;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.util.Log;
|
||||
|
||||
import com.facebook.common.executors.CallerThreadExecutor;
|
||||
import com.facebook.common.references.CloseableReference;
|
||||
|
|
@ -25,6 +26,7 @@ import java.lang.annotation.Retention;
|
|||
import java.lang.annotation.RetentionPolicy;
|
||||
|
||||
import fr.free.nrw.commons.R;
|
||||
import fr.free.nrw.commons.location.LatLng;
|
||||
import timber.log.Timber;
|
||||
|
||||
/**
|
||||
|
|
@ -36,6 +38,7 @@ public class ImageUtils {
|
|||
public static final int IMAGE_DARK = 1;
|
||||
public static final int IMAGE_BLURRY = 1 << 1;
|
||||
public static final int IMAGE_DUPLICATE = 1 << 2;
|
||||
public static final int IMAGE_GEOLOCATION_DIFFERENT = 1 << 3;
|
||||
public static final int IMAGE_OK = 0;
|
||||
public static final int IMAGE_KEEP = -1;
|
||||
public static final int IMAGE_WAIT = -2;
|
||||
|
|
@ -54,7 +57,8 @@ public class ImageUtils {
|
|||
IMAGE_WAIT,
|
||||
EMPTY_TITLE,
|
||||
FILE_NAME_EXISTS,
|
||||
NO_CATEGORY_SELECTED
|
||||
NO_CATEGORY_SELECTED,
|
||||
IMAGE_GEOLOCATION_DIFFERENT
|
||||
}
|
||||
)
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
|
|
@ -93,17 +97,30 @@ public class ImageUtils {
|
|||
}
|
||||
|
||||
/**
|
||||
* Pulls the pixels into an array and then runs through it while checking the brightness of each pixel.
|
||||
* The calculation of brightness of each pixel is done by extracting the RGB constituents of the pixel
|
||||
* and then applying the formula to calculate its "Luminance".
|
||||
* Pixels with luminance greater than 40% are considered to be bright pixels while the ones with luminance
|
||||
* greater than 26% but less than 40% are considered to be pixels with medium brightness. The rest are
|
||||
* dark pixels.
|
||||
* If the number of bright pixels is more than 2.5% or the number of pixels with medium brightness is
|
||||
* more than 30% of the total number of pixels then the image is considered to be OK else dark.
|
||||
* @param bitmap The bitmap that needs to be checked.
|
||||
* @return true if bitmap is dark or null, false if bitmap is bright
|
||||
* @param geolocationOfFileString Geolocation of image. If geotag doesn't exists, then this will be an empty string
|
||||
* @param wikidataItemLocationString Location of wikidata item will be edited after upload
|
||||
* @return false if image is neither dark nor blurry or if the input bitmapRegionDecoder provided is null
|
||||
* true if geolocation of the image and wikidata item are different
|
||||
*/
|
||||
public static boolean checkImageGeolocationIsDifferent(String geolocationOfFileString, String wikidataItemLocationString) {
|
||||
Timber.d("Comparing geolocation of file with nearby place location");
|
||||
if (geolocationOfFileString == null || geolocationOfFileString == "") { // Means that geolocation for this image is not given
|
||||
return false; // Since we don't know geolocation of file, we choose letting upload
|
||||
}
|
||||
|
||||
String[] geolocationOfFile = geolocationOfFileString.split("\\|");
|
||||
String[] wikidataItemLocation = wikidataItemLocationString.split("/");
|
||||
|
||||
Double distance = LengthUtils.computeDistanceBetween(
|
||||
new LatLng(Double.parseDouble(geolocationOfFile[0]),Double.parseDouble(geolocationOfFile[1]),0)
|
||||
, new LatLng(Double.parseDouble(wikidataItemLocation[0]), Double.parseDouble(wikidataItemLocation[1]),0));
|
||||
if ( distance >= 1000 ) {// Distance is more than 1 km, means that geolocation is wrong
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean checkIfImageIsDark(Bitmap bitmap) {
|
||||
if (bitmap == null) {
|
||||
Timber.e("Expected bitmap was null");
|
||||
|
|
@ -206,24 +223,37 @@ public class ImageUtils {
|
|||
}
|
||||
|
||||
public static String getErrorMessageForResult(Context context, @Result int result) {
|
||||
String errorMessage;
|
||||
if (result == ImageUtils.IMAGE_DARK)
|
||||
errorMessage = context.getString(R.string.upload_image_problem_dark);
|
||||
else if (result == ImageUtils.IMAGE_BLURRY)
|
||||
errorMessage = context.getString(R.string.upload_image_problem_blurry);
|
||||
else if (result == ImageUtils.IMAGE_DUPLICATE)
|
||||
errorMessage = context.getString(R.string.upload_image_problem_duplicate);
|
||||
else if (result == (ImageUtils.IMAGE_DARK|ImageUtils.IMAGE_BLURRY))
|
||||
errorMessage = context.getString(R.string.upload_image_problem_dark_blurry);
|
||||
else if (result == (ImageUtils.IMAGE_DARK|ImageUtils.IMAGE_DUPLICATE))
|
||||
errorMessage = context.getString(R.string.upload_image_problem_dark_duplicate);
|
||||
else if (result == (ImageUtils.IMAGE_BLURRY|ImageUtils.IMAGE_DUPLICATE))
|
||||
errorMessage = context.getString(R.string.upload_image_problem_blurry_duplicate);
|
||||
else if (result == (ImageUtils.IMAGE_DARK|ImageUtils.IMAGE_BLURRY|ImageUtils.IMAGE_DUPLICATE))
|
||||
errorMessage = context.getString(R.string.upload_image_problem_dark_blurry_duplicate);
|
||||
else
|
||||
return "";
|
||||
/**
|
||||
* Result variable is a result of an or operation of all possbile problems. Ie. if result
|
||||
* is 0001 means IMAGE_DARK, if result is 1100 IMAGE_DUPLICATE and IMAGE_GEOLOCATION_DIFFERENT
|
||||
*/
|
||||
StringBuilder errorMessage = new StringBuilder();
|
||||
if (((IMAGE_DARK | IMAGE_GEOLOCATION_DIFFERENT | IMAGE_BLURRY | IMAGE_DUPLICATE) & result) == 0 ) {
|
||||
Timber.d("No issues to warn user is found");
|
||||
} else {
|
||||
Timber.d("Issues found to warn user");
|
||||
|
||||
return errorMessage;
|
||||
errorMessage.append(context.getResources().getString(R.string.upload_problem_exist));
|
||||
|
||||
if ((IMAGE_DARK & result) != 0 ) { // We are checking image dark bit to see if that bit is set or not
|
||||
errorMessage.append("\n - ").append(context.getResources().getString(R.string.upload_problem_image_dark));
|
||||
}
|
||||
|
||||
if ((IMAGE_BLURRY & result) != 0 ) {
|
||||
errorMessage.append("\n - ").append(context.getResources().getString(R.string.upload_problem_image_blurry));
|
||||
}
|
||||
|
||||
if ((IMAGE_DUPLICATE & result) != 0 ) {
|
||||
errorMessage.append("\n - ").append(context.getResources().getString(R.string.upload_problem_image_duplicate));
|
||||
}
|
||||
|
||||
if ((IMAGE_GEOLOCATION_DIFFERENT & result) != 0 ) {
|
||||
errorMessage.append("\n - ").append(context.getResources().getString(R.string.upload_problem_different_geolocation));
|
||||
}
|
||||
|
||||
errorMessage.append("\n\n").append(context.getResources().getString(R.string.upload_problem_do_you_continue));
|
||||
}
|
||||
|
||||
return errorMessage.toString();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
25
app/src/main/java/fr/free/nrw/commons/utils/PlaceUtils.java
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package fr.free.nrw.commons.upload;
|
||||
package fr.free.nrw.commons.widget;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
|
|
@ -13,10 +13,7 @@ import android.view.Display;
|
|||
* Created by Ilgaz Er on 8/7/2018.
|
||||
*/
|
||||
public class HeightLimitedRecyclerView extends RecyclerView {
|
||||
|
||||
int height;
|
||||
|
||||
|
||||
public HeightLimitedRecyclerView(Context context) {
|
||||
super(context);
|
||||
DisplayMetrics displayMetrics = new DisplayMetrics();
|
||||
|
|
@ -2,4 +2,5 @@ package fr.free.nrw.commons.wikidata;
|
|||
|
||||
public class WikidataConstants {
|
||||
public static final String WIKIDATA_ENTITY_ID_PREF = "WikiDataEntityId";
|
||||
public static final String WIKIDATA_ITEM_LOCATION = "WikiDataItemLocation";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -58,6 +58,11 @@ public class WikidataEditService {
|
|||
return;
|
||||
}
|
||||
|
||||
if (!(directPrefs.getBoolean("Picture_Has_Correct_Location",true))) {
|
||||
Timber.d("Image location and nearby place location mismatched, so Wikidata item won't be edited");
|
||||
return;
|
||||
}
|
||||
|
||||
editWikidataProperty(wikidataEntityId, fileName);
|
||||
}
|
||||
|
||||
|
|
|
|||
BIN
app/src/main/res/drawable-hdpi/ic_campaign.png
Executable file
|
After Width: | Height: | Size: 807 B |
BIN
app/src/main/res/drawable-mdpi/ic_campaign.png
Executable file
|
After Width: | Height: | Size: 542 B |
BIN
app/src/main/res/drawable-xhdpi/ic_campaign.png
Executable file
|
After Width: | Height: | Size: 1 KiB |
BIN
app/src/main/res/drawable-xxhdpi/ic_campaign.png
Executable file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
app/src/main/res/drawable-xxxhdpi/ic_campaign.png
Executable file
|
After Width: | Height: | Size: 2.1 KiB |
59
app/src/main/res/drawable/ic_app_logo.xml
Normal 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>
|
||||
5
app/src/main/res/drawable/ic_download_white_24dp.xml
Normal 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>
|
||||
62
app/src/main/res/drawable/ic_launcher_foreground.xml
Normal 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>
|
||||
|
|
@ -9,21 +9,6 @@
|
|||
android:gravity="center"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/welcomeYesButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="@dimen/overflow_button_dimen"
|
||||
android:layout_marginEnd="@dimen/standard_gap"
|
||||
android:layout_marginRight="@dimen/standard_gap"
|
||||
android:layout_marginTop="@dimen/standard_gap"
|
||||
android:text="@string/welcome_skip_button"
|
||||
android:textColor="#fff"
|
||||
android:textSize="@dimen/normal_text"
|
||||
android:textStyle="bold"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<android.support.constraint.Guideline
|
||||
android:id="@+id/center_guideline"
|
||||
android:layout_width="wrap_content"
|
||||
|
|
|
|||
|
|
@ -6,21 +6,6 @@
|
|||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:background="#0c609c">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/welcomeYesButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="@dimen/overflow_button_dimen"
|
||||
android:layout_marginEnd="@dimen/standard_gap"
|
||||
android:layout_marginRight="@dimen/standard_gap"
|
||||
android:layout_marginTop="@dimen/standard_gap"
|
||||
android:text="@string/welcome_skip_button"
|
||||
android:textColor="#fff"
|
||||
android:textSize="@dimen/normal_text"
|
||||
android:textStyle="bold"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<android.support.constraint.Guideline
|
||||
android:id="@+id/center_guideline"
|
||||
android:layout_width="wrap_content"
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@
|
|||
android:layout_marginBottom="@dimen/large_gap"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/welcome_help_button_text"
|
||||
android:id="@+id/welcomeInfo"
|
||||
android:layout_gravity="end|top"
|
||||
android:layout_marginTop="@dimen/standard_gap"
|
||||
|
|
@ -73,7 +72,7 @@
|
|||
android:layout_height="@dimen/overflow_button_dimen"
|
||||
android:layout_marginTop="@dimen/standard_gap"
|
||||
android:text="@string/welcome_final_button_text"
|
||||
android:id="@+id/welcomeYesButton"
|
||||
android:id="@+id/finishTutorialButton"
|
||||
android:layout_gravity="center"
|
||||
android:background="@android:color/white"
|
||||
android:textColor="#0c609c"
|
||||
|
|
|
|||
|
|
@ -8,21 +8,6 @@
|
|||
android:gravity="center"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/welcomeYesButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="@dimen/overflow_button_dimen"
|
||||
android:layout_marginEnd="@dimen/standard_gap"
|
||||
android:layout_marginRight="@dimen/standard_gap"
|
||||
android:layout_marginTop="@dimen/standard_gap"
|
||||
android:text="@string/welcome_skip_button"
|
||||
android:textColor="#fff"
|
||||
android:textSize="@dimen/normal_text"
|
||||
android:textStyle="bold"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<android.support.constraint.Guideline
|
||||
android:id="@+id/center_guideline"
|
||||
android:layout_width="wrap_content"
|
||||
|
|
|
|||
|
|
@ -7,21 +7,6 @@
|
|||
android:layout_height="match_parent"
|
||||
android:background="#0c609c">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/welcomeYesButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="@dimen/overflow_button_dimen"
|
||||
android:layout_marginEnd="@dimen/standard_gap"
|
||||
android:layout_marginRight="@dimen/standard_gap"
|
||||
android:layout_marginTop="@dimen/standard_gap"
|
||||
android:text="@string/welcome_skip_button"
|
||||
android:textColor="#fff"
|
||||
android:textSize="@dimen/normal_text"
|
||||
android:textStyle="bold"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<android.support.constraint.Guideline
|
||||
android:id="@+id/center_guideline"
|
||||
android:layout_width="wrap_content"
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@
|
|||
android:id="@+id/backgroundImage"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:actualImageScaleType="centerCrop" />
|
||||
app:actualImageScaleType="fitCenter" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/single_upload_fragment_container"
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@
|
|||
android:layout_height="match_parent"
|
||||
android:layout_below="@id/toolbar"
|
||||
android:background="@color/commons_app_blue_dark"
|
||||
app:actualImageScaleType="centerCrop" />
|
||||
app:actualImageScaleType="fitCenter" />
|
||||
|
||||
<android.support.constraint.ConstraintLayout
|
||||
android:id="@+id/activity_upload_cards"
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@
|
|||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:srcCompat="@drawable/ic_expand_less_black_24dp" />
|
||||
|
||||
<fr.free.nrw.commons.upload.HeightLimitedRecyclerView
|
||||
<fr.free.nrw.commons.widget.HeightLimitedRecyclerView
|
||||
android:id="@+id/rv_descriptions"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
|
|
|
|||