Merge branch 'master' of git://github.com/commons-app/apps-android-commons

This commit is contained in:
albendz 2018-06-22 17:07:00 -07:00
commit 83980a947e
249 changed files with 7923 additions and 3466 deletions

View file

@ -19,12 +19,13 @@ android:
components: components:
- tools - tools
- platform-tools - platform-tools
- build-tools-26.0.2 - build-tools-27.0.0
- extra-google-m2repository - extra-google-m2repository
- extra-android-m2repository - extra-android-m2repository
- ${ANDROID_TARGET} - ${ANDROID_TARGET}
- android-25 - android-25
- android-26 - android-26
- android-27
- sys-img-${ANDROID_ABI}-${ANDROID_TARGET} - sys-img-${ANDROID_ABI}-${ANDROID_TARGET}
licenses: licenses:
- 'android-sdk-license-.+' - 'android-sdk-license-.+'

View file

@ -1,5 +1,14 @@
# Wikimedia Commons for Android # Wikimedia Commons for Android
## v2.7.2
- Modified subtext for "automatically get current location" setting to emphasize that it will reveal user's location
## v2.7.1
- Fixed UI and permission issues with Nearby
- Fixed issue with My Recent Uploads being empty
- Fixed blank category issue when uploading directly from Nearby
- Various crash fixes
## v2.7.0 ## v2.7.0
- New Nearby Places UI with direct uploads (and associated category suggestions) - New Nearby Places UI with direct uploads (and associated category suggestions)
- Added two-factor authentication login - Added two-factor authentication login

View file

@ -1 +1,34 @@
Please see our guidelines in the wiki: https://github.com/commons-app/apps-android-commons/wiki/Volunteers-welcome%21 Thanks for considering to contribute to this project! A few guidelines for
people who want to contribute their code to this software are documented in
[this project's Wiki](https://github.com/commons-app/apps-android-commons/wiki/Contributing-Guidelines).
If you're not sure where to start head on to [this wiki page](https://github.com/commons-app/apps-android-commons/wiki/Volunteers-welcome!).
Here's a gist of the guidelines,
1. Make separate commits for logically separate changes
1. Describe your changes well in the commit message
The first line of the commit message should be a short description of what has
changed. It is also good to prefix the first line with "area: " where the "area"
is a filename or identifier for the general area of the code being modified.
The body should provide a meaningful commit message.
1. Write Javadocs
We require contributors to include Javadocs for all new methods and classes
submitted via PRs (after 1 May 2018). This is aimed at making it easier for
new contributors to dive into our codebase, especially those who are new to
Android development. A few things to note:
- This should not replace the need for code that is easily-readable in
and of itself
- Please make sure that your Javadocs are reasonably descriptive, not just
a copy of the method name
- Please do not use `@author` tags - we aim for collective code ownership,
and if needed, Git allows us to see who wrote something without needing
to add these tags (`git blame`)
1. Write tests for your code (if possible)
1. Make sure the Wiki pages don't become stale by updating them (if needed)

View file

@ -1,15 +1,19 @@
## Description ## Title (required)
Fixes #{GitHub issue number} Fixes #{GitHub issue number and title (Please do not forget adding title) }
## Description (required)
Fixes #{GitHub issue number and title}
{Describe the changes made and why they were made.} {Describe the changes made and why they were made.}
## Tests performed ## Tests performed (required)
Tested on {API level & name of device/emulator}, with {build variant, e.g. ProdDebug}. Tested on {API level & name of device/emulator}, with {build variant, e.g. ProdDebug}.
{Please test your PR at least once before submitting.} ## Screenshots showing what changed (optional)
## Screenshots showing what changed
{Only for user interface changes, otherwise remove this section. See [how to take a screenshot](https://android.stackexchange.com/questions/1759/how-to-take-a-screenshot-with-an-android-device)} {Only for user interface changes, otherwise remove this section. See [how to take a screenshot](https://android.stackexchange.com/questions/1759/how-to-take-a-screenshot-with-an-android-device)}
_Note: Please ensure that you have read CONTRIBUTING.md if this is your first pull request._

View file

@ -7,10 +7,12 @@ apply from: 'quality.gradle'
apply plugin: 'com.getkeepsafe.dexcount' apply plugin: 'com.getkeepsafe.dexcount'
dependencies { dependencies {
implementation 'com.squareup.picasso:picasso:2.71828'
implementation 'com.prof.rssparser:rssparser:1.1'
implementation 'com.github.nicolas-raoul:Quadtree:ac16ea8035bf07' implementation 'com.github.nicolas-raoul:Quadtree:ac16ea8035bf07'
implementation 'fr.avianey.com.viewpagerindicator:library:2.4.1.1@aar' implementation 'fr.avianey.com.viewpagerindicator:library:2.4.1.1@aar'
implementation 'in.yuvi:http.fluent:1.3' implementation 'in.yuvi:http.fluent:1.3'
implementation 'com.android.volley:volley:1.0.0' implementation 'com.github.chrisbanes:PhotoView:2.0.0'
implementation 'ch.acra:acra:4.9.2' implementation 'ch.acra:acra:4.9.2'
implementation 'org.mediawiki:api:1.3' implementation 'org.mediawiki:api:1.3'
implementation 'commons-codec:commons-codec:1.10' implementation 'commons-codec:commons-codec:1.10'
@ -18,69 +20,59 @@ dependencies {
implementation 'com.google.code.gson:gson:2.8.1' implementation 'com.google.code.gson:gson:2.8.1'
implementation 'com.jakewharton.timber:timber:4.5.1' implementation 'com.jakewharton.timber:timber:4.5.1'
implementation 'info.debatty:java-string-similarity:0.24' implementation 'info.debatty:java-string-similarity:0.24'
implementation ('com.mapbox.mapboxsdk:mapbox-android-sdk:5.4.1@aar'){ implementation 'com.borjabravo:readmoretextview:2.1.0'
transitive=true implementation 'com.android.support.constraint:constraint-layout:1.0.2'
implementation('com.mapbox.mapboxsdk:mapbox-android-sdk:5.4.1@aar') {
transitive = true
} }
implementation 'com.github.deano2390:MaterialShowcaseView:1.2.0'
implementation "com.android.support:support-v4:$SUPPORT_LIB_VERSION" implementation "com.android.support:support-v4:$SUPPORT_LIB_VERSION"
implementation "com.android.support:appcompat-v7:$SUPPORT_LIB_VERSION" implementation "com.android.support:appcompat-v7:$SUPPORT_LIB_VERSION"
implementation "com.android.support:design:$SUPPORT_LIB_VERSION" implementation "com.android.support:design:$SUPPORT_LIB_VERSION"
implementation "com.android.support:customtabs:$SUPPORT_LIB_VERSION" implementation "com.android.support:customtabs:$SUPPORT_LIB_VERSION"
implementation "com.android.support:cardview-v7:$SUPPORT_LIB_VERSION" implementation "com.android.support:cardview-v7:$SUPPORT_LIB_VERSION"
implementation "com.jakewharton:butterknife:$BUTTERKNIFE_VERSION" implementation "com.jakewharton:butterknife:$BUTTERKNIFE_VERSION"
kapt "com.jakewharton:butterknife-compiler:$BUTTERKNIFE_VERSION" kapt "com.jakewharton:butterknife-compiler:$BUTTERKNIFE_VERSION"
implementation 'com.squareup.okhttp3:okhttp:3.9.1'
implementation 'com.squareup.okhttp3:okhttp:3.8.1'
implementation 'com.squareup.okio:okio:1.13.0' implementation 'com.squareup.okio:okio:1.13.0'
implementation 'io.reactivex.rxjava2:rxandroid:2.0.1' implementation 'io.reactivex.rxjava2:rxandroid:2.0.1'
// Because RxAndroid releases are few and far between, it is recommended you also // 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. // explicitly depend on RxJava's latest version for bug fixes and new features.
implementation 'com.android.support:multidex:1.0.3' implementation 'com.android.support:multidex:1.0.3'
testImplementation "org.robolectric:multidex:3.4.2"
implementation 'io.reactivex.rxjava2:rxjava:2.1.2' implementation 'io.reactivex.rxjava2:rxjava:2.1.2'
implementation 'com.jakewharton.rxbinding2:rxbinding:2.0.0' implementation 'com.jakewharton.rxbinding2:rxbinding:2.0.0'
implementation 'com.jakewharton.rxbinding2:rxbinding-support-v4:2.0.0' implementation 'com.jakewharton.rxbinding2:rxbinding-support-v4:2.0.0'
implementation 'com.jakewharton.rxbinding2:rxbinding-appcompat-v7:2.0.0' implementation 'com.jakewharton.rxbinding2:rxbinding-appcompat-v7:2.0.0'
implementation 'com.jakewharton.rxbinding2:rxbinding-design:2.0.0' implementation 'com.jakewharton.rxbinding2:rxbinding-design:2.0.0'
implementation 'org.jsoup:jsoup:1.11.3'
implementation 'com.facebook.fresco:fresco:1.5.0' implementation 'com.facebook.fresco:fresco:1.5.0'
implementation 'com.facebook.stetho:stetho:1.5.0' implementation 'com.facebook.stetho:stetho:1.5.0'
implementation "com.google.dagger:dagger:$DAGGER_VERSION" implementation "com.google.dagger:dagger:$DAGGER_VERSION"
implementation "com.google.dagger:dagger-android-support:$DAGGER_VERSION" implementation "com.google.dagger:dagger-android-support:$DAGGER_VERSION"
kapt "com.google.dagger:dagger-android-processor:$DAGGER_VERSION" kapt "com.google.dagger:dagger-android-processor:$DAGGER_VERSION"
kapt "com.google.dagger:dagger-compiler:$DAGGER_VERSION" kapt "com.google.dagger:dagger-compiler:$DAGGER_VERSION"
testImplementation 'org.robolectric:multidex:3.4.2'
testImplementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version" testImplementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
androidTestImplementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version" testImplementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
testImplementation 'junit:junit:4.12' testImplementation 'junit:junit:4.12'
testImplementation 'org.robolectric:robolectric:3.7.1' testImplementation 'org.robolectric:robolectric:3.7.1'
testImplementation 'org.mockito:mockito-all:1.10.19' testImplementation 'com.nhaarman:mockito-kotlin:1.5.0'
testImplementation 'com.squareup.okhttp3:mockwebserver:3.8.1' testImplementation 'com.squareup.okhttp3:mockwebserver:3.8.1'
implementation 'com.caverock:androidsvg:1.2.1'
implementation 'com.github.bumptech.glide:glide:4.7.1'
kapt 'com.github.bumptech.glide:compiler:4.7.1'
androidTestImplementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
androidTestImplementation 'com.squareup.okhttp3:mockwebserver:3.8.1' androidTestImplementation 'com.squareup.okhttp3:mockwebserver:3.8.1'
androidTestImplementation "com.android.support:support-annotations:$SUPPORT_LIB_VERSION" androidTestImplementation "com.android.support:support-annotations:$SUPPORT_LIB_VERSION"
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1' androidTestImplementation 'com.android.support.test:rules:1.0.2'
androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
debugImplementation "com.squareup.leakcanary:leakcanary-android:$LEAK_CANARY" debugImplementation "com.squareup.leakcanary:leakcanary-android:$LEAK_CANARY"
releaseImplementation "com.squareup.leakcanary:leakcanary-android-no-op:$LEAK_CANARY" releaseImplementation "com.squareup.leakcanary:leakcanary-android-no-op:$LEAK_CANARY"
testImplementation "com.squareup.leakcanary:leakcanary-android-no-op:$LEAK_CANARY" testImplementation "com.squareup.leakcanary:leakcanary-android-no-op:$LEAK_CANARY"
implementation "com.google.dagger:dagger:$DAGGER_VERSION"
implementation "com.google.dagger:dagger-android-support:$DAGGER_VERSION"
kapt "com.google.dagger:dagger-compiler:$DAGGER_VERSION"
kapt "com.google.dagger:dagger-android-processor:$DAGGER_VERSION"
implementation 'com.borjabravo:readmoretextview:2.1.0'
implementation 'com.android.support.constraint:constraint-layout:1.0.2'
} }
android { android {
@ -91,8 +83,8 @@ android {
defaultConfig { defaultConfig {
applicationId 'fr.free.nrw.commons' applicationId 'fr.free.nrw.commons'
versionCode 83 versionCode 85
versionName '2.7.0' versionName '2.7.2'
setProperty("archivesBaseName", "app-commons-v$versionName-" + getBranchName()) setProperty("archivesBaseName", "app-commons-v$versionName-" + getBranchName())
minSdkVersion project.minSdkVersion minSdkVersion project.minSdkVersion
@ -121,7 +113,7 @@ android {
buildTypes { buildTypes {
release { release {
minifyEnabled false // See https://stackoverflow.com/questions/40232404/google-play-apk-and-android-studio-apk-usb-debug-behaving-differently - proguard.cfg modification alone insufficient. minifyEnabled false // See https://stackoverflow.com/questions/40232404/google-play-apk-and-android-studio-apk-usb-debug-behaving-differently - proguard.cfg modification alone insufficient.
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt' proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt', 'proguard-glide.txt'
} }
debug { debug {
applicationIdSuffix ".debug" applicationIdSuffix ".debug"
@ -133,7 +125,9 @@ android {
flavorDimensions 'tier' flavorDimensions 'tier'
productFlavors { productFlavors {
prod { prod {
buildConfigField "String", "WIKIMEDIA_API_POTD", "\"https://commons.wikimedia.org/w/api.php?action=featuredfeed&feed=potd&feedformat=rss&language=en\""
buildConfigField "String", "WIKIMEDIA_API_HOST", "\"https://commons.wikimedia.org/w/api.php\"" buildConfigField "String", "WIKIMEDIA_API_HOST", "\"https://commons.wikimedia.org/w/api.php\""
buildConfigField "String", "WIKIDATA_API_HOST", "\"https://www.wikidata.org/w/api.php\""
buildConfigField "String", "WIKIMEDIA_FORGE_API_HOST", "\"https://tools.wmflabs.org/\"" buildConfigField "String", "WIKIMEDIA_FORGE_API_HOST", "\"https://tools.wmflabs.org/\""
buildConfigField "String", "IMAGE_URL_BASE", "\"https://upload.wikimedia.org/wikipedia/commons\"" buildConfigField "String", "IMAGE_URL_BASE", "\"https://upload.wikimedia.org/wikipedia/commons\""
buildConfigField "String", "HOME_URL", "\"https://commons.wikimedia.org/wiki/\"" buildConfigField "String", "HOME_URL", "\"https://commons.wikimedia.org/wiki/\""
@ -149,7 +143,9 @@ android {
beta { beta {
// What values do we need to hit the BETA versions of the site / api ? // What values do we need to hit the BETA versions of the site / api ?
buildConfigField "String", "WIKIMEDIA_API_POTD", "\"https://commons.wikimedia.org/w/api.php?action=featuredfeed&feed=potd&feedformat=rss&language=en\""
buildConfigField "String", "WIKIMEDIA_API_HOST", "\"https://commons.wikimedia.beta.wmflabs.org/w/api.php\"" buildConfigField "String", "WIKIMEDIA_API_HOST", "\"https://commons.wikimedia.beta.wmflabs.org/w/api.php\""
buildConfigField "String", "WIKIDATA_API_HOST", "\"https://www.wikidata.org/w/api.php\""
buildConfigField "String", "WIKIMEDIA_FORGE_API_HOST", "\"https://tools.wmflabs.org/\"" buildConfigField "String", "WIKIMEDIA_FORGE_API_HOST", "\"https://tools.wmflabs.org/\""
buildConfigField "String", "IMAGE_URL_BASE", "\"https://upload.beta.wmflabs.org/wikipedia/commons\"" buildConfigField "String", "IMAGE_URL_BASE", "\"https://upload.beta.wmflabs.org/wikipedia/commons\""
buildConfigField "String", "HOME_URL", "\"https://commons.wikimedia.beta.wmflabs.org/wiki/\"" buildConfigField "String", "HOME_URL", "\"https://commons.wikimedia.beta.wmflabs.org/wiki/\""

BIN
app/libs/java-json.jar Normal file

Binary file not shown.

9
app/proguard-glide.txt Normal file
View file

@ -0,0 +1,9 @@
-keep public class * implements com.bumptech.glide.module.GlideModule
-keep public class * extends com.bumptech.glide.module.AppGlideModule
-keep public enum com.bumptech.glide.load.ImageHeaderParser$** {
**[] $VALUES;
public *;
}
# for DexGuard only
-keepresourcexmlelements manifest/application/meta-data@value=GlideModule

View file

@ -1,5 +1,4 @@
-dontobfuscate -dontobfuscate
-keep class org.apache.http.** { *; } -keep class org.apache.http.** { *; }
-dontwarn org.apache.http.** -dontwarn org.apache.http.**
-keep class fr.free.nrw.commons.upload.MwVolleyApi$Page {*;}
-keep class android.support.v7.widget.ShareActionProvider { *; } -keep class android.support.v7.widget.ShareActionProvider { *; }

View file

@ -18,7 +18,7 @@ task checkstyle(type: Checkstyle) {
reports { reports {
html { html {
enabled true enabled true
destination "${project.buildDir}/reports/checkstyle/checkstyle.html" destination file("${project.buildDir}/reports/checkstyle/checkstyle.html")
} }
} }
} }
@ -36,10 +36,10 @@ task pmd(type: Pmd) {
xml.enabled = false xml.enabled = false
html.enabled = true html.enabled = true
xml { xml {
destination "${project.buildDir}/reports/pmd/pmd.xml" destination file("${project.buildDir}/reports/pmd/pmd.xml")
} }
html { html {
destination "${project.buildDir}/reports/pmd/pmd.html" destination file("${project.buildDir}/reports/pmd/pmd.html")
} }
} }
} }

View file

@ -15,6 +15,7 @@
<uses-permission android:name="android.permission.MANAGE_DOCUMENTS" /> <uses-permission android:name="android.permission.MANAGE_DOCUMENTS" />
<uses-permission android:name="com.google.android.apps.photos.permission.GOOGLE_PHOTOS" /> <uses-permission android:name="com.google.android.apps.photos.permission.GOOGLE_PHOTOS" />
<uses-permission android:name="android.permission.READ_LOGS"/> <uses-permission android:name="android.permission.READ_LOGS"/>
<uses-permission android:name="android.permission.SET_WALLPAPER"/>
<!-- Needed only if your app targets Android 5.0 (API level 21) or higher. --> <!-- Needed only if your app targets Android 5.0 (API level 21) or higher. -->
<uses-feature android:name="android.hardware.location.gps" /> <uses-feature android:name="android.hardware.location.gps" />
@ -26,10 +27,10 @@
android:theme="@style/LightAppTheme" android:theme="@style/LightAppTheme"
android:supportsRtl="true" > android:supportsRtl="true" >
<activity android:name="org.acra.CrashReportDialog" <activity android:name="org.acra.CrashReportDialog"
android:theme="@android:style/Theme.Dialog" android:theme="@android:style/Theme.Dialog"
android:launchMode="singleInstance" android:launchMode="singleInstance"
android:excludeFromRecents="true" android:excludeFromRecents="true"
android:finishOnTaskLaunch="true" /> android:finishOnTaskLaunch="true" />
<activity android:name=".auth.LoginActivity"> <activity android:name=".auth.LoginActivity">
<intent-filter> <intent-filter>
@ -91,6 +92,11 @@
android:name=".notification.NotificationActivity" android:name=".notification.NotificationActivity"
android:label="@string/navigation_item_notification" /> android:label="@string/navigation_item_notification" />
<activity
android:name=".category.CategoryImagesActivity"
android:label="@string/title_activity_featured_images"
android:parentActivityName=".contributions.ContributionsActivity" />
<service android:name=".upload.UploadService" /> <service android:name=".upload.UploadService" />
<service <service
@ -159,6 +165,16 @@
android:label="@string/provider_categories" android:label="@string/provider_categories"
android:syncable="false" /> android:syncable="false" />
<receiver android:name=".widget.PicOfDayAppWidget">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/pic_of_day_app_widget_info" />
</receiver>
</application> </application>
</manifest> </manifest>

View file

@ -9,9 +9,6 @@ import android.os.Bundle;
import android.text.Html; import android.text.Html;
import android.text.SpannableString; import android.text.SpannableString;
import android.text.style.UnderlineSpan; import android.text.style.UnderlineSpan;
import android.util.Log;
import android.support.customtabs.CustomTabsIntent;
import android.support.v4.content.ContextCompat;
import android.view.Menu; import android.view.Menu;
import android.view.MenuInflater; import android.view.MenuInflater;
import android.view.MenuItem; import android.view.MenuItem;
@ -20,7 +17,6 @@ import android.widget.ArrayAdapter;
import android.widget.LinearLayout; import android.widget.LinearLayout;
import android.widget.Spinner; import android.widget.Spinner;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast;
import butterknife.BindView; import butterknife.BindView;
import butterknife.ButterKnife; import butterknife.ButterKnife;
@ -28,8 +24,6 @@ import butterknife.OnClick;
import fr.free.nrw.commons.theme.NavigationBaseActivity; import fr.free.nrw.commons.theme.NavigationBaseActivity;
import fr.free.nrw.commons.ui.widget.HtmlTextView; import fr.free.nrw.commons.ui.widget.HtmlTextView;
import static android.widget.Toast.LENGTH_SHORT;
/** /**
* Represents about screen of this app * Represents about screen of this app
*/ */
@ -135,9 +129,10 @@ public class AboutActivity extends NavigationBaseActivity {
public boolean onOptionsItemSelected(MenuItem item) { public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) { switch (item.getItemId()) {
case R.id.share_app_icon: case R.id.share_app_icon:
String shareText = "Upload photos to Wikimedia Commons on your phone\nDownload the Commons app: http://play.google.com/store/apps/details?id=fr.free.nrw.commons";
Intent sendIntent = new Intent(); Intent sendIntent = new Intent();
sendIntent.setAction(Intent.ACTION_SEND); sendIntent.setAction(Intent.ACTION_SEND);
sendIntent.putExtra(Intent.EXTRA_TEXT, "http://play.google.com/store/apps/details?id=fr.free.nrw.commons"); sendIntent.putExtra(Intent.EXTRA_TEXT, shareText);
sendIntent.setType("text/plain"); sendIntent.setType("text/plain");
startActivity(Intent.createChooser(sendIntent, "Share app via...")); startActivity(Intent.createChooser(sendIntent, "Share app via..."));
return true; return true;

View file

@ -1,6 +1,5 @@
package fr.free.nrw.commons; package fr.free.nrw.commons;
import android.app.Application;
import android.content.Context; import android.content.Context;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteDatabase;
@ -27,7 +26,7 @@ import fr.free.nrw.commons.contributions.ContributionDao;
import fr.free.nrw.commons.data.DBOpenHelper; import fr.free.nrw.commons.data.DBOpenHelper;
import fr.free.nrw.commons.di.ApplicationlessInjection; import fr.free.nrw.commons.di.ApplicationlessInjection;
import fr.free.nrw.commons.modifications.ModifierSequenceDao; import fr.free.nrw.commons.modifications.ModifierSequenceDao;
import fr.free.nrw.commons.utils.FileUtils; import fr.free.nrw.commons.upload.FileUtils;
import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers; import io.reactivex.schedulers.Schedulers;
import timber.log.Timber; import timber.log.Timber;

View file

@ -61,8 +61,8 @@ public class MediaDataExtractor {
} }
try{ try{
Timber.d("Nominated for deletion: " + mediaWikiApi.pageExists("Commons:Deletion_requests/"+filename)); deletionStatus = mediaWikiApi.pageExists("Commons:Deletion_requests/" + filename);
deletionStatus = mediaWikiApi.pageExists("Commons:Deletion_requests/"+filename); Timber.d("Nominated for deletion: " + deletionStatus);
} }
catch (Exception e){ catch (Exception e){
Timber.d(e.getMessage()); Timber.d(e.getMessage());

View file

@ -178,6 +178,7 @@ public class Utils {
} }
public static void handleWebUrl(Context context, Uri url) { public static void handleWebUrl(Context context, Uri url) {
Timber.d("Launching web url %s", url.toString());
Intent browserIntent = new Intent(Intent.ACTION_VIEW, url); Intent browserIntent = new Intent(Intent.ACTION_VIEW, url);
if (browserIntent.resolveActivity(context.getPackageManager()) == null) { if (browserIntent.resolveActivity(context.getPackageManager()) == null) {
Toast toast = Toast.makeText(context, context.getString(R.string.no_web_browser), LENGTH_SHORT); Toast toast = Toast.makeText(context, context.getString(R.string.no_web_browser), LENGTH_SHORT);

View file

@ -1,5 +1,6 @@
package fr.free.nrw.commons; package fr.free.nrw.commons;
import android.net.Uri;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.v4.view.PagerAdapter; import android.support.v4.view.PagerAdapter;
import android.view.LayoutInflater; import android.view.LayoutInflater;
@ -9,6 +10,7 @@ import android.widget.TextView;
import butterknife.ButterKnife; import butterknife.ButterKnife;
import butterknife.OnClick; import butterknife.OnClick;
import butterknife.Optional;
public class WelcomePagerAdapter extends PagerAdapter { public class WelcomePagerAdapter extends PagerAdapter {
static final int[] PAGE_LAYOUTS = new int[]{ static final int[] PAGE_LAYOUTS = new int[]{
@ -20,6 +22,7 @@ public class WelcomePagerAdapter extends PagerAdapter {
}; };
private static final int PAGE_FINAL = 4; private static final int PAGE_FINAL = 4;
private Callback callback; private Callback callback;
private ViewGroup container;
/** /**
* Changes callback to provided one * Changes callback to provided one
@ -53,6 +56,7 @@ public class WelcomePagerAdapter extends PagerAdapter {
@Override @Override
public Object instantiateItem(ViewGroup container, int position) { public Object instantiateItem(ViewGroup container, int position) {
this.container=container;
LayoutInflater inflater = LayoutInflater.from(container.getContext()); LayoutInflater inflater = LayoutInflater.from(container.getContext());
ViewGroup layout = (ViewGroup) inflater.inflate(PAGE_LAYOUTS[position], container, false); ViewGroup layout = (ViewGroup) inflater.inflate(PAGE_LAYOUTS[position], container, false);
if( BuildConfig.FLAVOR == "beta"){ if( BuildConfig.FLAVOR == "beta"){
@ -102,5 +106,15 @@ public class WelcomePagerAdapter extends PagerAdapter {
} }
} }
@Optional
@OnClick(R.id.welcomeInfo)
void onHelpClicked () {
try {
Utils.handleWebUrl(container.getContext(),Uri.parse("https://commons.wikimedia.org/wiki/Help:Contents" ));
} catch (Exception e) {
e.printStackTrace();
}
}
} }
} }

View file

@ -4,8 +4,8 @@ import android.accounts.Account;
import android.accounts.AccountAuthenticatorActivity; import android.accounts.AccountAuthenticatorActivity;
import android.accounts.AccountAuthenticatorResponse; import android.accounts.AccountAuthenticatorResponse;
import android.accounts.AccountManager; import android.accounts.AccountManager;
import android.app.Activity;
import android.app.ProgressDialog; import android.app.ProgressDialog;
import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.net.Uri; import android.net.Uri;
@ -19,18 +19,16 @@ import android.support.v4.content.ContextCompat;
import android.support.v7.app.AppCompatDelegate; import android.support.v7.app.AppCompatDelegate;
import android.text.Editable; import android.text.Editable;
import android.text.TextWatcher; import android.text.TextWatcher;
import android.util.Log;
import android.view.MenuInflater; import android.view.MenuInflater;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.view.inputmethod.InputMethodManager;
import android.widget.Button; import android.widget.Button;
import android.widget.EditText; import android.widget.EditText;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast;
import java.io.IOException; import java.io.IOException;
import java.util.Locale;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Named; import javax.inject.Named;
@ -48,6 +46,7 @@ import fr.free.nrw.commons.di.ApplicationlessInjection;
import fr.free.nrw.commons.mwapi.MediaWikiApi; import fr.free.nrw.commons.mwapi.MediaWikiApi;
import fr.free.nrw.commons.theme.NavigationBaseActivity; import fr.free.nrw.commons.theme.NavigationBaseActivity;
import fr.free.nrw.commons.ui.widget.HtmlTextView; import fr.free.nrw.commons.ui.widget.HtmlTextView;
import fr.free.nrw.commons.utils.ViewUtil;
import io.reactivex.Observable; import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers; import io.reactivex.schedulers.Schedulers;
@ -85,6 +84,10 @@ public class LoginActivity extends AccountAuthenticatorActivity {
private LoginTextWatcher textWatcher = new LoginTextWatcher(); private LoginTextWatcher textWatcher = new LoginTextWatcher();
private Boolean loginCurrentlyInProgress = false; private Boolean loginCurrentlyInProgress = false;
private Boolean errorMessageShown = false;
private String resultantError;
private static final String RESULTANT_ERROR = "resultantError";
private static final String ERROR_MESSAGE_SHOWN = "errorMessageShown";
private static final String LOGING_IN = "logingIn"; private static final String LOGING_IN = "logingIn";
@Override @Override
@ -106,14 +109,14 @@ public class LoginActivity extends AccountAuthenticatorActivity {
usernameEdit.addTextChangedListener(textWatcher); usernameEdit.addTextChangedListener(textWatcher);
usernameEdit.setOnFocusChangeListener((v, hasFocus) -> { usernameEdit.setOnFocusChangeListener((v, hasFocus) -> {
if (!hasFocus) { if (!hasFocus) {
hideKeyboard(v); ViewUtil.hideKeyboard(v);
} }
}); });
passwordEdit.addTextChangedListener(textWatcher); passwordEdit.addTextChangedListener(textWatcher);
passwordEdit.setOnFocusChangeListener((v, hasFocus) -> { passwordEdit.setOnFocusChangeListener((v, hasFocus) -> {
if (!hasFocus) { if (!hasFocus) {
hideKeyboard(v); ViewUtil.hideKeyboard(v);
} }
}); });
@ -125,13 +128,18 @@ public class LoginActivity extends AccountAuthenticatorActivity {
forgotPasswordText.setOnClickListener(view -> forgotPassword()); forgotPasswordText.setOnClickListener(view -> forgotPassword());
if(BuildConfig.FLAVOR == "beta"){ if(BuildConfig.FLAVOR.equals("beta")){
loginCredentials.setText(getString(R.string.login_credential)); loginCredentials.setText(getString(R.string.login_credential));
} else { } else {
loginCredentials.setVisibility(View.GONE); loginCredentials.setVisibility(View.GONE);
} }
} }
public static void startYourself(Context context) {
Intent intent = new Intent(context, LoginActivity.class);
context.startActivity(intent);
}
private void forgotPassword() { private void forgotPassword() {
Utils.handleWebUrl(this, Uri.parse(BuildConfig.FORGOT_PASSWORD_URL)); Utils.handleWebUrl(this, Uri.parse(BuildConfig.FORGOT_PASSWORD_URL));
} }
@ -141,12 +149,6 @@ public class LoginActivity extends AccountAuthenticatorActivity {
Utils.handleWebUrl(this,Uri.parse("https://github.com/commons-app/apps-android-commons/wiki/Privacy-policy\\")); Utils.handleWebUrl(this,Uri.parse("https://github.com/commons-app/apps-android-commons/wiki/Privacy-policy\\"));
} }
public void hideKeyboard(View view) {
InputMethodManager inputMethodManager =(InputMethodManager)this.getSystemService(Activity.INPUT_METHOD_SERVICE);
inputMethodManager.hideSoftInputFromWindow(view.getWindowToken(), 0);
}
@Override @Override
protected void onPostCreate(Bundle savedInstanceState) { protected void onPostCreate(Bundle savedInstanceState) {
super.onPostCreate(savedInstanceState); super.onPostCreate(savedInstanceState);
@ -160,7 +162,10 @@ public class LoginActivity extends AccountAuthenticatorActivity {
WelcomeActivity.startYourself(this); WelcomeActivity.startYourself(this);
prefs.edit().putBoolean("firstrun", false).apply(); prefs.edit().putBoolean("firstrun", false).apply();
} }
if (sessionManager.getCurrentAccount() != null) {
if (sessionManager.getCurrentAccount() != null
&& sessionManager.isUserLoggedIn()
&& sessionManager.getCachedAuthCookie() != null) {
startMainActivity(); startMainActivity();
} }
} }
@ -215,6 +220,8 @@ public class LoginActivity extends AccountAuthenticatorActivity {
handlePassResult(username, password); handlePassResult(username, password);
} else { } else {
loginCurrentlyInProgress = false; loginCurrentlyInProgress = false;
errorMessageShown = true;
resultantError = result;
handleOtherResults(result); handleOtherResults(result);
} }
} }
@ -266,18 +273,18 @@ public class LoginActivity extends AccountAuthenticatorActivity {
if (result.equals("NetworkFailure")) { if (result.equals("NetworkFailure")) {
// Matches NetworkFailure which is created by the doInBackground method // Matches NetworkFailure which is created by the doInBackground method
showMessageAndCancelDialog(R.string.login_failed_network); showMessageAndCancelDialog(R.string.login_failed_network);
} else if (result.toLowerCase().contains("nosuchuser".toLowerCase()) || result.toLowerCase().contains("noname".toLowerCase())) { } else if (result.toLowerCase(Locale.getDefault()).contains("nosuchuser".toLowerCase()) || result.toLowerCase().contains("noname".toLowerCase())) {
// Matches nosuchuser, nosuchusershort, noname // Matches nosuchuser, nosuchusershort, noname
showMessageAndCancelDialog(R.string.login_failed_username); showMessageAndCancelDialog(R.string.login_failed_wrong_credentials);
emptySensitiveEditFields(); emptySensitiveEditFields();
} else if (result.toLowerCase().contains("wrongpassword".toLowerCase())) { } else if (result.toLowerCase(Locale.getDefault()).contains("wrongpassword".toLowerCase())) {
// Matches wrongpassword, wrongpasswordempty // Matches wrongpassword, wrongpasswordempty
showMessageAndCancelDialog(R.string.login_failed_password); showMessageAndCancelDialog(R.string.login_failed_wrong_credentials);
emptySensitiveEditFields(); emptySensitiveEditFields();
} else if (result.toLowerCase().contains("throttle".toLowerCase())) { } else if (result.toLowerCase(Locale.getDefault()).contains("throttle".toLowerCase())) {
// Matches unknown throttle error codes // Matches unknown throttle error codes
showMessageAndCancelDialog(R.string.login_failed_throttled); showMessageAndCancelDialog(R.string.login_failed_throttled);
} else if (result.toLowerCase().contains("userblocked".toLowerCase())) { } else if (result.toLowerCase(Locale.getDefault()).contains("userblocked".toLowerCase())) {
// Matches login-userblocked // Matches login-userblocked
showMessageAndCancelDialog(R.string.login_failed_blocked); showMessageAndCancelDialog(R.string.login_failed_blocked);
} else if (result.equals("2FA")) { } else if (result.equals("2FA")) {
@ -341,15 +348,22 @@ public class LoginActivity extends AccountAuthenticatorActivity {
protected void onSaveInstanceState(Bundle outState) { protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState); super.onSaveInstanceState(outState);
outState.putBoolean(LOGING_IN, loginCurrentlyInProgress); outState.putBoolean(LOGING_IN, loginCurrentlyInProgress);
outState.putBoolean(ERROR_MESSAGE_SHOWN, errorMessageShown);
outState.putString(RESULTANT_ERROR, resultantError);
} }
@Override @Override
protected void onRestoreInstanceState(Bundle savedInstanceState) { protected void onRestoreInstanceState(Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState); super.onRestoreInstanceState(savedInstanceState);
loginCurrentlyInProgress = savedInstanceState.getBoolean(LOGING_IN, false); loginCurrentlyInProgress = savedInstanceState.getBoolean(LOGING_IN, false);
errorMessageShown = savedInstanceState.getBoolean(ERROR_MESSAGE_SHOWN, false);
if(loginCurrentlyInProgress){ if(loginCurrentlyInProgress){
performLogin(); performLogin();
} }
if(errorMessageShown){
resultantError = savedInstanceState.getString(RESULTANT_ERROR);
handleOtherResults(resultantError);
}
} }
public void askUserForTwoFactorAuth() { public void askUserForTwoFactorAuth() {
@ -361,7 +375,9 @@ public class LoginActivity extends AccountAuthenticatorActivity {
public void showMessageAndCancelDialog(@StringRes int resId) { public void showMessageAndCancelDialog(@StringRes int resId) {
showMessage(resId, R.color.secondaryDarkColor); showMessage(resId, R.color.secondaryDarkColor);
progressDialog.cancel(); if(progressDialog != null){
progressDialog.cancel();
}
} }
public void showSuccessAndDismissDialog() { public void showSuccessAndDismissDialog() {

View file

@ -61,13 +61,11 @@ public class SessionManager {
} }
public String getAuthCookie() { public String getAuthCookie() {
boolean isLoggedIn = sharedPreferences.getBoolean("isUserLoggedIn", false); if (!isUserLoggedIn()) {
if (!isLoggedIn) {
Timber.e("User is not logged in"); Timber.e("User is not logged in");
return null; return null;
} else { } else {
String authCookie = sharedPreferences.getString("getAuthCookie", null); String authCookie = getCachedAuthCookie();
if (authCookie == null) { if (authCookie == null) {
Timber.e("Auth cookie is null even after login"); Timber.e("Auth cookie is null even after login");
} }
@ -75,6 +73,20 @@ public class SessionManager {
} }
} }
public String getCachedAuthCookie() {
return sharedPreferences.getString("getAuthCookie", null);
}
public boolean isUserLoggedIn() {
return sharedPreferences.getBoolean("isUserLoggedIn", false);
}
public void forceLogin(Context context) {
if (context != null) {
LoginActivity.startYourself(context);
}
}
public Completable clearAllAccounts() { public Completable clearAllAccounts() {
AccountManager accountManager = AccountManager.get(context); AccountManager accountManager = AccountManager.get(context);
Account[] allAccounts = accountManager.getAccountsByType(ACCOUNT_TYPE); Account[] allAccounts = accountManager.getAccountsByType(ACCOUNT_TYPE);

View file

@ -7,18 +7,25 @@ import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import fr.free.nrw.commons.upload.MwVolleyApi; import javax.inject.Inject;
import javax.inject.Singleton;
import fr.free.nrw.commons.upload.GpsCategoryModel;
import timber.log.Timber; import timber.log.Timber;
@Singleton
public class CacheController { public class CacheController {
private final GpsCategoryModel gpsCategoryModel;
private final QuadTree<List<String>> quadTree;
private double x, y; private double x, y;
private QuadTree<List<String>> quadTree;
private double xMinus, xPlus, yMinus, yPlus; private double xMinus, xPlus, yMinus, yPlus;
private static final int EARTH_RADIUS = 6378137; private static final int EARTH_RADIUS = 6378137;
public CacheController() { @Inject
CacheController(GpsCategoryModel gpsCategoryModel) {
this.gpsCategoryModel = gpsCategoryModel;
quadTree = new QuadTree<>(-180, -90, +180, +90); quadTree = new QuadTree<>(-180, -90, +180, +90);
} }
@ -31,8 +38,8 @@ public class CacheController {
public void cacheCategory() { public void cacheCategory() {
List<String> pointCatList = new ArrayList<>(); List<String> pointCatList = new ArrayList<>();
if (MwVolleyApi.GpsCatExists.getGpsCatExists()) { if (gpsCategoryModel.getGpsCatExists()) {
pointCatList.addAll(MwVolleyApi.getGpsCat()); pointCatList.addAll(gpsCategoryModel.getCategoryList());
Timber.d("Categories being cached: %s", pointCatList); Timber.d("Categories being cached: %s", pointCatList);
} else { } else {
Timber.d("No categories found, so no categories cached"); Timber.d("No categories found, so no categories cached");
@ -65,7 +72,7 @@ public class CacheController {
} }
//Based on algorithm at http://gis.stackexchange.com/questions/2951/algorithm-for-offsetting-a-latitude-longitude-by-some-amount-of-meters //Based on algorithm at http://gis.stackexchange.com/questions/2951/algorithm-for-offsetting-a-latitude-longitude-by-some-amount-of-meters
public void convertCoordRange() { private void convertCoordRange() {
//Position, decimal degrees //Position, decimal degrees
double lat = y; double lat = y;
double lon = x; double lon = x;

View file

@ -1,7 +1,6 @@
package fr.free.nrw.commons.category; package fr.free.nrw.commons.category;
import android.app.Activity;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.os.Bundle; import android.os.Bundle;
import android.support.v7.app.AlertDialog; import android.support.v7.app.AlertDialog;
@ -10,14 +9,12 @@ import android.support.v7.widget.RecyclerView;
import android.text.Editable; import android.text.Editable;
import android.text.TextUtils; import android.text.TextUtils;
import android.text.TextWatcher; import android.text.TextWatcher;
import android.util.Log;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.Menu; import android.view.Menu;
import android.view.MenuInflater; import android.view.MenuInflater;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.view.inputmethod.InputMethodManager;
import android.widget.EditText; import android.widget.EditText;
import android.widget.ProgressBar; import android.widget.ProgressBar;
import android.widget.TextView; import android.widget.TextView;
@ -42,9 +39,9 @@ import butterknife.ButterKnife;
import fr.free.nrw.commons.R; import fr.free.nrw.commons.R;
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment; import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
import fr.free.nrw.commons.mwapi.MediaWikiApi; import fr.free.nrw.commons.mwapi.MediaWikiApi;
import fr.free.nrw.commons.upload.MwVolleyApi; import fr.free.nrw.commons.upload.GpsCategoryModel;
import fr.free.nrw.commons.upload.SingleUploadFragment;
import fr.free.nrw.commons.utils.StringSortingUtils; import fr.free.nrw.commons.utils.StringSortingUtils;
import fr.free.nrw.commons.utils.ViewUtil;
import io.reactivex.Observable; import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers; import io.reactivex.schedulers.Schedulers;
@ -76,6 +73,7 @@ public class CategorizationFragment extends CommonsDaggerSupportFragment {
@Inject @Named("prefs") SharedPreferences prefsPrefs; @Inject @Named("prefs") SharedPreferences prefsPrefs;
@Inject @Named("direct_nearby_upload_prefs") SharedPreferences directPrefs; @Inject @Named("direct_nearby_upload_prefs") SharedPreferences directPrefs;
@Inject CategoryDao categoryDao; @Inject CategoryDao categoryDao;
@Inject GpsCategoryModel gpsCategoryModel;
private RVRendererAdapter<CategoryItem> categoriesAdapter; private RVRendererAdapter<CategoryItem> categoriesAdapter;
private OnCategoriesSaveHandler onCategoriesSaveHandler; private OnCategoriesSaveHandler onCategoriesSaveHandler;
@ -118,7 +116,7 @@ public class CategorizationFragment extends CommonsDaggerSupportFragment {
categoriesFilter.setOnFocusChangeListener((v, hasFocus) -> { categoriesFilter.setOnFocusChangeListener((v, hasFocus) -> {
if (!hasFocus) { if (!hasFocus) {
hideKeyboard(v); ViewUtil.hideKeyboard(v);
} }
}); });
@ -130,11 +128,6 @@ public class CategorizationFragment extends CommonsDaggerSupportFragment {
return rootView; return rootView;
} }
public void hideKeyboard(View view) {
InputMethodManager inputMethodManager = (InputMethodManager) getActivity().getSystemService(Activity.INPUT_METHOD_SERVICE);
inputMethodManager.hideSoftInputFromWindow(view.getWindowToken(), 0);
}
@Override @Override
public void onDestroyView() { public void onDestroyView() {
categoriesFilter.removeTextChangedListener(textWatcher); categoriesFilter.removeTextChangedListener(textWatcher);
@ -261,7 +254,6 @@ public class CategorizationFragment extends CommonsDaggerSupportFragment {
} }
private Observable<CategoryItem> defaultCategories() { private Observable<CategoryItem> defaultCategories() {
Observable<CategoryItem> directCat = directCategories(); Observable<CategoryItem> directCat = directCategories();
if (hasDirectCategories) { if (hasDirectCategories) {
Timber.d("Image has direct Cat"); Timber.d("Image has direct Cat");
@ -295,9 +287,7 @@ public class CategorizationFragment extends CommonsDaggerSupportFragment {
} }
private Observable<CategoryItem> gpsCategories() { private Observable<CategoryItem> gpsCategories() {
return Observable.fromIterable( return Observable.fromIterable(gpsCategoryModel.getCategoryList())
MwVolleyApi.GpsCatExists.getGpsCatExists()
? MwVolleyApi.getGpsCat() : new ArrayList<>())
.map(name -> new CategoryItem(name, false)); .map(name -> new CategoryItem(name, false));
} }

View file

@ -105,6 +105,7 @@ public class CategoryDao {
return items; return items;
} }
@NonNull
Category fromCursor(Cursor cursor) { Category fromCursor(Cursor cursor) {
// Hardcoding column positions! // Hardcoding column positions!
return new Category( return new Category(

View file

@ -0,0 +1,29 @@
package fr.free.nrw.commons.category;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Singleton;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
@Singleton
public class CategoryImageController {
private MediaWikiApi mediaWikiApi;
@Inject
public CategoryImageController(MediaWikiApi mediaWikiApi) {
this.mediaWikiApi = mediaWikiApi;
}
/**
* Takes a category name as input and calls the API to get a list of images for that category
* @param categoryName
* @return
*/
public List<Media> getCategoryImages(String categoryName) {
return mediaWikiApi.getCategoryImages(categoryName);
}
}

View file

@ -0,0 +1,225 @@
package fr.free.nrw.commons.category;
import org.jsoup.Jsoup;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import javax.annotation.Nullable;
import fr.free.nrw.commons.Media;
import timber.log.Timber;
public class CategoryImageUtils {
/**
* The method iterates over the child nodes to return a list of Media objects
* @param childNodes
* @return
*/
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);
categoryImages.add(getMediaFromPage(node));
}
return categoryImages;
}
/**
* Creates a new Media object from the XML response as received by the API
* @param node
* @return
*/
private static Media getMediaFromPage(Node node) {
Media media = new Media(null,
getImageUrl(node),
getFileName(node),
getDescription(node),
getDataLength(node),
getDateCreated(node),
getDateCreated(node),
getCreator(node)
);
media.setLicense(getLicense(node));
return media;
}
/**
* Extracts the filename of the uploaded image
* @param document
* @return
*/
private static String getFileName(Node document) {
Element element = (Element) document;
return element.getAttribute("title");
}
/**
* Extracts the image description for that particular upload
* @param document
* @return
*/
private static String getDescription(Node document) {
return getMetaDataValue(document, "ImageDescription");
}
/**
* Extracts license information from the image meta data
* @param document
* @return
*/
private static String getLicense(Node document) {
return getMetaDataValue(document, "License");
}
/**
* Returns the parsed value of artist from the response
* The artist information is returned as a HTML string from the API. Jsoup library parses the HTML string
* to extract just the text value
* @param document
* @return
*/
private static String getCreator(Node document) {
String artist = getMetaDataValue(document, "Artist");
if (artist != null) {
return Jsoup.parse(artist).text();
}
return null;
}
/**
* Returns the parsed date of creation of the image
* @param document
* @return
*/
private static Date getDateCreated(Node document) {
String dateTime = getMetaDataValue(document, "DateTime");
if (dateTime != null && !dateTime.equals("")) {
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
try {
return format.parse(dateTime);
} catch (ParseException e) {
Timber.d("Error occurred while parsing date %s", dateTime);
return new Date();
}
}
return new Date();
}
/**
* @param document
* @return Returns the url attribute from the imageInfo node
*/
private static String getImageUrl(Node document) {
Element element = (Element) getImageInfo(document);
if (element != null) {
return element.getAttribute("url");
}
return null;
}
/**
* Takes the node document and gives out the attribute length from the node document
* @param document
* @return
*/
private static long getDataLength(Node document) {
Element element = (Element) document;
if (element != null) {
String length = element.getAttribute("length");
if (length != null && !length.equals("")) {
return Long.parseLong(length);
}
}
return 0L;
}
/**
* Generic method to get the value of any meta as returned by the getMetaData function
* @param document node document as returned by API
* @param metaName the name of meta node to be returned
* @return
*/
private static String getMetaDataValue(Node document, String metaName) {
Element metaData = getMetaData(document, metaName);
if (metaData != null) {
return metaData.getAttribute("value");
}
return null;
}
/**
* Generic method to return an element taking the node document and metaName as input
* @param document node document as returned by API
* @param metaName the name of meta node to be returned
* @return
*/
@Nullable
private static Element getMetaData(Node document, String metaName) {
Node extraMetaData = getExtraMetaData(document);
if (extraMetaData != null) {
Node node = getNode(extraMetaData, metaName);
if (node != null) {
return (Element) node;
}
}
return null;
}
/**
* Extracts extmetadata from the response XML
* @param document
* @return
*/
@Nullable
private static Node getExtraMetaData(Node document) {
Node imageInfo = getImageInfo(document);
if (imageInfo != null) {
return getNode(imageInfo, "extmetadata");
}
return null;
}
/**
* Extracts the ii node from the imageinfo node
* @param document
* @return
*/
@Nullable
private static Node getImageInfo(Node document) {
Node imageInfo = getNode(document, "imageinfo");
if (imageInfo != null) {
return getNode(imageInfo, "ii");
}
return null;
}
/**
* Takes a parent node as input and returns a child node if present
* @param node parent node
* @param nodeName child node name
* @return
*/
@Nullable
public static Node getNode(Node node, String nodeName) {
NodeList childNodes = node.getChildNodes();
for (int i = 0; i < childNodes.getLength(); i++) {
Node nodeItem = childNodes.item(i);
Element item = (Element) nodeItem;
if (item.getTagName().equals(nodeName)) {
return nodeItem;
}
}
return null;
}
}

View file

@ -0,0 +1,159 @@
package fr.free.nrw.commons.category;
import android.content.Context;
import android.content.Intent;
import android.database.DataSetObserver;
import android.os.Bundle;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentTransaction;
import android.view.View;
import android.widget.AdapterView;
import butterknife.ButterKnife;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.auth.AuthenticatedActivity;
import fr.free.nrw.commons.media.MediaDetailPagerFragment;
/**
* This activity displays pictures of a particular category
* Its generic and simply takes the name of category name in its start intent to load all images in
* a particular category. This activity is currently being used to display a list of featured images,
* which is nothing but another category on wikimedia commons.
*/
public class CategoryImagesActivity
extends AuthenticatedActivity
implements FragmentManager.OnBackStackChangedListener,
MediaDetailPagerFragment.MediaDetailProvider,
AdapterView.OnItemClickListener{
private FragmentManager supportFragmentManager;
private CategoryImagesListFragment categoryImagesListFragment;
private MediaDetailPagerFragment mediaDetails;
@Override
protected void onAuthCookieAcquired(String authCookie) {
}
@Override
protected void onAuthFailure() {
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_category_images);
ButterKnife.bind(this);
// Activity can call methods in the fragment by acquiring a
// reference to the Fragment from FragmentManager, using findFragmentById()
supportFragmentManager = getSupportFragmentManager();
setCategoryImagesFragment();
supportFragmentManager.addOnBackStackChangedListener(this);
if (savedInstanceState != null) {
mediaDetails = (MediaDetailPagerFragment) supportFragmentManager
.findFragmentById(R.id.fragmentContainer);
}
requestAuthToken();
initDrawer();
setPageTitle();
}
/**
* Gets the categoryName from the intent and initializes the fragment for showing images of that category
*/
private void setCategoryImagesFragment() {
categoryImagesListFragment = new CategoryImagesListFragment();
String categoryName = getIntent().getStringExtra("categoryName");
if (getIntent() != null && categoryName != null) {
Bundle arguments = new Bundle();
arguments.putString("categoryName", categoryName);
categoryImagesListFragment.setArguments(arguments);
FragmentTransaction transaction = supportFragmentManager.beginTransaction();
transaction
.add(R.id.fragmentContainer, categoryImagesListFragment)
.commit();
}
}
/**
* Gets the passed title from the intents and displays it as the page title
*/
private void setPageTitle() {
if (getIntent() != null && getIntent().getStringExtra("title") != null) {
setTitle(getIntent().getStringExtra("title"));
}
}
@Override
public void onBackStackChanged() {
}
@Override
public void onItemClick(AdapterView<?> adapterView, View view, int i, long l) {
if (mediaDetails == null || !mediaDetails.isVisible()) {
// set isFeaturedImage true for featured images, to include author field on media detail
mediaDetails = new MediaDetailPagerFragment(false, true);
FragmentManager supportFragmentManager = getSupportFragmentManager();
supportFragmentManager
.beginTransaction()
.replace(R.id.fragmentContainer, mediaDetails)
.addToBackStack(null)
.commit();
supportFragmentManager.executePendingTransactions();
}
mediaDetails.showImage(i);
}
/**
* Consumers should be simply using this method to use this activity.
* @param context
* @param title Page title
* @param categoryName Name of the category for displaying its images
*/
public static void startYourself(Context context, String title, String categoryName) {
Intent intent = new Intent(context, CategoryImagesActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
intent.putExtra("title", title);
intent.putExtra("categoryName", categoryName);
context.startActivity(intent);
}
@Override
public Media getMediaAtPosition(int i) {
if (categoryImagesListFragment.getAdapter() == null) {
// not yet ready to return data
return null;
} else {
return (Media) categoryImagesListFragment.getAdapter().getItem(i);
}
}
@Override
public int getTotalMediaCount() {
if (categoryImagesListFragment.getAdapter() == null) {
return 0;
}
return categoryImagesListFragment.getAdapter().getCount();
}
@Override
public void notifyDatasetChanged() {
}
@Override
public void registerDataSetObserver(DataSetObserver observer) {
}
@Override
public void unregisterDataSetObserver(DataSetObserver observer) {
}
}

View file

@ -0,0 +1,237 @@
package fr.free.nrw.commons.category;
import android.annotation.SuppressLint;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AbsListView;
import android.widget.AdapterView;
import android.widget.GridView;
import android.widget.ListAdapter;
import android.widget.ProgressBar;
import android.widget.TextView;
import java.util.List;
import java.util.concurrent.TimeUnit;
import javax.inject.Inject;
import javax.inject.Named;
import butterknife.BindView;
import butterknife.ButterKnife;
import dagger.android.support.DaggerFragment;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.utils.NetworkUtils;
import fr.free.nrw.commons.utils.ViewUtil;
import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers;
import timber.log.Timber;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
/**
* Displays images for a particular category with load more on scrolling incorporated
*/
public class CategoryImagesListFragment extends DaggerFragment {
private static int TIMEOUT_SECONDS = 15;
private GridViewAdapter gridAdapter;
@BindView(R.id.statusMessage)
TextView statusTextView;
@BindView(R.id.loadingImagesProgressBar) ProgressBar progressBar;
@BindView(R.id.categoryImagesList) GridView gridView;
private boolean hasMoreImages = true;
private boolean isLoading;
private String categoryName = null;
@Inject CategoryImageController controller;
@Inject @Named("category_prefs") SharedPreferences categoryPreferences;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.fragment_category_images, container, false);
ButterKnife.bind(this, v);
return v;
}
@Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
gridView.setOnItemClickListener((AdapterView.OnItemClickListener) getActivity());
initViews();
}
/**
* Initializes the UI elements for the fragment
* Setup the grid view to and scroll listener for it
*/
private void initViews() {
String categoryName = getArguments().getString("categoryName");
if (getArguments() != null && categoryName != null) {
this.categoryName = categoryName;
resetQueryContinueValues(categoryName);
initList();
setScrollListener();
}
}
/**
* Query continue values determine the last page that was loaded for the particular keyword
* This method resets those values, so that the results can be queried from the first page itself
* @param keyword
*/
private void resetQueryContinueValues(String keyword) {
SharedPreferences.Editor editor = categoryPreferences.edit();
editor.remove(keyword);
editor.apply();
}
/**
* Checks for internet connection and then initializes the grid view with first 10 images of that category
*/
@SuppressLint("CheckResult")
private void initList() {
if(!NetworkUtils.isInternetConnectionEstablished(getContext())) {
handleNoInternet();
return;
}
isLoading = true;
progressBar.setVisibility(VISIBLE);
Observable.fromCallable(() -> controller.getCategoryImages(categoryName))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
.subscribe(this::handleSuccess, this::handleError);
}
/**
* Handles the UI updates for no internet scenario
*/
private void handleNoInternet() {
progressBar.setVisibility(GONE);
if (gridAdapter == null || gridAdapter.isEmpty()) {
statusTextView.setVisibility(VISIBLE);
statusTextView.setText(getString(R.string.no_internet));
} else {
ViewUtil.showSnackbar(gridView, R.string.no_internet);
}
}
/**
* Logs and handles API error scenario
* @param throwable
*/
private void handleError(Throwable throwable) {
Timber.e(throwable, "Error occurred while loading featured images");
initErrorView();
}
/**
* Handles the UI updates for a error scenario
*/
private void initErrorView() {
ViewUtil.showSnackbar(gridView, R.string.error_loading_images);
progressBar.setVisibility(GONE);
if (gridAdapter == null || gridAdapter.isEmpty()) {
statusTextView.setVisibility(VISIBLE);
statusTextView.setText(getString(R.string.no_images_found));
} else {
statusTextView.setVisibility(GONE);
}
}
/**
* Initializes the adapter with a list of Media objects
* @param mediaList
*/
private void setAdapter(List<Media> mediaList) {
gridAdapter = new GridViewAdapter(this.getContext(), R.layout.layout_category_images, mediaList);
gridView.setAdapter(gridAdapter);
}
/**
* Sets the scroll listener for the grid view so that more images are fetched when the user scrolls down
* Checks if the category has more images before loading
* Also checks whether images are currently being fetched before triggering another request
*/
private void setScrollListener() {
gridView.setOnScrollListener(new AbsListView.OnScrollListener() {
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
}
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
if (hasMoreImages && !isLoading && (firstVisibleItem + visibleItemCount + 1 >= totalItemCount)) {
isLoading = true;
fetchMoreImages();
}
}
});
}
/**
* Fetches more images for the category and adds it to the grid view adapter
*/
@SuppressLint("CheckResult")
private void fetchMoreImages() {
if(!NetworkUtils.isInternetConnectionEstablished(getContext())) {
handleNoInternet();
return;
}
progressBar.setVisibility(VISIBLE);
Observable.fromCallable(() -> controller.getCategoryImages(categoryName))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
.subscribe(this::handleSuccess, this::handleError);
}
/**
* Handles the success scenario
* On first load, it initializes the grid view. On subsequent loads, it adds items to the adapter
* @param collection
*/
private void handleSuccess(List<Media> collection) {
if(collection == null || collection.isEmpty()) {
initErrorView();
hasMoreImages = false;
return;
}
if(gridAdapter == null) {
setAdapter(collection);
} else {
gridAdapter.addItems(collection);
}
progressBar.setVisibility(GONE);
isLoading = false;
statusTextView.setVisibility(GONE);
}
public ListAdapter getAdapter() {
return gridView.getAdapter();
}
/**
* This method will be called on back pressed of CategoryImagesActivity.
* It initializes the grid view by setting adapter.
*/
@Override
public void onResume() {
gridView.setAdapter(gridAdapter);
super.onResume();
}
}

View file

@ -0,0 +1,88 @@
package fr.free.nrw.commons.category;
import android.app.Activity;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.TextView;
import java.util.ArrayList;
import java.util.List;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.MediaWikiImageView;
import fr.free.nrw.commons.R;
/**
* This is created to only display UI implementation. Needs to be changed in real implementation
*/
public class GridViewAdapter extends ArrayAdapter {
private Context context;
private List<Media> data;
public GridViewAdapter(Context context, int layoutResourceId, List<Media> data) {
super(context, layoutResourceId, data);
this.context = context;
this.data = data;
}
/**
* Adds more item to the list
* Its triggered on scrolling down in the list
* @param images
*/
public void addItems(List<Media> images) {
if (data == null) {
data = new ArrayList<>();
}
data.addAll(images);
notifyDataSetChanged();
}
@Override
public boolean isEmpty() {
return data == null || data.isEmpty();
}
/**
* Sets up the UI for the category image item
* @param position
* @param convertView
* @param parent
* @return
*/
@Override
public View getView(int position, View convertView, ViewGroup parent) {
if (convertView == null) {
LayoutInflater inflater = ((Activity) context).getLayoutInflater();
convertView = inflater.inflate(R.layout.layout_category_images, null);
}
Media item = data.get(position);
MediaWikiImageView imageView = convertView.findViewById(R.id.categoryImageView);
TextView fileName = convertView.findViewById(R.id.categoryImageTitle);
TextView author = convertView.findViewById(R.id.categoryImageAuthor);
fileName.setText(item.getFilename());
setAuthorView(item, author);
imageView.setMedia(item);
return convertView;
}
/**
* Shows author information if its present
* @param item
* @param author
*/
private void setAuthorView(Media item, TextView author) {
if (item.getCreator() != null && !item.getCreator().equals("")) {
String uploadedByTemplate = context.getString(R.string.image_uploaded_by);
author.setText(String.format(uploadedByTemplate, item.getCreator()));
} else {
author.setVisibility(View.GONE);
}
}
}

View file

@ -0,0 +1,24 @@
package fr.free.nrw.commons.category;
/**
* For APIs that return paginated responses, MediaWiki APIs uses the QueryContinue to facilitate fetching of subsequent pages
* https://www.mediawiki.org/wiki/API:Raw_query_continue
*/
public class QueryContinue {
private String continueParam;
private String gcmContinueParam;
public QueryContinue(String continueParam, String gcmContinueParam) {
this.continueParam = continueParam;
this.gcmContinueParam = gcmContinueParam;
}
public String getGcmContinueParam() {
return gcmContinueParam;
}
public String getContinueParam() {
return continueParam;
}
}

View file

@ -45,6 +45,7 @@ public class Contribution extends Media {
private long transferred; private long transferred;
private String decimalCoords; private String decimalCoords;
private boolean isMultiple; private boolean isMultiple;
private String wikiDataEntityId;
public Contribution(Uri contentUri, String filename, Uri localUri, String imageUrl, Date timestamp, public Contribution(Uri contentUri, String filename, Uri localUri, String imageUrl, Date timestamp,
int state, long dataLength, Date dateUploaded, long transferred, int state, long dataLength, Date dateUploaded, long transferred,
@ -222,4 +223,17 @@ public class Contribution extends Media {
throw new RuntimeException("Unrecognized license value: " + license); throw new RuntimeException("Unrecognized license value: " + license);
} }
public String getWikiDataEntityId() {
return wikiDataEntityId;
}
/**
* When the corresponding wikidata entity is known as in case of nearby uploads, it can be set
* using the setter method
* @param wikiDataEntityId
*/
public void setWikiDataEntityId(String wikiDataEntityId) {
this.wikiDataEntityId = wikiDataEntityId;
}
} }

View file

@ -90,7 +90,7 @@ public class ContributionController {
fragment.startActivityForResult(pickImageIntent, SELECT_FROM_GALLERY); fragment.startActivityForResult(pickImageIntent, SELECT_FROM_GALLERY);
} }
public void handleImagePicked(int requestCode, Intent data, boolean isDirectUpload) { public void handleImagePicked(int requestCode, Intent data, boolean isDirectUpload, String wikiDataEntityId) {
FragmentActivity activity = fragment.getActivity(); FragmentActivity activity = fragment.getActivity();
Timber.d("handleImagePicked() called with onActivityResult()"); Timber.d("handleImagePicked() called with onActivityResult()");
Intent shareIntent = new Intent(activity, ShareActivity.class); Intent shareIntent = new Intent(activity, ShareActivity.class);
@ -102,9 +102,6 @@ public class ContributionController {
shareIntent.setType(activity.getContentResolver().getType(imageData)); shareIntent.setType(activity.getContentResolver().getType(imageData));
shareIntent.putExtra(EXTRA_STREAM, imageData); shareIntent.putExtra(EXTRA_STREAM, imageData);
shareIntent.putExtra(EXTRA_SOURCE, SOURCE_GALLERY); shareIntent.putExtra(EXTRA_SOURCE, SOURCE_GALLERY);
if (isDirectUpload) {
shareIntent.putExtra("isDirectUpload", true);
}
break; break;
case SELECT_FROM_CAMERA: case SELECT_FROM_CAMERA:
//FIXME: Find out appropriate mime type //FIXME: Find out appropriate mime type
@ -113,9 +110,6 @@ public class ContributionController {
shareIntent.setType("image/jpeg"); shareIntent.setType("image/jpeg");
shareIntent.putExtra(EXTRA_STREAM, lastGeneratedCaptureUri); shareIntent.putExtra(EXTRA_STREAM, lastGeneratedCaptureUri);
shareIntent.putExtra(EXTRA_SOURCE, SOURCE_CAMERA); shareIntent.putExtra(EXTRA_SOURCE, SOURCE_CAMERA);
if (isDirectUpload) {
shareIntent.putExtra("isDirectUpload", true);
}
break; break;
default: default:
@ -123,6 +117,10 @@ public class ContributionController {
} }
Timber.i("Image selected"); Timber.i("Image selected");
try { try {
shareIntent.putExtra("isDirectUpload", isDirectUpload);
if (wikiDataEntityId != null && !wikiDataEntityId.equals("")) {
shareIntent.putExtra("wikiDataEntityId", wikiDataEntityId);
}
activity.startActivity(shareIntent); activity.startActivity(shareIntent);
} catch (SecurityException e) { } catch (SecurityException e) {
Timber.e(e, "Security Exception"); Timber.e(e, "Security Exception");

View file

@ -8,7 +8,6 @@ import android.net.Uri;
import android.os.RemoteException; import android.os.RemoteException;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.Log;
import java.util.Date; import java.util.Date;

View file

@ -117,7 +117,7 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment {
if (resultCode == RESULT_OK) { if (resultCode == RESULT_OK) {
Timber.d("OnActivityResult() parameters: Req code: %d Result code: %d Data: %s", Timber.d("OnActivityResult() parameters: Req code: %d Result code: %d Data: %s",
requestCode, resultCode, data); requestCode, resultCode, data);
controller.handleImagePicked(requestCode, data, false); controller.handleImagePicked(requestCode, data, false, null);
} else { } else {
Timber.e("OnActivityResult() parameters: Req code: %d Result code: %d Data: %s", Timber.e("OnActivityResult() parameters: Req code: %d Result code: %d Data: %s",
requestCode, resultCode, data); requestCode, resultCode, data);

View file

@ -83,7 +83,7 @@ public class DeleteTask extends AsyncTask<Void, Integer, Boolean> {
String logPageString = "\n{{Commons:Deletion requests/" + media.getFilename() + String logPageString = "\n{{Commons:Deletion requests/" + media.getFilename() +
"}}\n"; "}}\n";
SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd"); SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd", Locale.getDefault());
String date = sdf.format(calendar.getTime()); String date = sdf.format(calendar.getTime());
String userPageString = "\n{{subst:idw|" + media.getFilename() + String userPageString = "\n{{subst:idw|" + media.getFilename() +

View file

@ -7,6 +7,7 @@ import fr.free.nrw.commons.WelcomeActivity;
import fr.free.nrw.commons.auth.LoginActivity; import fr.free.nrw.commons.auth.LoginActivity;
import fr.free.nrw.commons.auth.SignupActivity; import fr.free.nrw.commons.auth.SignupActivity;
import fr.free.nrw.commons.contributions.ContributionsActivity; import fr.free.nrw.commons.contributions.ContributionsActivity;
import fr.free.nrw.commons.category.CategoryImagesActivity;
import fr.free.nrw.commons.nearby.NearbyActivity; import fr.free.nrw.commons.nearby.NearbyActivity;
import fr.free.nrw.commons.notification.NotificationActivity; import fr.free.nrw.commons.notification.NotificationActivity;
import fr.free.nrw.commons.settings.SettingsActivity; import fr.free.nrw.commons.settings.SettingsActivity;
@ -46,4 +47,7 @@ public abstract class ActivityBuilderModule {
@ContributesAndroidInjector @ContributesAndroidInjector
abstract NotificationActivity bindNotificationActivity(); abstract NotificationActivity bindNotificationActivity();
@ContributesAndroidInjector
abstract CategoryImagesActivity bindFeaturedImagesActivity();
} }

View file

@ -9,17 +9,18 @@ import dagger.android.support.AndroidSupportInjectionModule;
import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.MediaWikiImageView; import fr.free.nrw.commons.MediaWikiImageView;
import fr.free.nrw.commons.auth.LoginActivity; import fr.free.nrw.commons.auth.LoginActivity;
import fr.free.nrw.commons.contributions.Contribution;
import fr.free.nrw.commons.contributions.ContributionsActivity;
import fr.free.nrw.commons.contributions.ContributionsSyncAdapter; import fr.free.nrw.commons.contributions.ContributionsSyncAdapter;
import fr.free.nrw.commons.delete.DeleteTask; import fr.free.nrw.commons.delete.DeleteTask;
import fr.free.nrw.commons.modifications.ModificationsSyncAdapter; import fr.free.nrw.commons.modifications.ModificationsSyncAdapter;
import fr.free.nrw.commons.settings.SettingsFragment;
import fr.free.nrw.commons.nearby.PlaceRenderer; import fr.free.nrw.commons.nearby.PlaceRenderer;
import fr.free.nrw.commons.upload.FileProcessor;
import fr.free.nrw.commons.settings.SettingsFragment;
@Singleton @Singleton
@Component(modules = { @Component(modules = {
CommonsApplicationModule.class, CommonsApplicationModule.class,
NetworkingModule.class,
AndroidInjectionModule.class, AndroidInjectionModule.class,
AndroidSupportInjectionModule.class, AndroidSupportInjectionModule.class,
ActivityBuilderModule.class, ActivityBuilderModule.class,
@ -47,6 +48,8 @@ public interface CommonsApplicationComponent extends AndroidInjector<Application
void inject(PlaceRenderer placeRenderer); void inject(PlaceRenderer placeRenderer);
void inject(FileProcessor fileProcessor);
@Component.Builder @Component.Builder
@SuppressWarnings({"WeakerAccess", "unused"}) @SuppressWarnings({"WeakerAccess", "unused"})
interface Builder { interface Builder {

View file

@ -11,17 +11,16 @@ import javax.inject.Singleton;
import dagger.Module; import dagger.Module;
import dagger.Provides; import dagger.Provides;
import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.auth.AccountUtil; import fr.free.nrw.commons.auth.AccountUtil;
import fr.free.nrw.commons.auth.SessionManager; import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.caching.CacheController;
import fr.free.nrw.commons.data.DBOpenHelper; import fr.free.nrw.commons.data.DBOpenHelper;
import fr.free.nrw.commons.location.LocationServiceManager; import fr.free.nrw.commons.location.LocationServiceManager;
import fr.free.nrw.commons.mwapi.ApacheHttpClientMediaWikiApi;
import fr.free.nrw.commons.mwapi.MediaWikiApi; import fr.free.nrw.commons.mwapi.MediaWikiApi;
import fr.free.nrw.commons.nearby.NearbyPlaces; import fr.free.nrw.commons.nearby.NearbyPlaces;
import fr.free.nrw.commons.upload.UploadController; import fr.free.nrw.commons.upload.UploadController;
import fr.free.nrw.commons.wikidata.WikidataEditListener;
import fr.free.nrw.commons.wikidata.WikidataEditListenerImpl;
import static android.content.Context.MODE_PRIVATE; import static android.content.Context.MODE_PRIVATE;
import static fr.free.nrw.commons.contributions.ContributionsContentProvider.CONTRIBUTION_AUTHORITY; import static fr.free.nrw.commons.contributions.ContributionsContentProvider.CONTRIBUTION_AUTHORITY;
@ -31,7 +30,6 @@ import static fr.free.nrw.commons.modifications.ModificationsContentProvider.MOD
@SuppressWarnings({"WeakerAccess", "unused"}) @SuppressWarnings({"WeakerAccess", "unused"})
public class CommonsApplicationModule { public class CommonsApplicationModule {
public static final String CATEGORY_AUTHORITY = "fr.free.nrw.commons.categories.contentprovider"; public static final String CATEGORY_AUTHORITY = "fr.free.nrw.commons.categories.contentprovider";
public static final long OK_HTTP_CACHE_SIZE = 10 * 1024 * 1024;
private Context applicationContext; private Context applicationContext;
@ -85,6 +83,17 @@ public class CommonsApplicationModule {
return context.getSharedPreferences("prefs", MODE_PRIVATE); return context.getSharedPreferences("prefs", MODE_PRIVATE);
} }
/**
*
* @param context
* @return returns categoryPrefs
*/
@Provides
@Named("category_prefs")
public SharedPreferences providesCategorySharedPreferences(Context context) {
return context.getSharedPreferences("categoryPrefs", MODE_PRIVATE);
}
@Provides @Provides
@Named("direct_nearby_upload_prefs") @Named("direct_nearby_upload_prefs")
public SharedPreferences providesDirectNearbyUploadPreferences(Context context) { public SharedPreferences providesDirectNearbyUploadPreferences(Context context) {
@ -104,24 +113,12 @@ public class CommonsApplicationModule {
return new SessionManager(context, mediaWikiApi, sharedPreferences); return new SessionManager(context, mediaWikiApi, sharedPreferences);
} }
@Provides
@Singleton
public MediaWikiApi provideMediaWikiApi(Context context, @Named("default_preferences") SharedPreferences sharedPreferences) {
return new ApacheHttpClientMediaWikiApi(context, BuildConfig.WIKIMEDIA_API_HOST, sharedPreferences);
}
@Provides @Provides
@Singleton @Singleton
public LocationServiceManager provideLocationServiceManager(Context context) { public LocationServiceManager provideLocationServiceManager(Context context) {
return new LocationServiceManager(context); return new LocationServiceManager(context);
} }
@Provides
@Singleton
public CacheController provideCacheController() {
return new CacheController();
}
@Provides @Provides
@Singleton @Singleton
public DBOpenHelper provideDBOpenHelper(Context context) { public DBOpenHelper provideDBOpenHelper(Context context) {
@ -139,4 +136,10 @@ public class CommonsApplicationModule {
public LruCache<String, String> provideLruCache() { public LruCache<String, String> provideLruCache() {
return new LruCache<>(1024); return new LruCache<>(1024);
} }
@Provides
@Singleton
public WikidataEditListener provideWikidataEditListener() {
return new WikidataEditListenerImpl();
}
} }

View file

@ -4,6 +4,7 @@ import dagger.Module;
import dagger.android.ContributesAndroidInjector; import dagger.android.ContributesAndroidInjector;
import fr.free.nrw.commons.category.CategorizationFragment; import fr.free.nrw.commons.category.CategorizationFragment;
import fr.free.nrw.commons.contributions.ContributionsListFragment; import fr.free.nrw.commons.contributions.ContributionsListFragment;
import fr.free.nrw.commons.category.CategoryImagesListFragment;
import fr.free.nrw.commons.media.MediaDetailFragment; import fr.free.nrw.commons.media.MediaDetailFragment;
import fr.free.nrw.commons.media.MediaDetailPagerFragment; import fr.free.nrw.commons.media.MediaDetailPagerFragment;
import fr.free.nrw.commons.nearby.NearbyListFragment; import fr.free.nrw.commons.nearby.NearbyListFragment;
@ -47,4 +48,7 @@ public abstract class FragmentBuilderModule {
@ContributesAndroidInjector @ContributesAndroidInjector
abstract SingleUploadFragment bindSingleUploadFragment(); abstract SingleUploadFragment bindSingleUploadFragment();
@ContributesAndroidInjector
abstract CategoryImagesListFragment bindFeaturedImagesListFragment();
} }

View file

@ -0,0 +1,59 @@
package fr.free.nrw.commons.di;
import android.content.Context;
import android.content.SharedPreferences;
import android.support.annotation.NonNull;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import javax.inject.Named;
import javax.inject.Singleton;
import dagger.Module;
import dagger.Provides;
import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.mwapi.ApacheHttpClientMediaWikiApi;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import okhttp3.HttpUrl;
import okhttp3.OkHttpClient;
@Module
@SuppressWarnings({"WeakerAccess", "unused"})
public class NetworkingModule {
public static final long OK_HTTP_CACHE_SIZE = 10 * 1024 * 1024;
@Provides
@Singleton
public OkHttpClient provideOkHttpClient() {
return new OkHttpClient.Builder().build();
}
@Provides
@Singleton
public MediaWikiApi provideMediaWikiApi(Context context,
@Named("default_preferences") SharedPreferences defaultPreferences,
@Named("category_prefs") SharedPreferences categoryPrefs,
Gson gson) {
return new ApacheHttpClientMediaWikiApi(context, BuildConfig.WIKIMEDIA_API_HOST, BuildConfig.WIKIDATA_API_HOST, defaultPreferences, categoryPrefs, gson);
}
@Provides
@Named("commons_mediawiki_url")
@NonNull
@SuppressWarnings("ConstantConditions")
public HttpUrl provideMwUrl() {
return HttpUrl.parse(BuildConfig.COMMONS_URL);
}
/**
* Gson objects are very heavy. The app should ideally be using just one instance of it instead of creating new instances everywhere.
* @return returns a singleton Gson instance
*/
@Provides
@Singleton
public Gson provideGson() {
return new GsonBuilder().create();
}
}

View file

@ -0,0 +1,36 @@
package fr.free.nrw.commons.glide;
import android.support.annotation.NonNull;
import com.bumptech.glide.load.Options;
import com.bumptech.glide.load.ResourceDecoder;
import com.bumptech.glide.load.engine.Resource;
import com.bumptech.glide.load.resource.SimpleResource;
import com.caverock.androidsvg.SVG;
import com.caverock.androidsvg.SVGParseException;
import java.io.IOException;
import java.io.InputStream;
/**
* Decodes an SVG internal representation from an {@link InputStream}.
*/
public class SvgDecoder implements ResourceDecoder<InputStream, SVG> {
@Override
public boolean handles(@NonNull InputStream source, @NonNull Options options) {
// TODO: Can we tell?
return true;
}
public Resource<SVG> decode(@NonNull InputStream source, int width, int height,
@NonNull Options options)
throws IOException {
try {
SVG svg = SVG.getFromInputStream(source);
return new SimpleResource<>(svg);
} catch (SVGParseException ex) {
throw new IOException("Cannot load SVG from stream", ex);
}
}
}

View file

@ -0,0 +1,28 @@
package fr.free.nrw.commons.glide;
import android.graphics.Picture;
import android.graphics.drawable.PictureDrawable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import com.bumptech.glide.load.Options;
import com.bumptech.glide.load.engine.Resource;
import com.bumptech.glide.load.resource.SimpleResource;
import com.bumptech.glide.load.resource.transcode.ResourceTranscoder;
import com.caverock.androidsvg.SVG;
/**
* Convert the {@link SVG}'s internal representation to an Android-compatible one
* ({@link Picture}).
*/
public class SvgDrawableTranscoder implements ResourceTranscoder<SVG, PictureDrawable> {
@Nullable
@Override
public Resource<PictureDrawable> transcode(@NonNull Resource<SVG> toTranscode,
@NonNull Options options) {
SVG svg = toTranscode.get();
Picture picture = svg.renderToPicture();
PictureDrawable drawable = new PictureDrawable(picture);
return new SimpleResource<>(drawable);
}
}

View file

@ -0,0 +1,51 @@
package fr.free.nrw.commons.glide;
import android.graphics.drawable.PictureDrawable;
import android.widget.ImageView;
import com.bumptech.glide.load.DataSource;
import com.bumptech.glide.load.engine.GlideException;
import com.bumptech.glide.request.RequestListener;
import com.bumptech.glide.request.target.ImageViewTarget;
import com.bumptech.glide.request.target.Target;
/**
* Listener which updates the {@link ImageView} to be software rendered, because
* {@link com.caverock.androidsvg.SVG SVG}/{@link android.graphics.Picture Picture} can't render on
* a hardware backed {@link android.graphics.Canvas Canvas}.
*/
public class SvgSoftwareLayerSetter implements RequestListener<PictureDrawable> {
/**
* Sets the layer type to none if the load fails
* @param e
* @param model
* @param target
* @param isFirstResource
* @return
*/
@Override
public boolean onLoadFailed(GlideException e, Object model, Target<PictureDrawable> target,
boolean isFirstResource) {
ImageView view = ((ImageViewTarget<?>) target).getView();
view.setLayerType(ImageView.LAYER_TYPE_NONE, null);
return false;
}
/**
* Sets the layer type to software when the resource is ready
* @param resource
* @param model
* @param target
* @param dataSource
* @param isFirstResource
* @return
*/
@Override
public boolean onResourceReady(PictureDrawable resource, Object model,
Target<PictureDrawable> target, DataSource dataSource, boolean isFirstResource) {
ImageView view = ((ImageViewTarget<?>) target).getView();
view.setLayerType(ImageView.LAYER_TYPE_SOFTWARE, null);
return false;
}
}

View file

@ -1,6 +1,7 @@
package fr.free.nrw.commons.location; package fr.free.nrw.commons.location;
import android.Manifest; import android.Manifest;
import android.annotation.SuppressLint;
import android.app.Activity; import android.app.Activity;
import android.content.Context; import android.content.Context;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
@ -10,9 +11,10 @@ import android.location.LocationManager;
import android.os.Bundle; import android.os.Bundle;
import android.support.v4.app.ActivityCompat; import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat; import android.support.v4.content.ContextCompat;
import android.util.Log;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CopyOnWriteArrayList;
import timber.log.Timber; import timber.log.Timber;
@ -29,6 +31,7 @@ public class LocationServiceManager implements LocationListener {
private Location lastLocation; private Location lastLocation;
private final List<LocationUpdateListener> locationListeners = new CopyOnWriteArrayList<>(); private final List<LocationUpdateListener> locationListeners = new CopyOnWriteArrayList<>();
private boolean isLocationManagerRegistered = false; private boolean isLocationManagerRegistered = false;
private Set<Activity> locationExplanationDisplayed = new HashSet<>();
/** /**
* Constructs a new instance of LocationServiceManager. * Constructs a new instance of LocationServiceManager.
@ -51,7 +54,6 @@ public class LocationServiceManager implements LocationListener {
/** /**
* Returns whether the location permission is granted. * Returns whether the location permission is granted.
*
* @return true if the location permission is granted * @return true if the location permission is granted
*/ */
public boolean isLocationPermissionGranted() { public boolean isLocationPermissionGranted() {
@ -73,10 +75,41 @@ public class LocationServiceManager implements LocationListener {
LOCATION_REQUEST); LOCATION_REQUEST);
} }
/**
* The permission explanation dialog box is now displayed just once for a particular activity. We are subscribing
* to updates from multiple providers so its important to show the dialog just once. Otherwise it will be displayed
* once for every provider, which in our case currently is 2.
* @param activity
* @return
*/
public boolean isPermissionExplanationRequired(Activity activity) { public boolean isPermissionExplanationRequired(Activity activity) {
return !activity.isFinishing() && if (activity.isFinishing()) {
ActivityCompat.shouldShowRequestPermissionRationale(activity, return false;
Manifest.permission.ACCESS_FINE_LOCATION); }
boolean showRequestPermissionRationale = ActivityCompat.shouldShowRequestPermissionRationale(activity, Manifest.permission.ACCESS_FINE_LOCATION);
if (showRequestPermissionRationale && !locationExplanationDisplayed.contains(activity)) {
locationExplanationDisplayed.add(activity);
return true;
}
return false;
}
/**
* Gets the last known location in cases where there wasn't time to register a listener
* (e.g. when Location permission just granted)
* @return last known LatLng
*/
@SuppressLint("MissingPermission")
public LatLng getLKL() {
if (isLocationPermissionGranted()) {
Location lastKL = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER);
if (lastKL == null) {
lastKL = locationManager.getLastKnownLocation(LocationManager.NETWORK_PROVIDER);
}
return LatLng.from(lastKL);
} else {
return null;
}
} }
public LatLng getLastLocation() { public LatLng getLastLocation() {
@ -90,9 +123,10 @@ public class LocationServiceManager implements LocationListener {
* Registers a LocationManager to listen for current location. * Registers a LocationManager to listen for current location.
*/ */
public void registerLocationManager() { public void registerLocationManager() {
if (!isLocationManagerRegistered) if (!isLocationManagerRegistered) {
isLocationManagerRegistered = requestLocationUpdatesFromProvider(LocationManager.NETWORK_PROVIDER) isLocationManagerRegistered = requestLocationUpdatesFromProvider(LocationManager.NETWORK_PROVIDER)
&& requestLocationUpdatesFromProvider(LocationManager.GPS_PROVIDER); && requestLocationUpdatesFromProvider(LocationManager.GPS_PROVIDER);
}
} }
/** /**
@ -125,7 +159,7 @@ public class LocationServiceManager implements LocationListener {
* @return LOCATION_SIGNIFICANTLY_CHANGED if location changed significantly * @return LOCATION_SIGNIFICANTLY_CHANGED if location changed significantly
* LOCATION_SLIGHTLY_CHANGED if location changed slightly * LOCATION_SLIGHTLY_CHANGED if location changed slightly
*/ */
protected LocationChangeType isBetterLocation(Location location, Location currentBestLocation) { private LocationChangeType isBetterLocation(Location location, Location currentBestLocation) {
if (currentBestLocation == null) { if (currentBestLocation == null) {
// A new location is always better than no location // A new location is always better than no location
@ -249,6 +283,8 @@ public class LocationServiceManager implements LocationListener {
public enum LocationChangeType{ public enum LocationChangeType{
LOCATION_SIGNIFICANTLY_CHANGED, //Went out of borders of nearby markers LOCATION_SIGNIFICANTLY_CHANGED, //Went out of borders of nearby markers
LOCATION_SLIGHTLY_CHANGED, //User might be walking or driving LOCATION_SLIGHTLY_CHANGED, //User might be walking or driving
LOCATION_NOT_CHANGED LOCATION_NOT_CHANGED,
PERMISSION_JUST_GRANTED,
MAP_UPDATED
} }
} }

View file

@ -9,6 +9,7 @@ import android.os.AsyncTask;
import android.os.Bundle; import android.os.Bundle;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.text.Editable; import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher; import android.text.TextWatcher;
import android.util.TypedValue; import android.util.TypedValue;
import android.view.LayoutInflater; import android.view.LayoutInflater;
@ -22,6 +23,9 @@ import android.widget.ScrollView;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.OnClick;
import java.io.IOException; import java.io.IOException;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.ArrayList; import java.util.ArrayList;
@ -45,19 +49,23 @@ import fr.free.nrw.commons.mwapi.MediaWikiApi;
import fr.free.nrw.commons.ui.widget.CompatTextView; import fr.free.nrw.commons.ui.widget.CompatTextView;
import timber.log.Timber; import timber.log.Timber;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
import static android.widget.Toast.LENGTH_SHORT; import static android.widget.Toast.LENGTH_SHORT;
public class MediaDetailFragment extends CommonsDaggerSupportFragment { public class MediaDetailFragment extends CommonsDaggerSupportFragment {
private boolean editable; private boolean editable;
private boolean isCategoryImage;
private MediaDetailPagerFragment.MediaDetailProvider detailProvider; private MediaDetailPagerFragment.MediaDetailProvider detailProvider;
private int index; private int index;
public static MediaDetailFragment forMedia(int index, boolean editable) { public static MediaDetailFragment forMedia(int index, boolean editable, boolean isCategoryImage) {
MediaDetailFragment mf = new MediaDetailFragment(); MediaDetailFragment mf = new MediaDetailFragment();
Bundle state = new Bundle(); Bundle state = new Bundle();
state.putBoolean("editable", editable); state.putBoolean("editable", editable);
state.putBoolean("isCategoryImage", isCategoryImage);
state.putInt("index", index); state.putInt("index", index);
state.putInt("listIndex", 0); state.putInt("listIndex", 0);
state.putInt("listTop", 0); state.putInt("listTop", 0);
@ -72,21 +80,37 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
@Inject @Inject
MediaWikiApi mwApi; MediaWikiApi mwApi;
private MediaWikiImageView image;
private MediaDetailSpacer spacer;
private int initialListTop = 0; private int initialListTop = 0;
private TextView title; @BindView(R.id.mediaDetailImage)
private TextView desc; MediaWikiImageView image;
private TextView license; @BindView(R.id.mediaDetailSpacer)
private TextView coordinates; MediaDetailSpacer spacer;
private TextView uploadedDate; @BindView(R.id.mediaDetailTitle)
private TextView seeMore; TextView title;
private LinearLayout nominatedforDeletion; @BindView(R.id.mediaDetailDesc)
private LinearLayout categoryContainer; TextView desc;
private Button delete; @BindView(R.id.mediaDetailAuthor)
private ScrollView scrollView; TextView author;
@BindView(R.id.mediaDetailLicense)
TextView license;
@BindView(R.id.mediaDetailCoordinates)
TextView coordinates;
@BindView(R.id.mediaDetailuploadeddate)
TextView uploadedDate;
@BindView(R.id.seeMore)
TextView seeMore;
@BindView(R.id.nominatedDeletionBanner)
LinearLayout nominatedForDeletion;
@BindView(R.id.mediaDetailCategoryContainer)
LinearLayout categoryContainer;
@BindView(R.id.authorLinearLayout)
LinearLayout authorLayout;
@BindView(R.id.nominateDeletion)
Button delete;
@BindView(R.id.mediaDetailScrollView)
ScrollView scrollView;
private ArrayList<String> categoryNames; private ArrayList<String> categoryNames;
private boolean categoriesLoaded = false; private boolean categoriesLoaded = false;
private boolean categoriesPresent = false; private boolean categoriesPresent = false;
@ -96,11 +120,15 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
private AsyncTask<Void, Void, Boolean> detailFetchTask; private AsyncTask<Void, Void, Boolean> detailFetchTask;
private LicenseList licenseList; private LicenseList licenseList;
//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;
@Override @Override
public void onSaveInstanceState(Bundle outState) { public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState); super.onSaveInstanceState(outState);
outState.putInt("index", index); outState.putInt("index", index);
outState.putBoolean("editable", editable); outState.putBoolean("editable", editable);
outState.putBoolean("isCategoryImage", isCategoryImage);
getScrollPosition(); getScrollPosition();
outState.putInt("listTop", initialListTop); outState.putInt("listTop", initialListTop);
@ -116,32 +144,28 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
if (savedInstanceState != null) { if (savedInstanceState != null) {
editable = savedInstanceState.getBoolean("editable"); editable = savedInstanceState.getBoolean("editable");
isCategoryImage = savedInstanceState.getBoolean("isCategoryImage");
index = savedInstanceState.getInt("index"); index = savedInstanceState.getInt("index");
initialListTop = savedInstanceState.getInt("listTop"); initialListTop = savedInstanceState.getInt("listTop");
} else { } else {
editable = getArguments().getBoolean("editable"); editable = getArguments().getBoolean("editable");
isCategoryImage = getArguments().getBoolean("isCategoryImage");
index = getArguments().getInt("index"); index = getArguments().getInt("index");
initialListTop = 0; initialListTop = 0;
} }
categoryNames = new ArrayList<>(); categoryNames = new ArrayList<>();
categoryNames.add(getString(R.string.detail_panel_cats_loading)); categoryNames.add(getString(R.string.detail_panel_cats_loading));
final View view = inflater.inflate(R.layout.fragment_media_detail, container, false); final View view = inflater.inflate(R.layout.fragment_media_detail, container, false);
image = (MediaWikiImageView) view.findViewById(R.id.mediaDetailImage); ButterKnife.bind(this,view);
scrollView = (ScrollView) view.findViewById(R.id.mediaDetailScrollView);
// Detail consists of a list view with main pane in header view, plus category list. if (isCategoryImage){
spacer = (MediaDetailSpacer) view.findViewById(R.id.mediaDetailSpacer); authorLayout.setVisibility(VISIBLE);
title = (TextView) view.findViewById(R.id.mediaDetailTitle); } else {
desc = (TextView) view.findViewById(R.id.mediaDetailDesc); authorLayout.setVisibility(GONE);
license = (TextView) view.findViewById(R.id.mediaDetailLicense); }
coordinates = (TextView) view.findViewById(R.id.mediaDetailCoordinates);
uploadedDate = (TextView) view.findViewById(R.id.mediaDetailuploadeddate);
seeMore = (TextView) view.findViewById(R.id.seeMore);
nominatedforDeletion = (LinearLayout) view.findViewById(R.id.nominatedDeletionBanner);
delete = (Button) view.findViewById(R.id.nominateDeletion);
categoryContainer = (LinearLayout) view.findViewById(R.id.mediaDetailCategoryContainer);
licenseList = new LicenseList(getActivity()); licenseList = new LicenseList(getActivity());
@ -179,7 +203,7 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
@Override @Override
public void onResume() { public void onResume() {
super.onResume(); super.onResume();
Media media = detailProvider.getMediaAtPosition(index); media = detailProvider.getMediaAtPosition(index);
if (media == null) { if (media == null) {
// Ask the detail provider to ping us when we're ready // Ask the detail provider to ping us when we're ready
Timber.d("MediaDetailFragment not yet ready to display details; registering observer"); Timber.d("MediaDetailFragment not yet ready to display details; registering observer");
@ -192,17 +216,18 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
Timber.d("MediaDetailFragment ready to display delayed details!"); Timber.d("MediaDetailFragment ready to display delayed details!");
detailProvider.unregisterDataSetObserver(dataObserver); detailProvider.unregisterDataSetObserver(dataObserver);
dataObserver = null; dataObserver = null;
displayMediaDetails(detailProvider.getMediaAtPosition(index)); media=detailProvider.getMediaAtPosition(index);
displayMediaDetails();
} }
}; };
detailProvider.registerDataSetObserver(dataObserver); detailProvider.registerDataSetObserver(dataObserver);
} else { } else {
Timber.d("MediaDetailFragment ready to display details"); Timber.d("MediaDetailFragment ready to display details");
displayMediaDetails(media); displayMediaDetails();
} }
} }
private void displayMediaDetails(final Media media) { private void displayMediaDetails() {
//Always load image from Internet to allow viewing the desc, license, and cats //Always load image from Internet to allow viewing the desc, license, and cats
image.setMedia(media); image.setMedia(media);
@ -239,7 +264,6 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
if (success) { if (success) {
extractor.fill(media); extractor.fill(media);
setTextFields(media); setTextFields(media);
setOnClickListeners(media);
} else { } else {
Timber.d("Failed to load photo details."); Timber.d("Failed to load photo details.");
} }
@ -290,71 +314,99 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
} }
rebuildCatList(); rebuildCatList();
if(media.getCreator() == null || media.getCreator().equals("")) {
authorLayout.setVisibility(GONE);
} else {
author.setText(media.getCreator());
}
checkDeletion(media); checkDeletion(media);
} }
private void setOnClickListeners(final Media media) { @OnClick(R.id.mediaDetailLicense)
if (licenseLink(media) != null) { public void onMediaDetailLicenceClicked(){
license.setOnClickListener(v -> openWebBrowser(licenseLink(media))); if (!TextUtils.isEmpty(licenseLink(media))) {
openWebBrowser(licenseLink(media));
} else { } else {
Toast toast = Toast.makeText(getContext(), getString(R.string.null_url), Toast.LENGTH_SHORT); if(isCategoryImage) {
toast.show(); Timber.d("Unable to fetch license URL for %s", media.getLicense());
} else {
Toast toast = Toast.makeText(getContext(), getString(R.string.null_url), Toast.LENGTH_SHORT);
toast.show();
}
} }
}
@OnClick(R.id.mediaDetailCoordinates)
public void onMediaDetailCoordinatesClicked(){
if (media.getCoordinates() != null) { if (media.getCoordinates() != null) {
coordinates.setOnClickListener(v -> openMap(media.getCoordinates())); openMap(media.getCoordinates());
} }
if (delete.getVisibility() == View.VISIBLE) { }
delete.setOnClickListener(v -> {
delete.setEnabled(false);
delete.setTextColor(getResources().getColor(R.color.deleteButtonLight));
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, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int whichButton) {
String reason = input.getText().toString();
DeleteTask deleteTask = new DeleteTask(getActivity(), media, reason);
deleteTask.execute();
}
});
alert.setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int 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);
}
}
@Override @OnClick(R.id.nominateDeletion)
public void afterTextChanged(Editable arg0) { public void onDeleteButtonClicked(){
handleText(); //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, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int whichButton) {
String reason = input.getText().toString();
DeleteTask deleteTask = new DeleteTask(getActivity(), media, reason);
deleteTask.execute();
enableDeleteButton(false);
}
});
alert.setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int 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);
}
}
@Override @Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) { public void afterTextChanged(Editable arg0) {
} handleText();
}
@Override @Override
public void onTextChanged(CharSequence s, int start, int before, int count) { public void beforeTextChanged(CharSequence s, int start, int count, int after) {
} }
});
d.show(); @Override
d.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false); public void onTextChanged(CharSequence s, int start, int before, int count) {
}); }
});
d.show();
d.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false);
}
@OnClick(R.id.seeMore)
public void onSeeMoreClicked(){
if(nominatedForDeletion.getVisibility()== VISIBLE) {
openWebBrowser(media.getFilePageTitle().getMobileUri().toString());
} }
if (nominatedforDeletion.getVisibility() == View.VISIBLE){ }
seeMore.setOnClickListener(v -> {
openWebBrowser(media.getFilePageTitle().getMobileUri().toString()); private void enableDeleteButton(boolean visibility) {
}); delete.setEnabled(visibility);
if(visibility) {
delete.setTextColor(getResources().getColor(R.color.primaryTextColor));
} else {
delete.setTextColor(getResources().getColor(R.color.deleteButtonLight));
} }
} }
@ -431,7 +483,7 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
if (date == null || date.toString() == null || date.toString().isEmpty()) { if (date == null || date.toString() == null || date.toString().isEmpty()) {
return "Uploaded date not available"; return "Uploaded date not available";
} }
SimpleDateFormat formatter = new SimpleDateFormat("dd MMM yyyy"); SimpleDateFormat formatter = new SimpleDateFormat("dd MMM yyyy", Locale.getDefault());
return formatter.format(date); return formatter.format(date);
} }
@ -449,12 +501,11 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
private void checkDeletion(Media media){ private void checkDeletion(Media media){
if (media.getRequestedDeletion()){ if (media.getRequestedDeletion()){
delete.setVisibility(View.GONE); delete.setVisibility(GONE);
nominatedforDeletion.setVisibility(View.VISIBLE); nominatedForDeletion.setVisibility(VISIBLE);
} } else if (!isCategoryImage) {
else{ delete.setVisibility(VISIBLE);
delete.setVisibility(View.VISIBLE); nominatedForDeletion.setVisibility(GONE);
nominatedforDeletion.setVisibility(View.GONE);
} }
} }

View file

@ -26,6 +26,8 @@ import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.Toast; import android.widget.Toast;
import butterknife.BindView;
import butterknife.ButterKnife;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Named; import javax.inject.Named;
@ -36,6 +38,8 @@ import fr.free.nrw.commons.contributions.Contribution;
import fr.free.nrw.commons.contributions.ContributionsActivity; import fr.free.nrw.commons.contributions.ContributionsActivity;
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment; import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
import fr.free.nrw.commons.mwapi.MediaWikiApi; import fr.free.nrw.commons.mwapi.MediaWikiApi;
import fr.free.nrw.commons.utils.ImageUtils;
import timber.log.Timber;
import static android.Manifest.permission.READ_EXTERNAL_STORAGE; import static android.Manifest.permission.READ_EXTERNAL_STORAGE;
import static android.content.Context.DOWNLOAD_SERVICE; import static android.content.Context.DOWNLOAD_SERVICE;
@ -53,16 +57,19 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple
@Named("default_preferences") @Named("default_preferences")
SharedPreferences prefs; SharedPreferences prefs;
private ViewPager pager; @BindView(R.id.mediaDetailsPager)
ViewPager pager;
private Boolean editable; private Boolean editable;
private boolean isFeaturedImage;
public MediaDetailPagerFragment() { public MediaDetailPagerFragment() {
this(false); this(false, false);
} }
@SuppressLint("ValidFragment") @SuppressLint("ValidFragment")
public MediaDetailPagerFragment(Boolean editable) { public MediaDetailPagerFragment(Boolean editable, boolean isFeaturedImage) {
this.editable = editable; this.editable = editable;
this.isFeaturedImage = isFeaturedImage;
} }
@Override @Override
@ -70,7 +77,7 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple
ViewGroup container, ViewGroup container,
Bundle savedInstanceState) { Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_media_detail_pager, container, false); View view = inflater.inflate(R.layout.fragment_media_detail_pager, container, false);
pager = (ViewPager) view.findViewById(R.id.mediaDetailsPager); ButterKnife.bind(this,view);
pager.addOnPageChangeListener(this); pager.addOnPageChangeListener(this);
final MediaDetailAdapter adapter = new MediaDetailAdapter(getChildFragmentManager()); final MediaDetailAdapter adapter = new MediaDetailAdapter(getChildFragmentManager());
@ -96,6 +103,7 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple
super.onSaveInstanceState(outState); super.onSaveInstanceState(outState);
outState.putInt("current-page", pager.getCurrentItem()); outState.putInt("current-page", pager.getCurrentItem());
outState.putBoolean("editable", editable); outState.putBoolean("editable", editable);
outState.putBoolean("isFeaturedImage", isFeaturedImage);
} }
@Override @Override
@ -103,6 +111,7 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
if (savedInstanceState != null) { if (savedInstanceState != null) {
editable = savedInstanceState.getBoolean("editable"); editable = savedInstanceState.getBoolean("editable");
isFeaturedImage = savedInstanceState.getBoolean("isFeaturedImage");
} }
setHasOptionsMenu(true); setHasOptionsMenu(true);
} }
@ -133,6 +142,10 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple
// Download // Download
downloadMedia(m); downloadMedia(m);
return true; return true;
case R.id.menu_set_as_wallpaper:
// Set wallpaper
setWallpaper(m);
return true;
case R.id.menu_retry_current_image: case R.id.menu_retry_current_image:
// Retry // Retry
((ContributionsActivity) getActivity()).retryUpload(pager.getCurrentItem()); ((ContributionsActivity) getActivity()).retryUpload(pager.getCurrentItem());
@ -148,6 +161,19 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple
} }
} }
/**
* Set the media as the device's wallpaper if the imageUrl is not null
* Fails silently if setting the wallpaper fails
* @param media
*/
private void setWallpaper(Media media) {
if(media.getImageUrl() == null || media.getImageUrl().isEmpty()) {
Timber.d("Media URL not present");
return;
}
ImageUtils.setWallpaperFromImageUrl(getActivity(), Uri.parse(media.getImageUrl()));
}
/** /**
* Start the media file downloading to the local SD card/storage. * Start the media file downloading to the local SD card/storage.
* The file can then be opened in Gallery or other apps. * The file can then be opened in Gallery or other apps.
@ -291,7 +317,7 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple
// See bug https://code.google.com/p/android/issues/detail?id=27526 // See bug https://code.google.com/p/android/issues/detail?id=27526
pager.postDelayed(() -> getActivity().supportInvalidateOptionsMenu(), 5); pager.postDelayed(() -> getActivity().supportInvalidateOptionsMenu(), 5);
} }
return MediaDetailFragment.forMedia(i, editable); return MediaDetailFragment.forMedia(i, editable, isFeaturedImage);
} }
@Override @Override

View file

@ -9,6 +9,8 @@ import android.support.annotation.VisibleForTesting;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.Log; import android.util.Log;
import com.google.gson.Gson;
import org.apache.http.HttpResponse; import org.apache.http.HttpResponse;
import org.apache.http.conn.ClientConnectionManager; import org.apache.http.conn.ClientConnectionManager;
import org.apache.http.conn.scheme.PlainSocketFactory; import org.apache.http.conn.scheme.PlainSocketFactory;
@ -23,6 +25,8 @@ import org.apache.http.params.CoreProtocolPNames;
import org.apache.http.util.EntityUtils; import org.apache.http.util.EntityUtils;
import org.mediawiki.api.ApiResult; import org.mediawiki.api.ApiResult;
import org.mediawiki.api.MWApi; import org.mediawiki.api.MWApi;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList; import org.w3c.dom.NodeList;
import java.io.IOException; import java.io.IOException;
@ -38,7 +42,10 @@ import java.util.Locale;
import java.util.concurrent.Callable; import java.util.concurrent.Callable;
import fr.free.nrw.commons.BuildConfig; import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.PageTitle; import fr.free.nrw.commons.PageTitle;
import fr.free.nrw.commons.category.CategoryImageUtils;
import fr.free.nrw.commons.category.QueryContinue;
import fr.free.nrw.commons.notification.Notification; import fr.free.nrw.commons.notification.Notification;
import fr.free.nrw.commons.notification.NotificationUtils; import fr.free.nrw.commons.notification.NotificationUtils;
import in.yuvi.http.fluent.Http; import in.yuvi.http.fluent.Http;
@ -46,6 +53,8 @@ import io.reactivex.Observable;
import io.reactivex.Single; import io.reactivex.Single;
import timber.log.Timber; import timber.log.Timber;
import static fr.free.nrw.commons.utils.ContinueUtils.getQueryContinue;
/** /**
* @author Addshore * @author Addshore
*/ */
@ -55,10 +64,18 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
private static final String THUMB_SIZE = "640"; private static final String THUMB_SIZE = "640";
private AbstractHttpClient httpClient; private AbstractHttpClient httpClient;
private MWApi api; private MWApi api;
private MWApi wikidataApi;
private Context context; private Context context;
private SharedPreferences sharedPreferences; private SharedPreferences defaultPreferences;
private SharedPreferences categoryPreferences;
private Gson gson;
public ApacheHttpClientMediaWikiApi(Context context, String apiURL, SharedPreferences sharedPreferences) { public ApacheHttpClientMediaWikiApi(Context context,
String apiURL,
String wikidatApiURL,
SharedPreferences defaultPreferences,
SharedPreferences categoryPreferences,
Gson gson) {
this.context = context; this.context = context;
BasicHttpParams params = new BasicHttpParams(); BasicHttpParams params = new BasicHttpParams();
SchemeRegistry schemeRegistry = new SchemeRegistry(); SchemeRegistry schemeRegistry = new SchemeRegistry();
@ -69,7 +86,10 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
params.setParameter(CoreProtocolPNames.USER_AGENT, getUserAgent()); params.setParameter(CoreProtocolPNames.USER_AGENT, getUserAgent());
httpClient = new DefaultHttpClient(cm, params); httpClient = new DefaultHttpClient(cm, params);
api = new MWApi(apiURL, httpClient); api = new MWApi(apiURL, httpClient);
this.sharedPreferences = sharedPreferences; wikidataApi = new MWApi(wikidatApiURL, httpClient);
this.defaultPreferences = defaultPreferences;
this.categoryPreferences = categoryPreferences;
this.gson = gson;
} }
@Override @Override
@ -160,7 +180,7 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
} }
private void setAuthCookieOnLogin(boolean isLoggedIn) { private void setAuthCookieOnLogin(boolean isLoggedIn) {
SharedPreferences.Editor editor = sharedPreferences.edit(); SharedPreferences.Editor editor = defaultPreferences.edit();
if (isLoggedIn) { if (isLoggedIn) {
editor.putBoolean("isUserLoggedIn", true); editor.putBoolean("isUserLoggedIn", true);
editor.putString("getAuthCookie", api.getAuthCookie()); editor.putString("getAuthCookie", api.getAuthCookie());
@ -191,6 +211,15 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
return api.getEditToken(); return api.getEditToken();
} }
@Override
public String getCentralAuthToken() throws IOException {
String centralAuthToken = api.action("centralauthtoken")
.get()
.getString("/api/centralauthtoken/@centralauthtoken");
Timber.d("MediaWiki Central auth token is %s", centralAuthToken);
return centralAuthToken;
}
@Override @Override
public boolean fileExistsWithName(String fileName) throws IOException { public boolean fileExistsWithName(String fileName) throws IOException {
return api.action("query") return api.action("query")
@ -336,6 +365,98 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
}).flatMapObservable(Observable::fromIterable); }).flatMapObservable(Observable::fromIterable);
} }
/**
* Get the edit token for making wiki data edits
* https://www.mediawiki.org/wiki/API:Tokens
* @return
* @throws IOException
*/
private String getWikidataEditToken() throws IOException {
return wikidataApi.getEditToken();
}
@Override
public String getWikidataCsrfToken() throws IOException {
String wikidataCsrfToken = wikidataApi.action("query")
.param("action", "query")
.param("centralauthtoken", getCentralAuthToken())
.param("meta", "tokens")
.post()
.getString("/api/query/tokens/@csrftoken");
Timber.d("Wikidata csrf token is %s", wikidataCsrfToken);
return wikidataCsrfToken;
}
/**
* Creates a new claim using the wikidata API
* https://www.mediawiki.org/wiki/Wikibase/API
* @param entityId the wikidata entity to be edited
* @param property the property to be edited, for eg P18 for images
* @param snaktype the type of value stored for that property
* @param value the actual value to be stored for the property, for eg filename in case of P18
* @return returns revisionId if the claim is successfully created else returns null
* @throws IOException
*/
@Nullable
@Override
public String wikidatCreateClaim(String entityId, String property, String snaktype, String value) throws IOException {
Timber.d("Filename is %s", value);
ApiResult result = wikidataApi.action("wbcreateclaim")
.param("entity", entityId)
.param("centralauthtoken", getCentralAuthToken())
.param("token", getWikidataCsrfToken())
.param("snaktype", snaktype)
.param("property", property)
.param("value", value)
.post();
if (result == null || result.getNode("api") == null) {
return null;
}
Node node = result.getNode("api").getDocument();
Element element = (Element) node;
if (element != null && element.getAttribute("success").equals("1")) {
return result.getString("api/pageinfo/@lastrevid");
} else {
Timber.e(result.getString("api/error/@code") + " " + result.getString("api/error/@info"));
}
return null;
}
/**
* Adds the wikimedia-commons-app tag to the edits made on wikidata
* @param revisionId
* @return
* @throws IOException
*/
@Nullable
@Override
public boolean addWikidataEditTag(String revisionId) throws IOException {
ApiResult result = wikidataApi.action("tag")
.param("revid", revisionId)
.param("centralauthtoken", getCentralAuthToken())
.param("token", getWikidataCsrfToken())
.param("add", "wikimedia-commons-app")
.param("reason", "Add tag for edits made using Android Commons app")
.post();
if (result == null || result.getNode("api") == null) {
return false;
}
Node node = result.getNode("api").getDocument();
Element element = (Element) node;
if (element != null && element.getAttribute("status").equals("success")) {
return true;
} else {
Timber.e(result.getString("api/error/@code") + " " + result.getString("api/error/@info"));
}
return false;
}
@Override @Override
@NonNull @NonNull
public Observable<String> searchTitles(String title, int searchCatsLimit) { public Observable<String> searchTitles(String title, int searchCatsLimit) {
@ -429,8 +550,8 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
.param("notprop", "list") .param("notprop", "list")
.param("format", "xml") .param("format", "xml")
.param("meta", "notifications") .param("meta", "notifications")
// .param("meta", "notifications")
.param("notformat", "model") .param("notformat", "model")
.param("notwikis", "wikidatawiki|commonswiki|enwiki")
.get() .get()
.getNode("/api/query/notifications/list"); .getNode("/api/query/notifications/list");
} catch (IOException e) { } catch (IOException e) {
@ -448,6 +569,83 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
return NotificationUtils.getNotificationsFromList(context, childNodes); return NotificationUtils.getNotificationsFromList(context, childNodes);
} }
/**
* The method takes categoryName as input and returns a List of Media objects
* It uses the generator query API to get the images in a category, 10 at a time.
* Uses the query continue values for fetching paginated responses
* @param categoryName Category name as defined on commons
* @return
*/
@Override
@NonNull
public List<Media> getCategoryImages(String categoryName) {
ApiResult apiResult = null;
try {
MWApi.RequestBuilder requestBuilder = api.action("query")
.param("generator", "categorymembers")
.param("format", "xml")
.param("gcmtype", "file")
.param("gcmtitle", categoryName)
.param("gcmsort", "timestamp")//property to sort by;timestamp
.param("gcmdir", "desc")//in which direction to sort;descending
.param("prop", "imageinfo")
.param("gcmlimit", "10")
.param("iiprop", "url|extmetadata");
QueryContinue queryContinueValues = getQueryContinueValues(categoryName);
if (queryContinueValues != null) {
requestBuilder.param("continue", queryContinueValues.getContinueParam());
requestBuilder.param("gcmcontinue", queryContinueValues.getGcmContinueParam());
}
apiResult = requestBuilder.get();
} catch (IOException e) {
Timber.e("Failed to obtain searchCategories", e);
}
if (apiResult == null) {
return new ArrayList<>();
}
ApiResult categoryImagesNode = apiResult.getNode("/api/query/pages");
if (categoryImagesNode == null
|| categoryImagesNode.getDocument() == null
|| categoryImagesNode.getDocument().getChildNodes() == null
|| categoryImagesNode.getDocument().getChildNodes().getLength() == 0) {
return new ArrayList<>();
}
QueryContinue queryContinue = getQueryContinue(apiResult.getNode("/api/continue").getDocument());
setQueryContinueValues(categoryName, queryContinue);
NodeList childNodes = categoryImagesNode.getDocument().getChildNodes();
return CategoryImageUtils.getMediaList(childNodes);
}
/**
* For APIs that return paginated responses, MediaWiki APIs uses the QueryContinue to facilitate fetching of subsequent pages
* https://www.mediawiki.org/wiki/API:Raw_query_continue
* After fetching images a page of image for a particular category, shared prefs are updated with the latest QueryContinue Values
* @param keyword
* @param queryContinue
*/
private void setQueryContinueValues(String keyword, QueryContinue queryContinue) {
SharedPreferences.Editor editor = categoryPreferences.edit();
editor.putString(keyword, gson.toJson(queryContinue));
editor.apply();
}
/**
* Before making a paginated API call, this method is called to get the latest query continue values to be used
* @param keyword
* @return
*/
@Nullable
private QueryContinue getQueryContinueValues(String keyword) {
String queryContinueString = categoryPreferences.getString(keyword, null);
return gson.fromJson(queryContinueString, QueryContinue.class);
}
@Override @Override
public boolean existingFile(String fileSha1) throws IOException { public boolean existingFile(String fileSha1) throws IOException {
return api.action("query") return api.action("query")
@ -496,6 +694,7 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
String resultStatus = result.getString("/api/upload/@result"); String resultStatus = result.getString("/api/upload/@result");
if (!resultStatus.equals("Success")) { if (!resultStatus.equals("Success")) {
String errorCode = result.getString("/api/error/@code"); String errorCode = result.getString("/api/error/@code");
Timber.e(errorCode);
return new UploadResult(resultStatus, errorCode); return new UploadResult(resultStatus, errorCode);
} else { } else {
Date dateUploaded = parseMWDate(result.getString("/api/upload/imageinfo/@timestamp")); Date dateUploaded = parseMWDate(result.getString("/api/upload/imageinfo/@timestamp"));

View file

@ -0,0 +1,101 @@
package fr.free.nrw.commons.mwapi;
import com.google.gson.Gson;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import javax.inject.Inject;
import javax.inject.Named;
import fr.free.nrw.commons.mwapi.model.ApiResponse;
import fr.free.nrw.commons.mwapi.model.Page;
import fr.free.nrw.commons.mwapi.model.PageCategory;
import io.reactivex.Single;
import okhttp3.HttpUrl;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
import timber.log.Timber;
/**
* Uses the OkHttp library to implement calls to the Commons MediaWiki API to match GPS coordinates
* with nearby Commons categories. Parses the results using GSON to obtain a list of relevant
* categories. Note: that caller is responsible for executing the request() method on a background
* thread.
*/
public class CategoryApi {
private final OkHttpClient okHttpClient;
private final HttpUrl mwUrl;
private final Gson gson;
@Inject
public CategoryApi(OkHttpClient okHttpClient, Gson gson,
@Named("commons_mediawiki_url") HttpUrl mwUrl) {
this.okHttpClient = okHttpClient;
this.mwUrl = mwUrl;
this.gson = gson;
}
public Single<List<String>> request(String coords) {
return Single.fromCallable(() -> {
HttpUrl apiUrl = buildUrl(coords);
Timber.d("URL: %s", apiUrl.toString());
Request request = new Request.Builder().get().url(apiUrl).build();
Response response = okHttpClient.newCall(request).execute();
ResponseBody body = response.body();
if (body == null) {
return Collections.emptyList();
}
ApiResponse apiResponse = gson.fromJson(body.charStream(), ApiResponse.class);
Set<String> categories = new LinkedHashSet<>();
if (apiResponse != null && apiResponse.hasPages()) {
for (Page page : apiResponse.query.pages) {
for (PageCategory category : page.getCategories()) {
categories.add(category.withoutPrefix());
}
}
}
return new ArrayList<>(categories);
});
}
/**
* Builds URL with image coords for MediaWiki API calls
* Example URL: https://commons.wikimedia.org/w/api.php?action=query&prop=categories|coordinates|pageprops&format=json&clshow=!hidden&coprop=type|name|dim|country|region|globe&codistancefrompoint=38.11386944444445|13.356263888888888&generator=geosearch&redirects=&ggscoord=38.11386944444445|1.356263888888888&ggsradius=100&ggslimit=10&ggsnamespace=6&ggsprop=type|name|dim|country|region|globe&ggsprimary=all&formatversion=2
*
* @param coords Coordinates to build query with
* @return URL for API query
*/
private HttpUrl buildUrl(String coords) {
return mwUrl.newBuilder()
.addPathSegment("w")
.addPathSegment("api.php")
.addQueryParameter("action", "query")
.addQueryParameter("prop", "categories|coordinates|pageprops")
.addQueryParameter("format", "json")
.addQueryParameter("clshow", "!hidden")
.addQueryParameter("coprop", "type|name|dim|country|region|globe")
.addQueryParameter("codistancefrompoint", coords)
.addQueryParameter("generator", "geosearch")
.addQueryParameter("ggscoord", coords)
.addQueryParameter("ggsradius", "10000")
.addQueryParameter("ggslimit", "10")
.addQueryParameter("ggsnamespace", "6")
.addQueryParameter("ggsprop", "type|name|dim|country|region|globe")
.addQueryParameter("ggsprimary", "all")
.addQueryParameter("formatversion", "2")
.build();
}
}

View file

@ -7,6 +7,7 @@ import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.util.List; import java.util.List;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.notification.Notification; import fr.free.nrw.commons.notification.Notification;
import io.reactivex.Observable; import io.reactivex.Observable;
import io.reactivex.Single; import io.reactivex.Single;
@ -26,6 +27,10 @@ public interface MediaWikiApi {
String getEditToken() throws IOException; String getEditToken() throws IOException;
String getWikidataCsrfToken() throws IOException;
String getCentralAuthToken() throws IOException;
boolean fileExistsWithName(String fileName) throws IOException; boolean fileExistsWithName(String fileName) throws IOException;
boolean pageExists(String pageName) throws IOException; boolean pageExists(String pageName) throws IOException;
@ -34,6 +39,8 @@ public interface MediaWikiApi {
boolean logEvents(LogBuilder[] logBuilders); boolean logEvents(LogBuilder[] logBuilders);
List<Media> getCategoryImages(String categoryName);
@NonNull @NonNull
UploadResult uploadFile(String filename, InputStream file, long dataLength, String pageContents, String editSummary, ProgressListener progressListener) throws IOException; UploadResult uploadFile(String filename, InputStream file, long dataLength, String pageContents, String editSummary, ProgressListener progressListener) throws IOException;
@ -46,6 +53,12 @@ public interface MediaWikiApi {
@Nullable @Nullable
String appendEdit(String editToken, String processedPageContent, String filename, String summary) throws IOException; String appendEdit(String editToken, String processedPageContent, String filename, String summary) throws IOException;
@Nullable
String wikidatCreateClaim(String entityId, String property, String snaktype, String value) throws IOException;
@Nullable
boolean addWikidataEditTag(String revisionId) throws IOException;
@NonNull @NonNull
MediaResult fetchMediaByFilename(String filename) throws IOException; MediaResult fetchMediaByFilename(String filename) throws IOException;

View file

@ -0,0 +1,12 @@
package fr.free.nrw.commons.mwapi.model;
public class ApiResponse {
public Query query;
public ApiResponse() {
}
public boolean hasPages() {
return query != null && query.pages != null;
}
}

View file

@ -0,0 +1,17 @@
package fr.free.nrw.commons.mwapi.model;
import android.support.annotation.NonNull;
public class Page {
public String title;
public PageCategory[] categories;
public PageCategory category;
public Page() {
}
@NonNull
public PageCategory[] getCategories() {
return categories != null ? categories : new PageCategory[0];
}
}

View file

@ -0,0 +1,12 @@
package fr.free.nrw.commons.mwapi.model;
public class PageCategory {
public String title;
public PageCategory() {
}
public String withoutPrefix() {
return title != null ? title.replace("Category:", "") : "";
}
}

View file

@ -0,0 +1,10 @@
package fr.free.nrw.commons.mwapi.model;
public class Query {
public Page[] pages;
public Query() {
pages = new Page[0];
}
}

View file

@ -1,6 +1,5 @@
package fr.free.nrw.commons.nearby; package fr.free.nrw.commons.nearby;
import android.content.SharedPreferences;
import android.os.Build; import android.os.Build;
import android.support.v4.app.Fragment; import android.support.v4.app.Fragment;
import android.support.v4.content.ContextCompat; import android.support.v4.content.ContextCompat;

View file

@ -4,23 +4,24 @@ import android.content.BroadcastReceiver;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.IntentFilter; import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.graphics.Typeface;
import android.net.Uri; import android.net.Uri;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.design.widget.BottomSheetBehavior; import android.support.design.widget.BottomSheetBehavior;
import android.support.v4.app.FragmentTransaction; import android.support.v4.app.FragmentTransaction;
import android.support.v7.app.AlertDialog; import android.support.v7.app.AlertDialog;
import android.util.Log;
import android.view.Menu; import android.view.Menu;
import android.view.MenuInflater; import android.view.MenuInflater;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.widget.LinearLayout; import android.widget.LinearLayout;
import android.widget.ProgressBar; import android.widget.ProgressBar;
import android.widget.Toast;
import com.google.gson.Gson; import com.google.gson.Gson;
import com.google.gson.GsonBuilder; import com.google.gson.GsonBuilder;
@ -28,28 +29,34 @@ import com.google.gson.GsonBuilder;
import java.util.List; import java.util.List;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Named;
import butterknife.BindView; import butterknife.BindView;
import butterknife.ButterKnife; import butterknife.ButterKnife;
import fr.free.nrw.commons.R; import fr.free.nrw.commons.R;
import fr.free.nrw.commons.location.LatLng; import fr.free.nrw.commons.location.LatLng;
import fr.free.nrw.commons.location.LocationServiceManager; import fr.free.nrw.commons.location.LocationServiceManager;
import fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType;
import fr.free.nrw.commons.location.LocationUpdateListener; import fr.free.nrw.commons.location.LocationUpdateListener;
import fr.free.nrw.commons.theme.NavigationBaseActivity; import fr.free.nrw.commons.theme.NavigationBaseActivity;
import fr.free.nrw.commons.utils.NetworkUtils; import fr.free.nrw.commons.utils.NetworkUtils;
import fr.free.nrw.commons.utils.UriSerializer; import fr.free.nrw.commons.utils.UriSerializer;
import fr.free.nrw.commons.utils.ViewUtil; import fr.free.nrw.commons.utils.ViewUtil;
import fr.free.nrw.commons.wikidata.WikidataEditListener;
import io.reactivex.Observable; import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable; import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers; import io.reactivex.schedulers.Schedulers;
import timber.log.Timber; import timber.log.Timber;
import uk.co.deanwild.materialshowcaseview.IShowcaseListener;
import uk.co.deanwild.materialshowcaseview.MaterialShowcaseView;
import static fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType.*;
import static fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType.MAP_UPDATED;
public class NearbyActivity extends NavigationBaseActivity implements LocationUpdateListener { public class NearbyActivity extends NavigationBaseActivity implements LocationUpdateListener,
WikidataEditListener.WikidataP18EditListener {
private static final int LOCATION_REQUEST = 1; private static final int LOCATION_REQUEST = 1;
@ -62,13 +69,18 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
LinearLayout bottomSheetDetails; LinearLayout bottomSheetDetails;
@BindView(R.id.transparentView) @BindView(R.id.transparentView)
View transparentView; View transparentView;
@BindView(R.id.fab_recenter)
View fabRecenter;
@Inject @Inject
LocationServiceManager locationManager; LocationServiceManager locationManager;
@Inject @Inject
NearbyController nearbyController; NearbyController nearbyController;
@Inject WikidataEditListener wikidataEditListener;
private LatLng curLatLang; @Inject
@Named("application_preferences") SharedPreferences applicationPrefs;
private LatLng curLatLng;
private Bundle bundle; private Bundle bundle;
private Disposable placesDisposable; private Disposable placesDisposable;
private boolean lockNearbyView; //Determines if the nearby places needs to be refreshed private boolean lockNearbyView; //Determines if the nearby places needs to be refreshed
@ -78,10 +90,18 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
private NearbyListFragment nearbyListFragment; private NearbyListFragment nearbyListFragment;
private static final String TAG_RETAINED_MAP_FRAGMENT = NearbyMapFragment.class.getSimpleName(); private static final String TAG_RETAINED_MAP_FRAGMENT = NearbyMapFragment.class.getSimpleName();
private static final String TAG_RETAINED_LIST_FRAGMENT = NearbyListFragment.class.getSimpleName(); private static final String TAG_RETAINED_LIST_FRAGMENT = NearbyListFragment.class.getSimpleName();
private View listButton; // Reference to list button to use in tutorial
private final String NETWORK_INTENT_ACTION = "android.net.conn.CONNECTIVITY_CHANGE"; private final String NETWORK_INTENT_ACTION = "android.net.conn.CONNECTIVITY_CHANGE";
private BroadcastReceiver broadcastReceiver; private BroadcastReceiver broadcastReceiver;
private boolean isListShowcaseAdded = false;
private boolean isMapShowCaseAdded = false;
private LatLng lastKnownLocation;
private MaterialShowcaseView secondSingleShowCaseView;
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
@ -92,6 +112,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
initBottomSheetBehaviour(); initBottomSheetBehaviour();
initDrawer(); initDrawer();
wikidataEditListener.setAuthenticationStateListener(this);
} }
private void resumeFragment() { private void resumeFragment() {
@ -131,16 +152,55 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
MenuInflater inflater = getMenuInflater(); MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.menu_nearby, menu); inflater.inflate(R.menu.menu_nearby, menu);
new Handler().post(() -> {
listButton = findViewById(R.id.action_display_list);
secondSingleShowCaseView = new MaterialShowcaseView.Builder(this)
.setTarget(listButton)
.setDismissText(getString(R.string.showcase_view_got_it_button))
.setContentText(getString(R.string.showcase_view_list_icon))
.setDelay(500) // optional but starting animations immediately in onCreate can make them choppy
.singleUse(ViewUtil.SHOWCASE_VIEW_ID_1) // provide a unique ID used to ensure it is only shown once
.setDismissStyle(Typeface.defaultFromStyle(Typeface.BOLD))
.setListener(new IShowcaseListener() {
@Override
public void onShowcaseDisplayed(MaterialShowcaseView materialShowcaseView) {
}
// If dismissed, we can inform fragment to start showcase sequence there
@Override
public void onShowcaseDismissed(MaterialShowcaseView materialShowcaseView) {
nearbyMapFragment.onNearbyMaterialShowcaseDismissed();
}
})
.build();
isListShowcaseAdded = true;
if (isMapShowCaseAdded) { // If map showcase is also ready, start ShowcaseSequence
// Probably this case is not possible. Just added to be careful
setMapViewTutorialShowCase();
}
});
return super.onCreateOptionsMenu(menu); return super.onCreateOptionsMenu(menu);
} }
@Override @Override
public boolean onOptionsItemSelected(MenuItem item) { public boolean onOptionsItemSelected(MenuItem item) {
// Handle item selection // Handle item selection
switch (item.getItemId()) { switch (item.getItemId()) {
case R.id.action_display_list: case R.id.action_display_list:
bottomSheetBehaviorForDetails.setState(BottomSheetBehavior.STATE_HIDDEN); if(bottomSheetBehavior.getState()==BottomSheetBehavior.STATE_COLLAPSED || bottomSheetBehavior.getState()==BottomSheetBehavior.STATE_HIDDEN){
bottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED); bottomSheetBehaviorForDetails.setState(BottomSheetBehavior.STATE_HIDDEN);
bottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
}else if(bottomSheetBehavior.getState()==BottomSheetBehavior.STATE_EXPANDED){
bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
}
return true; return true;
default: default:
return super.onOptionsItemSelected(item); return super.onOptionsItemSelected(item);
@ -158,7 +218,11 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
switch (requestCode) { switch (requestCode) {
case LOCATION_REQUEST: { case LOCATION_REQUEST: {
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
refreshView(LocationServiceManager.LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED); Timber.d("Location permission granted, refreshing view");
//Still need to check if GPS is enabled
checkGps();
lastKnownLocation = locationManager.getLKL();
refreshView(PERMISSION_JUST_GRANTED);
} else { } else {
//If permission not granted, go to page that says Nearby Places cannot be displayed //If permission not granted, go to page that says Nearby Places cannot be displayed
hideProgressBar(); hideProgressBar();
@ -218,7 +282,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
private void checkLocationPermission() { private void checkLocationPermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (locationManager.isLocationPermissionGranted()) { if (locationManager.isLocationPermissionGranted()) {
refreshView(LocationServiceManager.LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED); refreshView(LOCATION_SIGNIFICANTLY_CHANGED);
} else { } else {
// Should we show an explanation? // Should we show an explanation?
if (locationManager.isPermissionExplanationRequired(this)) { if (locationManager.isPermissionExplanationRequired(this)) {
@ -244,7 +308,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
} }
} }
} else { } else {
refreshView(LocationServiceManager.LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED); refreshView(LOCATION_SIGNIFICANTLY_CHANGED);
} }
} }
@ -253,7 +317,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
super.onActivityResult(requestCode, resultCode, data); super.onActivityResult(requestCode, resultCode, data);
if (requestCode == 1) { if (requestCode == 1) {
Timber.d("User is back from Settings page"); Timber.d("User is back from Settings page");
refreshView(LocationServiceManager.LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED); refreshView(LOCATION_SIGNIFICANTLY_CHANGED);
} }
} }
@ -261,7 +325,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
protected void onStart() { protected void onStart() {
super.onStart(); super.onStart();
locationManager.addLocationListener(this); locationManager.addLocationListener(this);
locationManager.registerLocationManager(); registerLocationUpdates();
} }
@Override @Override
@ -312,8 +376,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
@Override @Override
public void onReceive(Context context, Intent intent) { public void onReceive(Context context, Intent intent) {
if (NetworkUtils.isInternetConnectionEstablished(NearbyActivity.this)) { if (NetworkUtils.isInternetConnectionEstablished(NearbyActivity.this)) {
refreshView(LocationServiceManager refreshView(LOCATION_SIGNIFICANTLY_CHANGED);
.LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED);
} else { } else {
ViewUtil.showLongToast(NearbyActivity.this, getString(R.string.no_internet)); ViewUtil.showLongToast(NearbyActivity.this, getString(R.string.no_internet));
} }
@ -329,7 +392,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
* *
* @param locationChangeType defines if location shanged significantly or slightly * @param locationChangeType defines if location shanged significantly or slightly
*/ */
private void refreshView(LocationServiceManager.LocationChangeType locationChangeType) { private void refreshView(LocationChangeType locationChangeType) {
if (lockNearbyView) { if (lockNearbyView) {
return; return;
} }
@ -339,38 +402,91 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
return; return;
} }
locationManager.registerLocationManager(); registerLocationUpdates();
LatLng lastLocation = locationManager.getLastLocation(); LatLng lastLocation = locationManager.getLastLocation();
if (curLatLang != null && curLatLang.equals(lastLocation)) { //refresh view only if location has changed if (curLatLng != null && curLatLng.equals(lastLocation)
&& !locationChangeType.equals(MAP_UPDATED)) { //refresh view only if location has changed
return; return;
} }
curLatLang = lastLocation; curLatLng = lastLocation;
if (curLatLang == null) { if (locationChangeType.equals(PERMISSION_JUST_GRANTED)) {
curLatLng = lastKnownLocation;
}
if (curLatLng == null) {
Timber.d("Skipping update of nearby places as location is unavailable"); Timber.d("Skipping update of nearby places as location is unavailable");
return; return;
} }
if (locationChangeType if (locationChangeType.equals(LOCATION_SIGNIFICANTLY_CHANGED)
.equals(LocationServiceManager.LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED)) { || locationChangeType.equals(PERMISSION_JUST_GRANTED)
|| locationChangeType.equals(MAP_UPDATED)) {
progressBar.setVisibility(View.VISIBLE); progressBar.setVisibility(View.VISIBLE);
placesDisposable = Observable.fromCallable(() -> nearbyController
.loadAttractionsFromLocation(curLatLang)) //TODO: This hack inserts curLatLng before populatePlaces is called (see #1440). Ideally a proper fix should be found
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(this::populatePlaces);
} else if (locationChangeType
.equals(LocationServiceManager.LocationChangeType.LOCATION_SLIGHTLY_CHANGED)) {
Gson gson = new GsonBuilder() Gson gson = new GsonBuilder()
.registerTypeAdapter(Uri.class, new UriSerializer()) .registerTypeAdapter(Uri.class, new UriSerializer())
.create(); .create();
String gsonCurLatLng = gson.toJson(curLatLang); String gsonCurLatLng = gson.toJson(curLatLng);
bundle.clear();
bundle.putString("CurLatLng", gsonCurLatLng);
placesDisposable = Observable.fromCallable(() -> nearbyController
.loadAttractionsFromLocation(curLatLng))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(this::populatePlaces,
throwable -> {
Timber.d(throwable);
showErrorMessage(getString(R.string.error_fetching_nearby_places));
progressBar.setVisibility(View.GONE);
});
} else if (locationChangeType
.equals(LOCATION_SLIGHTLY_CHANGED)) {
Gson gson = new GsonBuilder()
.registerTypeAdapter(Uri.class, new UriSerializer())
.create();
String gsonCurLatLng = gson.toJson(curLatLng);
bundle.putString("CurLatLng", gsonCurLatLng); bundle.putString("CurLatLng", gsonCurLatLng);
updateMapFragment(true); updateMapFragment(true);
} }
} }
/**
* This method first checks if the location permissions has been granted and then register the location manager for updates.
*/
private void registerLocationUpdates() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (locationManager.isLocationPermissionGranted()) {
locationManager.registerLocationManager();
} else {
// Should we show an explanation?
if (locationManager.isPermissionExplanationRequired(this)) {
new AlertDialog.Builder(this)
.setMessage(getString(R.string.location_permission_rationale_nearby))
.setPositiveButton("OK", (dialog, which) -> {
requestLocationPermissions();
dialog.dismiss();
})
.setNegativeButton("Cancel", (dialog, id) -> {
showLocationPermissionDeniedErrorDialog();
dialog.cancel();
})
.create()
.show();
} else {
// No explanation needed, we can request the permission.
requestLocationPermissions();
}
}
} else {
locationManager.registerLocationManager();
}
}
private void populatePlaces(NearbyController.NearbyPlacesInfo nearbyPlacesInfo) { private void populatePlaces(NearbyController.NearbyPlacesInfo nearbyPlacesInfo) {
List<Place> placeList = nearbyPlacesInfo.placeList; List<Place> placeList = nearbyPlacesInfo.placeList;
LatLng[] boundaryCoordinates = nearbyPlacesInfo.boundaryCoordinates; LatLng[] boundaryCoordinates = nearbyPlacesInfo.boundaryCoordinates;
@ -378,20 +494,20 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
.registerTypeAdapter(Uri.class, new UriSerializer()) .registerTypeAdapter(Uri.class, new UriSerializer())
.create(); .create();
String gsonPlaceList = gson.toJson(placeList); String gsonPlaceList = gson.toJson(placeList);
String gsonCurLatLng = gson.toJson(curLatLang); String gsonCurLatLng = gson.toJson(curLatLng);
String gsonBoundaryCoordinates = gson.toJson(boundaryCoordinates); String gsonBoundaryCoordinates = gson.toJson(boundaryCoordinates);
if (placeList.size() == 0) { if (placeList.size() == 0) {
ViewUtil.showSnackbar(findViewById(R.id.container), R.string.no_nearby); ViewUtil.showSnackbar(findViewById(R.id.container), R.string.no_nearby);
} }
bundle.clear();
bundle.putString("PlaceList", gsonPlaceList); bundle.putString("PlaceList", gsonPlaceList);
bundle.putString("CurLatLng", gsonCurLatLng); //bundle.putString("CurLatLng", gsonCurLatLng);
bundle.putString("BoundaryCoord", gsonBoundaryCoordinates); bundle.putString("BoundaryCoord", gsonBoundaryCoordinates);
// First time to init fragments // First time to init fragments
if (nearbyMapFragment == null) { if (nearbyMapFragment == null) {
Timber.d("Init map fragment for the first time");
lockNearbyView(true); lockNearbyView(true);
setMapFragment(); setMapFragment();
setListFragment(); setListFragment();
@ -399,9 +515,49 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
lockNearbyView(false); lockNearbyView(false);
} else { } else {
// There are fragments, just update the map and list // There are fragments, just update the map and list
Timber.d("Map fragment already exists, just update the map and list");
updateMapFragment(false); updateMapFragment(false);
updateListFragment(); updateListFragment();
} }
isMapShowCaseAdded = true;
}
public void setMapViewTutorialShowCase() {
/*
*This showcase view will be the first step of our nearbyMaterialShowcaseSequence. The reason we use a
* single item instead of adding another step to nearbyMaterialShowcaseSequence is that we are not able to
* call withoutShape() method on steps. For mapView we need an showcase view without
* any circle on it, it should cover the whole page.
* */
MaterialShowcaseView firstSingleShowCaseView = new MaterialShowcaseView.Builder(this)
.setTarget(nearbyMapFragment.mapView)
.setDismissText(getString(R.string.showcase_view_got_it_button))
.setContentText(getString(R.string.showcase_view_whole_nearby_activity))
.setDelay(500) // optional but starting animations immediately in onCreate can make them choppy
.singleUse(ViewUtil.SHOWCASE_VIEW_ID_2) // provide a unique ID used to ensure it is only shown once
.withoutShape() // no shape on map view since there are no view to focus on
.setDismissStyle(Typeface.defaultFromStyle(Typeface.BOLD))
.setListener(new IShowcaseListener() {
@Override
public void onShowcaseDisplayed(MaterialShowcaseView materialShowcaseView) {
}
@Override
public void onShowcaseDismissed(MaterialShowcaseView materialShowcaseView) {
/* Add other nearbyMaterialShowcaseSequence here, it will make the user feel as they are a
* nearbyMaterialShowcaseSequence whole together.
* */
secondSingleShowCaseView.show(NearbyActivity.this);
}
})
.build();
if (applicationPrefs.getBoolean("firstRunNearby", true)) {
applicationPrefs.edit().putBoolean("firstRunNearby", false).apply();
firstSingleShowCaseView.show(this);
}
} }
private void lockNearbyView(boolean lock) { private void lockNearbyView(boolean lock) {
@ -411,7 +567,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
locationManager.removeLocationListener(this); locationManager.removeLocationListener(this);
} else { } else {
lockNearbyView = false; lockNearbyView = false;
locationManager.registerLocationManager(); registerLocationUpdates();
locationManager.addLocationListener(this); locationManager.addLocationListener(this);
} }
} }
@ -457,34 +613,39 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
NearbyMapFragment nearbyMapFragment = getMapFragment(); NearbyMapFragment nearbyMapFragment = getMapFragment();
if (nearbyMapFragment != null && curLatLang != null) { if (nearbyMapFragment != null && curLatLng != null) {
hideProgressBar(); // In case it is visible (this happens, not an impossible case) hideProgressBar(); // In case it is visible (this happens, not an impossible case)
/* /*
* If we are close to nearby places boundaries, we need a significant update to * If we are close to nearby places boundaries, we need a significant update to
* get new nearby places. Check order is south, north, west, east * get new nearby places. Check order is south, north, west, east
* */ * */
if (nearbyMapFragment.boundaryCoordinates != null if (nearbyMapFragment.boundaryCoordinates != null
&& (curLatLang.getLatitude() <= nearbyMapFragment.boundaryCoordinates[0].getLatitude() && (curLatLng.getLatitude() <= nearbyMapFragment.boundaryCoordinates[0].getLatitude()
|| curLatLang.getLatitude() >= nearbyMapFragment.boundaryCoordinates[1].getLatitude() || curLatLng.getLatitude() >= nearbyMapFragment.boundaryCoordinates[1].getLatitude()
|| curLatLang.getLongitude() <= nearbyMapFragment.boundaryCoordinates[2].getLongitude() || curLatLng.getLongitude() <= nearbyMapFragment.boundaryCoordinates[2].getLongitude()
|| curLatLang.getLongitude() >= nearbyMapFragment.boundaryCoordinates[3].getLongitude())) { || curLatLng.getLongitude() >= nearbyMapFragment.boundaryCoordinates[3].getLongitude())) {
// populate places // populate places
placesDisposable = Observable.fromCallable(() -> nearbyController placesDisposable = Observable.fromCallable(() -> nearbyController
.loadAttractionsFromLocation(curLatLang)) .loadAttractionsFromLocation(curLatLng))
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe(this::populatePlaces); .subscribe(this::populatePlaces,
nearbyMapFragment.setArguments(bundle); throwable -> {
Timber.d(throwable);
showErrorMessage(getString(R.string.error_fetching_nearby_places));
progressBar.setVisibility(View.GONE);
});
nearbyMapFragment.setBundleForUpdtes(bundle);
nearbyMapFragment.updateMapSignificantly(); nearbyMapFragment.updateMapSignificantly();
updateListFragment(); updateListFragment();
return; return;
} }
if (isSlightUpdate) { if (isSlightUpdate) {
nearbyMapFragment.setArguments(bundle); nearbyMapFragment.setBundleForUpdtes(bundle);
nearbyMapFragment.updateMapSlightly(); nearbyMapFragment.updateMapSlightly();
} else { } else {
nearbyMapFragment.setArguments(bundle); nearbyMapFragment.setBundleForUpdtes(bundle);
nearbyMapFragment.updateMapSignificantly(); nearbyMapFragment.updateMapSignificantly();
updateListFragment(); updateListFragment();
} }
@ -498,7 +659,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
} }
private void updateListFragment() { private void updateListFragment() {
nearbyListFragment.setArguments(bundle); nearbyListFragment.setBundleForUpdates(bundle);
nearbyListFragment.updateNearbyListSignificantly(); nearbyListFragment.updateNearbyListSignificantly();
} }
@ -528,15 +689,24 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
@Override @Override
public void onLocationChangedSignificantly(LatLng latLng) { public void onLocationChangedSignificantly(LatLng latLng) {
refreshView(LocationServiceManager.LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED); refreshView(LOCATION_SIGNIFICANTLY_CHANGED);
} }
@Override @Override
public void onLocationChangedSlightly(LatLng latLng) { public void onLocationChangedSlightly(LatLng latLng) {
refreshView(LocationServiceManager.LocationChangeType.LOCATION_SLIGHTLY_CHANGED); refreshView(LOCATION_SLIGHTLY_CHANGED);
} }
public void prepareViewsForSheetPosition(int bottomSheetState) { public void prepareViewsForSheetPosition(int bottomSheetState) {
// TODO // TODO
} }
private void showErrorMessage(String message) {
ViewUtil.showLongToast(NearbyActivity.this, message);
}
@Override
public void onWikidataEditSuccessful() {
refreshView(MAP_UPDATED);
}
} }

View file

@ -7,6 +7,7 @@ import android.support.graphics.drawable.VectorDrawableCompat;
import com.mapbox.mapboxsdk.annotations.IconFactory; import com.mapbox.mapboxsdk.annotations.IconFactory;
import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
@ -44,7 +45,7 @@ public class NearbyController {
* @return NearbyPlacesInfo a variable holds Place list without distance information * @return NearbyPlacesInfo a variable holds Place list without distance information
* and boundary coordinates of current Place List * and boundary coordinates of current Place List
*/ */
public NearbyPlacesInfo loadAttractionsFromLocation(LatLng curLatLng) { public NearbyPlacesInfo loadAttractionsFromLocation(LatLng curLatLng) throws IOException {
Timber.d("Loading attractions near %s", curLatLng); Timber.d("Loading attractions near %s", curLatLng);
NearbyPlacesInfo nearbyPlacesInfo = new NearbyPlacesInfo(); NearbyPlacesInfo nearbyPlacesInfo = new NearbyPlacesInfo();

View file

@ -2,6 +2,7 @@ package fr.free.nrw.commons.nearby;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
@ -21,6 +22,9 @@ import java.lang.reflect.Type;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import javax.inject.Inject;
import javax.inject.Named;
import dagger.android.support.AndroidSupportInjection; import dagger.android.support.AndroidSupportInjection;
import dagger.android.support.DaggerFragment; import dagger.android.support.DaggerFragment;
import fr.free.nrw.commons.R; import fr.free.nrw.commons.R;
@ -33,6 +37,8 @@ import static android.app.Activity.RESULT_OK;
import static android.content.pm.PackageManager.PERMISSION_GRANTED; import static android.content.pm.PackageManager.PERMISSION_GRANTED;
public class NearbyListFragment extends DaggerFragment { public class NearbyListFragment extends DaggerFragment {
private Bundle bundleForUpdates; // Carry information from activity about changed nearby places and current location
private static final Type LIST_TYPE = new TypeToken<List<Place>>() { private static final Type LIST_TYPE = new TypeToken<List<Place>>() {
}.getType(); }.getType();
private static final Type CUR_LAT_LNG_TYPE = new TypeToken<LatLng>() { private static final Type CUR_LAT_LNG_TYPE = new TypeToken<LatLng>() {
@ -45,6 +51,11 @@ public class NearbyListFragment extends DaggerFragment {
private RecyclerView recyclerView; private RecyclerView recyclerView;
private ContributionController controller; private ContributionController controller;
@Inject
@Named("direct_nearby_upload_prefs")
SharedPreferences directPrefs;
@Override @Override
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
@ -80,9 +91,11 @@ public class NearbyListFragment extends DaggerFragment {
} }
public void updateNearbyListSignificantly() { public void updateNearbyListSignificantly() {
Bundle bundle = this.getArguments(); try {
adapterFactory.updateAdapterData(getPlaceListFromBundle(bundle), adapterFactory.updateAdapterData(getPlaceListFromBundle(bundleForUpdates), (RVRendererAdapter<Place>) recyclerView.getAdapter());
(RVRendererAdapter<Place>) recyclerView.getAdapter()); } catch (NullPointerException e) {
Timber.e("Null pointer exception from calling recyclerView.getAdapter()");
}
} }
private List<Place> getPlaceListFromBundle(Bundle bundle) { private List<Place> getPlaceListFromBundle(Bundle bundle) {
@ -133,11 +146,15 @@ public class NearbyListFragment extends DaggerFragment {
if (resultCode == RESULT_OK) { if (resultCode == RESULT_OK) {
Timber.d("OnActivityResult() parameters: Req code: %d Result code: %d Data: %s", Timber.d("OnActivityResult() parameters: Req code: %d Result code: %d Data: %s",
requestCode, resultCode, data); requestCode, resultCode, data);
controller.handleImagePicked(requestCode, data, true); controller.handleImagePicked(requestCode, data, true, directPrefs.getString("WikiDataEntityId", null));
} else { } else {
Timber.e("OnActivityResult() parameters: Req code: %d Result code: %d Data: %s", Timber.e("OnActivityResult() parameters: Req code: %d Result code: %d Data: %s",
requestCode, resultCode, data); requestCode, resultCode, data);
} }
} }
public void setBundleForUpdates(Bundle bundleForUpdates) {
this.bundleForUpdates = bundleForUpdates;
}
} }

View file

@ -7,6 +7,7 @@ import android.content.Intent;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.graphics.Color; import android.graphics.Color;
import android.graphics.Typeface;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
@ -58,13 +59,14 @@ import fr.free.nrw.commons.contributions.ContributionController;
import fr.free.nrw.commons.utils.UriDeserializer; import fr.free.nrw.commons.utils.UriDeserializer;
import fr.free.nrw.commons.utils.ViewUtil; import fr.free.nrw.commons.utils.ViewUtil;
import timber.log.Timber; import timber.log.Timber;
import uk.co.deanwild.materialshowcaseview.MaterialShowcaseView;
import static android.app.Activity.RESULT_OK; import static android.app.Activity.RESULT_OK;
import static android.content.pm.PackageManager.PERMISSION_GRANTED; import static android.content.pm.PackageManager.PERMISSION_GRANTED;
public class NearbyMapFragment extends DaggerFragment { public class NearbyMapFragment extends DaggerFragment {
private MapView mapView; public MapView mapView;
private List<NearbyBaseMarker> baseMarkerOptions; private List<NearbyBaseMarker> baseMarkerOptions;
private fr.free.nrw.commons.location.LatLng curLatLng; private fr.free.nrw.commons.location.LatLng curLatLng;
public fr.free.nrw.commons.location.LatLng[] boundaryCoordinates; public fr.free.nrw.commons.location.LatLng[] boundaryCoordinates;
@ -111,6 +113,12 @@ public class NearbyMapFragment extends DaggerFragment {
private final double CAMERA_TARGET_SHIFT_FACTOR_PORTRAIT = 0.06; private final double CAMERA_TARGET_SHIFT_FACTOR_PORTRAIT = 0.06;
private final double CAMERA_TARGET_SHIFT_FACTOR_LANDSCAPE = 0.04; private final double CAMERA_TARGET_SHIFT_FACTOR_LANDSCAPE = 0.04;
private boolean isSecondMaterialShowcaseDismissed;
private boolean isMapReady;
private MaterialShowcaseView thirdSingleShowCaseView;
private Bundle bundleForUpdtes;// Carry information from activity about changed nearby places and current location
@Inject @Inject
@Named("prefs") @Named("prefs")
SharedPreferences prefs; SharedPreferences prefs;
@ -124,6 +132,7 @@ public class NearbyMapFragment extends DaggerFragment {
@Override @Override
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
Timber.d("Nearby map fragment created");
controller = new ContributionController(this); controller = new ContributionController(this);
directUpload = new DirectUpload(this, controller); directUpload = new DirectUpload(this, controller);
@ -149,17 +158,20 @@ public class NearbyMapFragment extends DaggerFragment {
getActivity()); getActivity());
boundaryCoordinates = gson.fromJson(gsonBoundaryCoordinates, gsonBoundaryCoordinatesType); boundaryCoordinates = gson.fromJson(gsonBoundaryCoordinates, gsonBoundaryCoordinatesType);
} }
Mapbox.getInstance(getActivity(), if (curLatLng != null) {
getString(R.string.mapbox_commons_app_token)); Mapbox.getInstance(getActivity(),
MapboxTelemetry.getInstance().setTelemetryEnabled(false); getString(R.string.mapbox_commons_app_token));
MapboxTelemetry.getInstance().setTelemetryEnabled(false);
}
setRetainInstance(true); setRetainInstance(true);
} }
@Override @Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) { Bundle savedInstanceState) {
Timber.d("onCreateView called");
if (curLatLng != null) { if (curLatLng != null) {
Timber.d("curLatLng found, setting up map view...");
setupMapView(savedInstanceState); setupMapView(savedInstanceState);
} }
@ -192,14 +204,12 @@ public class NearbyMapFragment extends DaggerFragment {
} }
public void updateMapSlightly() { public void updateMapSlightly() {
// Get arguments from bundle for new location
Bundle bundle = this.getArguments();
if (mapboxMap != null) { if (mapboxMap != null) {
Gson gson = new GsonBuilder() Gson gson = new GsonBuilder()
.registerTypeAdapter(Uri.class, new UriDeserializer()) .registerTypeAdapter(Uri.class, new UriDeserializer())
.create(); .create();
if (bundle != null) { if (bundleForUpdtes != null) {
String gsonLatLng = bundle.getString("CurLatLng"); String gsonLatLng = bundleForUpdtes.getString("CurLatLng");
Type curLatLngType = new TypeToken<fr.free.nrw.commons.location.LatLng>() {}.getType(); Type curLatLngType = new TypeToken<fr.free.nrw.commons.location.LatLng>() {}.getType();
curLatLng = gson.fromJson(gsonLatLng, curLatLngType); curLatLng = gson.fromJson(gsonLatLng, curLatLngType);
} }
@ -209,17 +219,15 @@ public class NearbyMapFragment extends DaggerFragment {
} }
public void updateMapSignificantly() { public void updateMapSignificantly() {
Bundle bundle = this.getArguments();
if (mapboxMap != null) { if (mapboxMap != null) {
if (bundle != null) { if (bundleForUpdtes != null) {
Gson gson = new GsonBuilder() Gson gson = new GsonBuilder()
.registerTypeAdapter(Uri.class, new UriDeserializer()) .registerTypeAdapter(Uri.class, new UriDeserializer())
.create(); .create();
String gsonPlaceList = bundle.getString("PlaceList"); String gsonPlaceList = bundleForUpdtes.getString("PlaceList");
String gsonLatLng = bundle.getString("CurLatLng"); String gsonLatLng = bundleForUpdtes.getString("CurLatLng");
String gsonBoundaryCoordinates = bundle.getString("BoundaryCoord"); String gsonBoundaryCoordinates = bundleForUpdtes.getString("BoundaryCoord");
Type listType = new TypeToken<List<Place>>() {}.getType(); Type listType = new TypeToken<List<Place>>() {}.getType();
List<Place> placeList = gson.fromJson(gsonPlaceList, listType); List<Place> placeList = gson.fromJson(gsonPlaceList, listType);
Type curLatLngType = new TypeToken<fr.free.nrw.commons.location.LatLng>() {}.getType(); Type curLatLngType = new TypeToken<fr.free.nrw.commons.location.LatLng>() {}.getType();
@ -457,6 +465,8 @@ public class NearbyMapFragment extends DaggerFragment {
private void setupMapView(Bundle savedInstanceState) { private void setupMapView(Bundle savedInstanceState) {
MapboxMapOptions options = new MapboxMapOptions() MapboxMapOptions options = new MapboxMapOptions()
.compassGravity(Gravity.BOTTOM | Gravity.LEFT)
.compassMargins(new int[]{12, 0, 0, 24})
.styleUrl(Style.OUTDOORS) .styleUrl(Style.OUTDOORS)
.logoEnabled(false) .logoEnabled(false)
.attributionEnabled(false) .attributionEnabled(false)
@ -471,6 +481,7 @@ public class NearbyMapFragment extends DaggerFragment {
mapView.getMapAsync(new OnMapReadyCallback() { mapView.getMapAsync(new OnMapReadyCallback() {
@Override @Override
public void onMapReady(MapboxMap mapboxMap) { public void onMapReady(MapboxMap mapboxMap) {
((NearbyActivity)getActivity()).setMapViewTutorialShowCase();
NearbyMapFragment.this.mapboxMap = mapboxMap; NearbyMapFragment.this.mapboxMap = mapboxMap;
updateMapSignificantly(); updateMapSignificantly();
} }
@ -514,6 +525,7 @@ public class NearbyMapFragment extends DaggerFragment {
private void addNearbyMarkerstoMapBoxMap() { private void addNearbyMarkerstoMapBoxMap() {
mapboxMap.addMarkers(baseMarkerOptions); mapboxMap.addMarkers(baseMarkerOptions);
mapboxMap.setOnInfoWindowCloseListener(marker -> { mapboxMap.setOnInfoWindowCloseListener(marker -> {
if (marker == selected) { if (marker == selected) {
bottomSheetDetailsBehavior.setState(BottomSheetBehavior.STATE_HIDDEN); bottomSheetDetailsBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
@ -529,6 +541,7 @@ public class NearbyMapFragment extends DaggerFragment {
}); });
mapboxMap.setOnMarkerClickListener(marker -> { mapboxMap.setOnMarkerClickListener(marker -> {
if (marker instanceof NearbyMarker) { if (marker instanceof NearbyMarker) {
this.selected = marker; this.selected = marker;
NearbyMarker nearbyMarker = (NearbyMarker) marker; NearbyMarker nearbyMarker = (NearbyMarker) marker;
@ -536,6 +549,7 @@ public class NearbyMapFragment extends DaggerFragment {
passInfoToSheet(place); passInfoToSheet(place);
bottomSheetListBehavior.setState(BottomSheetBehavior.STATE_HIDDEN); bottomSheetListBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
bottomSheetDetailsBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED); bottomSheetDetailsBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
} }
return false; return false;
}); });
@ -629,7 +643,19 @@ public class NearbyMapFragment extends DaggerFragment {
addAnchorToSmallFABs(fabGallery, getActivity().findViewById(R.id.empty_view).getId()); addAnchorToSmallFABs(fabGallery, getActivity().findViewById(R.id.empty_view).getId());
addAnchorToSmallFABs(fabCamera, getActivity().findViewById(R.id.empty_view1).getId()); addAnchorToSmallFABs(fabCamera, getActivity().findViewById(R.id.empty_view1).getId());
thirdSingleShowCaseView = new MaterialShowcaseView.Builder(this.getActivity())
.setTarget(fabPlus)
.setDismissText(getString(R.string.showcase_view_got_it_button))
.setContentText(getString(R.string.showcase_view_plus_fab))
.setDelay(500) // optional but starting animations immediately in onCreate can make them choppy
.singleUse(ViewUtil.SHOWCASE_VIEW_ID_3) // provide a unique ID used to ensure it is only shown once
.setDismissStyle(Typeface.defaultFromStyle(Typeface.BOLD))
.build();
isMapReady = true;
if (isSecondMaterialShowcaseDismissed) {
thirdSingleShowCaseView.show(getActivity());
}
} }
@ -666,7 +692,7 @@ public class NearbyMapFragment extends DaggerFragment {
directionsButton.setOnClickListener(view -> { directionsButton.setOnClickListener(view -> {
//Open map app at given position //Open map app at given position
Intent mapIntent = new Intent(Intent.ACTION_VIEW, place.location.getGmmIntentUri()); Intent mapIntent = new Intent(Intent.ACTION_VIEW, this.place.location.getGmmIntentUri());
if (mapIntent.resolveActivity(getActivity().getPackageManager()) != null) { if (mapIntent.resolveActivity(getActivity().getPackageManager()) != null) {
startActivity(mapIntent); startActivity(mapIntent);
} }
@ -705,6 +731,7 @@ public class NearbyMapFragment extends DaggerFragment {
editor.putString("Title", place.getName()); editor.putString("Title", place.getName());
editor.putString("Desc", place.getLongDescription()); editor.putString("Desc", place.getLongDescription());
editor.putString("Category", place.getCategory()); editor.putString("Category", place.getCategory());
editor.putString("WikiDataEntityId", place.getWikiDataEntityId());
editor.apply(); editor.apply();
} }
@ -740,7 +767,7 @@ public class NearbyMapFragment extends DaggerFragment {
if (resultCode == RESULT_OK) { if (resultCode == RESULT_OK) {
Timber.d("OnActivityResult() parameters: Req code: %d Result code: %d Data: %s", Timber.d("OnActivityResult() parameters: Req code: %d Result code: %d Data: %s",
requestCode, resultCode, data); requestCode, resultCode, data);
controller.handleImagePicked(requestCode, data, true); controller.handleImagePicked(requestCode, data, true, directPrefs.getString("WikiDataEntityId", null));
} else { } else {
Timber.e("OnActivityResult() parameters: Req code: %d Result code: %d Data: %s", Timber.e("OnActivityResult() parameters: Req code: %d Result code: %d Data: %s",
requestCode, resultCode, data); requestCode, resultCode, data);
@ -771,7 +798,7 @@ public class NearbyMapFragment extends DaggerFragment {
} }
} }
private void closeFabs ( boolean isFabOpen){ private void closeFabs ( boolean isFabOpen){
if (isFabOpen) { if (isFabOpen) {
fabPlus.startAnimation(rotate_backward); fabPlus.startAnimation(rotate_backward);
fabCamera.startAnimation(fab_close); fabCamera.startAnimation(fab_close);
@ -782,6 +809,18 @@ public class NearbyMapFragment extends DaggerFragment {
} }
} }
public void setBundleForUpdtes(Bundle bundleForUpdtes) {
this.bundleForUpdtes = bundleForUpdtes;
}
public void onNearbyMaterialShowcaseDismissed() {
isSecondMaterialShowcaseDismissed = true;
if (isMapReady) {
thirdSingleShowCaseView.show(getActivity());
}
}
@Override @Override
public void onStart() { public void onStart() {
if (mapView != null) { if (mapView != null) {

View file

@ -0,0 +1,18 @@
package fr.free.nrw.commons.nearby;
import android.app.Activity;
import uk.co.deanwild.materialshowcaseview.MaterialShowcaseSequence;
import uk.co.deanwild.materialshowcaseview.ShowcaseConfig;
public class NearbyMaterialShowcaseSequence extends MaterialShowcaseSequence {
public NearbyMaterialShowcaseSequence(Activity activity, String sequenceID) {
super(activity, sequenceID);
ShowcaseConfig config = new ShowcaseConfig();
config.setDelay(500); // half second between each showcase view
this.setConfig(config);
this.singleUse(sequenceID); // Display tutorial only once
}
}

View file

@ -17,7 +17,7 @@ import java.util.regex.Pattern;
import fr.free.nrw.commons.Utils; import fr.free.nrw.commons.Utils;
import fr.free.nrw.commons.location.LatLng; import fr.free.nrw.commons.location.LatLng;
import fr.free.nrw.commons.utils.FileUtils; import fr.free.nrw.commons.upload.FileUtils;
import timber.log.Timber; import timber.log.Timber;
public class NearbyPlaces { public class NearbyPlaces {
@ -40,10 +40,9 @@ public class NearbyPlaces {
} }
} }
List<Place> getFromWikidataQuery(LatLng curLatLng, String lang) { List<Place> getFromWikidataQuery(LatLng curLatLng, String lang) throws IOException {
List<Place> places = Collections.emptyList(); List<Place> places = Collections.emptyList();
try {
// increase the radius gradually to find a satisfactory number of nearby places // increase the radius gradually to find a satisfactory number of nearby places
while (radius <= MAX_RADIUS) { while (radius <= MAX_RADIUS) {
places = getFromWikidataQuery(curLatLng, lang, radius); places = getFromWikidataQuery(curLatLng, lang, radius);
@ -54,13 +53,6 @@ public class NearbyPlaces {
radius *= RADIUS_MULTIPLIER; radius *= RADIUS_MULTIPLIER;
} }
} }
} catch (IOException e) {
Timber.d(e.toString());
// errors tend to be caused by too many results (and time out)
// try a small radius next time
Timber.d("back to initial radius: %f", radius);
radius = INITIAL_RADIUS;
}
// make sure we will be able to send at least one request next time // make sure we will be able to send at least one request next time
if (radius > MAX_RADIUS) { if (radius > MAX_RADIUS) {
radius = MAX_RADIUS; radius = MAX_RADIUS;

View file

@ -3,6 +3,7 @@ package fr.free.nrw.commons.nearby;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.net.Uri; import android.net.Uri;
import android.support.annotation.DrawableRes; import android.support.annotation.DrawableRes;
import android.support.annotation.Nullable;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
@ -50,6 +51,20 @@ public class Place {
this.distance = distance; this.distance = distance;
} }
/**
* Extracts the entity id from the wikidata link
* @return returns the entity id if wikidata link exists
*/
@Nullable
public String getWikiDataEntityId() {
if (!hasWikidataLink()) {
return null;
}
String wikiDataLink = siteLinks.getWikidataLink().toString();
return wikiDataLink.replace("http://www.wikidata.org/entity/", "");
}
public boolean hasWikipediaLink() { public boolean hasWikipediaLink() {
return !(siteLinks == null || Uri.EMPTY.equals(siteLinks.getWikipediaLink())); return !(siteLinks == null || Uri.EMPTY.equals(siteLinks.getWikipediaLink()));
} }

View file

@ -25,7 +25,6 @@ import javax.inject.Named;
import butterknife.BindView; import butterknife.BindView;
import butterknife.ButterKnife; import butterknife.ButterKnife;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.R; import fr.free.nrw.commons.R;
import fr.free.nrw.commons.Utils; import fr.free.nrw.commons.Utils;
import fr.free.nrw.commons.contributions.ContributionController; import fr.free.nrw.commons.contributions.ContributionController;

View file

@ -16,7 +16,6 @@ import android.widget.RelativeLayout;
import com.pedrogomez.renderers.RVRendererAdapter; import com.pedrogomez.renderers.RVRendererAdapter;
import java.lang.ref.WeakReference;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
@ -26,6 +25,7 @@ import butterknife.BindView;
import butterknife.ButterKnife; import butterknife.ButterKnife;
import fr.free.nrw.commons.R; import fr.free.nrw.commons.R;
import fr.free.nrw.commons.Utils; import fr.free.nrw.commons.Utils;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import fr.free.nrw.commons.theme.NavigationBaseActivity; import fr.free.nrw.commons.theme.NavigationBaseActivity;
import fr.free.nrw.commons.utils.NetworkUtils; import fr.free.nrw.commons.utils.NetworkUtils;
import fr.free.nrw.commons.utils.ViewUtil; import fr.free.nrw.commons.utils.ViewUtil;
@ -46,6 +46,8 @@ public class NotificationActivity extends NavigationBaseActivity {
@BindView(R.id.container) RelativeLayout relativeLayout; @BindView(R.id.container) RelativeLayout relativeLayout;
@Inject NotificationController controller; @Inject NotificationController controller;
@Inject
MediaWikiApi mediaWikiApi;
private static final String TAG_NOTIFICATION_WORKER_FRAGMENT = "NotificationWorkerFragment"; private static final String TAG_NOTIFICATION_WORKER_FRAGMENT = "NotificationWorkerFragment";
private NotificationWorkerFragment mNotificationWorkerFragment; private NotificationWorkerFragment mNotificationWorkerFragment;
@ -81,7 +83,6 @@ public class NotificationActivity extends NavigationBaseActivity {
} }
} }
@SuppressLint("CheckResult") @SuppressLint("CheckResult")
private void addNotifications() { private void addNotifications() {
Timber.d("Add notifications"); Timber.d("Add notifications");

View file

@ -1,6 +1,7 @@
package fr.free.nrw.commons.notification; package fr.free.nrw.commons.notification;
import android.util.Log; import android.graphics.drawable.PictureDrawable;
import android.text.Html;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
@ -8,17 +9,23 @@ import android.widget.ImageView;
import android.widget.TextView; import android.widget.TextView;
import com.borjabravo.readmoretextview.ReadMoreTextView; import com.borjabravo.readmoretextview.ReadMoreTextView;
import com.bumptech.glide.RequestBuilder;
import com.pedrogomez.renderers.Renderer; import com.pedrogomez.renderers.Renderer;
import butterknife.BindView; import butterknife.BindView;
import butterknife.ButterKnife; import butterknife.ButterKnife;
import fr.free.nrw.commons.R; import fr.free.nrw.commons.R;
import fr.free.nrw.commons.glide.SvgSoftwareLayerSetter;
import static com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions.withCrossFade;
/** /**
* Created by root on 19.12.2017. * Created by root on 19.12.2017.
*/ */
public class NotificationRenderer extends Renderer<Notification> { public class NotificationRenderer extends Renderer<Notification> {
private RequestBuilder<PictureDrawable> requestBuilder;
@BindView(R.id.title) ReadMoreTextView title; @BindView(R.id.title) ReadMoreTextView title;
@BindView(R.id.time) TextView time; @BindView(R.id.time) TextView time;
@BindView(R.id.icon) ImageView icon; @BindView(R.id.icon) ImageView icon;
@ -41,23 +48,32 @@ public class NotificationRenderer extends Renderer<Notification> {
protected View inflate(LayoutInflater layoutInflater, ViewGroup viewGroup) { protected View inflate(LayoutInflater layoutInflater, ViewGroup viewGroup) {
View inflatedView = layoutInflater.inflate(R.layout.item_notification, viewGroup, false); View inflatedView = layoutInflater.inflate(R.layout.item_notification, viewGroup, false);
ButterKnife.bind(this, inflatedView); ButterKnife.bind(this, inflatedView);
requestBuilder = GlideApp.with(inflatedView.getContext())
.as(PictureDrawable.class)
.error(R.drawable.round_icon_unknown)
.transition(withCrossFade())
.listener(new SvgSoftwareLayerSetter());
return inflatedView; return inflatedView;
} }
@Override @Override
public void render() { public void render() {
Notification notification = getContent(); Notification notification = getContent();
String str = notification.notificationText.trim(); setTitle(notification.notificationText);
str = str.concat(" ");
title.setText(str);
time.setText(notification.date); time.setText(notification.date);
switch (notification.notificationType) { requestBuilder.load(notification.iconUrl).into(icon);
case THANK_YOU_EDIT: }
icon.setImageResource(R.drawable.ic_edit_black_24dp);
break; /**
default: * Cleans up the notification text and sets it as the title
icon.setImageResource(R.drawable.round_icon_unknown); * Clean up is required to fix escaped HTML string and extra white spaces at the beginning of the notification
} * @param notificationText
*/
private void setTitle(String notificationText) {
notificationText = notificationText.trim().replaceAll("(^\\h*)|(\\h*$)", "");
notificationText = Html.fromHtml(notificationText).toString();
notificationText = notificationText.concat(" ");
title.setText(notificationText);
} }
public interface NotificationClicked{ public interface NotificationClicked{

View file

@ -16,12 +16,13 @@ import javax.annotation.Nullable;
import fr.free.nrw.commons.BuildConfig; import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.R; import fr.free.nrw.commons.R;
import static fr.free.nrw.commons.notification.NotificationType.THANK_YOU_EDIT;
import static fr.free.nrw.commons.notification.NotificationType.UNKNOWN; import static fr.free.nrw.commons.notification.NotificationType.UNKNOWN;
public class NotificationUtils { public class NotificationUtils {
private static final String COMMONS_WIKI = "commonswiki"; private static final String COMMONS_WIKI = "commonswiki";
private static final String WIKIDATA_WIKI = "wikidatawiki";
private static final String WIKIPEDIA_WIKI = "enwiki";
public static boolean isCommonsNotification(Node document) { public static boolean isCommonsNotification(Node document) {
if (document == null || !document.hasAttributes()) { if (document == null || !document.hasAttributes()) {
@ -31,6 +32,32 @@ public class NotificationUtils {
return COMMONS_WIKI.equals(element.getAttribute("wiki")); return COMMONS_WIKI.equals(element.getAttribute("wiki"));
} }
/**
* Returns true if the wiki attribute corresponds to wikidatawiki
* @param document
* @return
*/
public static boolean isWikidataNotification(Node document) {
if (document == null || !document.hasAttributes()) {
return false;
}
Element element = (Element) document;
return WIKIDATA_WIKI.equals(element.getAttribute("wiki"));
}
/**
* Returns true if the wiki attribute corresponds to enwiki
* @param document
* @return
*/
public static boolean isWikipediaNotification(Node document) {
if (document == null || !document.hasAttributes()) {
return false;
}
Element element = (Element) document;
return WIKIPEDIA_WIKI.equals(element.getAttribute("wiki"));
}
public static NotificationType getNotificationType(Node document) { public static NotificationType getNotificationType(Node document) {
Element element = (Element) document; Element element = (Element) document;
String type = element.getAttribute("type"); String type = element.getAttribute("type");
@ -68,10 +95,17 @@ public class NotificationUtils {
return notifications; return notifications;
} }
/**
* Currently the app is interested in showing notifications just from the following three wikis: commons, wikidata, wikipedia
* This function returns true only if the notification belongs to any of the above wikis and is of a known notification type
* @param node
* @return
*/
private static boolean isUsefulNotification(Node node) { private static boolean isUsefulNotification(Node node) {
return isCommonsNotification(node) return (isCommonsNotification(node)
&& !getNotificationType(node).equals(UNKNOWN) || isWikidataNotification(node)
&& !getNotificationType(node).equals(THANK_YOU_EDIT); || isWikipediaNotification(node))
&& !getNotificationType(node).equals(UNKNOWN);
} }
public static boolean isBundledNotification(Node document) { public static boolean isBundledNotification(Node document) {
@ -97,7 +131,7 @@ public class NotificationUtils {
switch (type) { switch (type) {
case THANK_YOU_EDIT: case THANK_YOU_EDIT:
notificationText = context.getString(R.string.notifications_thank_you_edit); notificationText = getThankYouEditDescription(document);
break; break;
case EDIT_USER_TALK: case EDIT_USER_TALK:
notificationText = getNotificationText(document); notificationText = getNotificationText(document);
@ -146,6 +180,16 @@ public class NotificationUtils {
return body != null ? body.getTextContent() : ""; return body != null ? body.getTextContent() : "";
} }
/**
* Gets the header node returned in the XML document to form the description for thank you edits
* @param document
* @return
*/
private static String getThankYouEditDescription(Node document) {
Node body = getNode(getModel(document), "header");
return body != null ? body.getTextContent() : "";
}
private static String getNotificationIconUrl(Node document) { private static String getNotificationIconUrl(Node document) {
String format = "%s%s"; String format = "%s%s";
Node iconUrl = getNode(getModel(document), "iconUrl"); Node iconUrl = getNode(getModel(document), "iconUrl");

View file

@ -0,0 +1,35 @@
package fr.free.nrw.commons.notification;
import android.content.Context;
import android.graphics.drawable.PictureDrawable;
import android.support.annotation.NonNull;
import com.bumptech.glide.Glide;
import com.bumptech.glide.Registry;
import com.bumptech.glide.annotation.GlideModule;
import com.bumptech.glide.module.AppGlideModule;
import com.caverock.androidsvg.SVG;
import java.io.InputStream;
import fr.free.nrw.commons.glide.SvgDecoder;
import fr.free.nrw.commons.glide.SvgDrawableTranscoder;
/**
* Module for the SVG sample app.
*/
@GlideModule
public class SvgModule extends AppGlideModule {
@Override
public void registerComponents(@NonNull Context context, @NonNull Glide glide,
@NonNull Registry registry) {
registry.register(SVG.class, PictureDrawable.class, new SvgDrawableTranscoder())
.append(InputStream.class, SVG.class, new SvgDecoder());
}
// Disable manifest parsing to avoid adding similar modules twice.
@Override
public boolean isManifestParsingEnabled() {
return false;
}
}

View file

@ -3,13 +3,10 @@ package fr.free.nrw.commons.settings;
import android.Manifest; import android.Manifest;
import android.app.AlertDialog; import android.app.AlertDialog;
import android.content.ActivityNotFoundException; import android.content.ActivityNotFoundException;
import android.content.ComponentName;
import android.content.Context; import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent; import android.content.Intent;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.net.Uri; import android.net.Uri;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
@ -24,8 +21,6 @@ import android.support.v4.content.FileProvider;
import android.widget.Toast; import android.widget.Toast;
import java.io.File; import java.io.File;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Named; import javax.inject.Named;
@ -35,7 +30,7 @@ import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.R; import fr.free.nrw.commons.R;
import fr.free.nrw.commons.Utils; import fr.free.nrw.commons.Utils;
import fr.free.nrw.commons.di.ApplicationlessInjection; import fr.free.nrw.commons.di.ApplicationlessInjection;
import fr.free.nrw.commons.utils.FileUtils; import fr.free.nrw.commons.upload.FileUtils;
public class SettingsFragment extends PreferenceFragment { public class SettingsFragment extends PreferenceFragment {
@ -102,6 +97,11 @@ public class SettingsFragment extends PreferenceFragment {
return true; return true;
}); });
Preference betaTesterPreference = findPreference("becomeBetaTester");
betaTesterPreference.setOnPreferenceClickListener(preference -> {
Utils.handleWebUrl(getActivity(),Uri.parse(getResources().getString(R.string.beta_opt_in_link)));
return true;
});
Preference sendLogsPreference = findPreference("sendLogFile"); Preference sendLogsPreference = findPreference("sendLogFile");
sendLogsPreference.setOnPreferenceClickListener(preference -> { sendLogsPreference.setOnPreferenceClickListener(preference -> {
//first we need to check if we have the necessary permissions //first we need to check if we have the necessary permissions
@ -128,8 +128,8 @@ public class SettingsFragment extends PreferenceFragment {
@Override @Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults); super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == REQUEST_CODE_WRITE_EXTERNAL_STORAGE) { if (requestCode == REQUEST_CODE_WRITE_EXTERNAL_STORAGE && grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { {
sendAppLogsViaEmail(); sendAppLogsViaEmail();
} }
} }

View file

@ -23,11 +23,11 @@ import fr.free.nrw.commons.AboutActivity;
import fr.free.nrw.commons.BuildConfig; import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.R; import fr.free.nrw.commons.R;
import fr.free.nrw.commons.Utils;
import fr.free.nrw.commons.WelcomeActivity; import fr.free.nrw.commons.WelcomeActivity;
import fr.free.nrw.commons.auth.AccountUtil; import fr.free.nrw.commons.auth.AccountUtil;
import fr.free.nrw.commons.auth.LoginActivity; import fr.free.nrw.commons.auth.LoginActivity;
import fr.free.nrw.commons.contributions.ContributionsActivity; import fr.free.nrw.commons.contributions.ContributionsActivity;
import fr.free.nrw.commons.category.CategoryImagesActivity;
import fr.free.nrw.commons.nearby.NearbyActivity; import fr.free.nrw.commons.nearby.NearbyActivity;
import fr.free.nrw.commons.notification.NotificationActivity; import fr.free.nrw.commons.notification.NotificationActivity;
import fr.free.nrw.commons.settings.SettingsActivity; import fr.free.nrw.commons.settings.SettingsActivity;
@ -36,6 +36,8 @@ import timber.log.Timber;
public abstract class NavigationBaseActivity extends BaseActivity public abstract class NavigationBaseActivity extends BaseActivity
implements NavigationView.OnNavigationItemSelectedListener { implements NavigationView.OnNavigationItemSelectedListener {
private static final String FEATURED_IMAGES_CATEGORY = "Category:Featured_pictures_on_Wikimedia_Commons";
@BindView(R.id.toolbar) @BindView(R.id.toolbar)
Toolbar toolbar; Toolbar toolbar;
@BindView(R.id.navigation_view) @BindView(R.id.navigation_view)
@ -154,6 +156,10 @@ public abstract class NavigationBaseActivity extends BaseActivity
drawerLayout.closeDrawer(navigationView); drawerLayout.closeDrawer(navigationView);
NotificationActivity.startYourself(this); NotificationActivity.startYourself(this);
return true; return true;
case R.id.action_featured_images:
drawerLayout.closeDrawer(navigationView);
CategoryImagesActivity.startYourself(this, getString(R.string.title_activity_featured_images), FEATURED_IMAGES_CATEGORY);
return true;
default: default:
Timber.e("Unknown option [%s] selected from the navigation menu", itemId); Timber.e("Unknown option [%s] selected from the navigation menu", itemId);
return false; return false;

View file

@ -1,10 +1,8 @@
package fr.free.nrw.commons.upload; package fr.free.nrw.commons.upload;
import android.app.Activity; import android.app.Activity;
import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.graphics.BitmapRegionDecoder; import android.graphics.BitmapRegionDecoder;
import android.net.Uri;
import android.os.AsyncTask; import android.os.AsyncTask;
import android.support.v7.app.AlertDialog; import android.support.v7.app.AlertDialog;

View file

@ -0,0 +1,263 @@
package fr.free.nrw.commons.upload;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.ContentResolver;
import android.content.Context;
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.ParcelFileDescriptor;
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.lang.ref.WeakReference;
import java.util.Date;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Named;
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
*/
public class FileProcessor implements SimilarImageDialogFragment.onResponse {
@Inject
CacheController cacheController;
@Inject
GpsCategoryModel gpsCategoryModel;
@Inject
CategoryApi apiCall;
@Inject
@Named("default_preferences")
SharedPreferences prefs;
private Uri mediaUri;
private ContentResolver contentResolver;
private GPSExtractor imageObj;
private Context context;
private String decimalCoords;
private boolean haveCheckedForOtherImages = false;
private String filePath;
private boolean useExtStorage;
private boolean cacheFound;
private GPSExtractor tempImageObj;
FileProcessor(Uri mediaUri, ContentResolver contentResolver, Context context) {
this.mediaUri = mediaUri;
this.contentResolver = contentResolver;
this.context = context;
ApplicationlessInjection.getInstance(context.getApplicationContext()).getCommonsApplicationComponent().inject(this);
useExtStorage = prefs.getBoolean("useExternalStorage", true);
}
/**
* Gets file path from media URI.
* In older devices getPath() may fail depending on the source URI, creating and using a copy of the file seems to work instead.
*
* @return file path of media
*/
@Nullable
private String getPathOfMediaOrCopy() {
filePath = FileUtils.getPath(context, mediaUri);
Timber.d("Filepath: " + filePath);
if (filePath == null) {
String copyPath = null;
try {
ParcelFileDescriptor descriptor = contentResolver.openFileDescriptor(mediaUri, "r");
if (descriptor != null) {
if (useExtStorage) {
copyPath = FileUtils.createCopyPath(descriptor);
return copyPath;
}
copyPath = getApplicationContext().getCacheDir().getAbsolutePath() + "/" + new Date().getTime() + ".jpg";
FileUtils.copy(descriptor.getFileDescriptor(), copyPath);
Timber.d("Filepath (copied): %s", copyPath);
return copyPath;
}
} catch (IOException e) {
Timber.w(e, "Error in file " + copyPath);
return null;
}
}
return filePath;
}
/**
* Processes file coordinates, either from EXIF data or user location
*
* @param gpsEnabled if true use GPS
*/
GPSExtractor processFileCoordinates(boolean gpsEnabled) {
Timber.d("Calling GPSExtractor");
try {
ParcelFileDescriptor descriptor = contentResolver.openFileDescriptor(mediaUri, "r");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
if (descriptor != null) {
imageObj = new GPSExtractor(descriptor.getFileDescriptor(), context, prefs);
}
} else {
String filePath = getPathOfMediaOrCopy();
if (filePath != null) {
imageObj = new GPSExtractor(filePath, context, prefs);
}
}
decimalCoords = imageObj.getCoords(gpsEnabled);
if (decimalCoords == null || !imageObj.imageCoordsExists) {
//Find other photos taken around the same time which has gps coordinates
if (!haveCheckedForOtherImages)
findOtherImages(gpsEnabled);// Do not do repeat the process
} else {
useImageCoords();
}
} catch (FileNotFoundException e) {
Timber.w("File not found: " + mediaUri, e);
}
return imageObj;
}
String getDecimalCoords() {
return decimalCoords;
}
/**
* Find other images around the same location that were taken within the last 20 sec
*
* @param gpsEnabled True if GPS is enabled
*/
private void findOtherImages(boolean gpsEnabled) {
Timber.d("filePath" + getPathOfMediaOrCopy());
long timeOfCreation = new File(filePath).lastModified();//Time when the original image was created
File folder = new File(filePath.substring(0, filePath.lastIndexOf('/')));
File[] files = folder.listFiles();
Timber.d("folderTime Number:" + files.length);
for (File file : files) {
if (file.lastModified() - timeOfCreation <= (120 * 1000) && file.lastModified() - timeOfCreation >= -(120 * 1000)) {
//Make sure the photos were taken within 20seconds
Timber.d("fild date:" + file.lastModified() + " time of creation" + timeOfCreation);
tempImageObj = null;//Temporary GPSExtractor to extract coords from these photos
ParcelFileDescriptor descriptor = null;
try {
descriptor = contentResolver.openFileDescriptor(Uri.parse(file.getAbsolutePath()), "r");
} catch (FileNotFoundException e) {
e.printStackTrace();
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
if (descriptor != null) {
tempImageObj = new GPSExtractor(descriptor.getFileDescriptor(), context, prefs);
}
} else {
if (filePath != null) {
tempImageObj = new GPSExtractor(file.getAbsolutePath(), context, prefs);
}
}
if (tempImageObj != null) {
Timber.d("not null fild EXIF" + tempImageObj.imageCoordsExists + " coords" + tempImageObj.getCoords(gpsEnabled));
if (tempImageObj.getCoords(gpsEnabled) != null && tempImageObj.imageCoordsExists) {
// Current image has gps coordinates and it's not current gps locaiton
Timber.d("This file has image coords:" + file.getAbsolutePath());
SimilarImageDialogFragment newFragment = new SimilarImageDialogFragment();
Bundle args = new Bundle();
args.putString("originalImagePath", filePath);
args.putString("possibleImagePath", file.getAbsolutePath());
newFragment.setArguments(args);
newFragment.show(((AppCompatActivity) context).getSupportFragmentManager(), "dialog");
break;
}
}
}
}
haveCheckedForOtherImages = true; //Finished checking for other images
}
/**
* Initiates retrieval of image coordinates or user coordinates, and caching of coordinates.
* Then initiates the calls to MediaWiki API through an instance of CategoryApi.
*/
@SuppressLint("CheckResult")
public void useImageCoords() {
if (decimalCoords != null) {
Timber.d("Decimal coords of image: %s", decimalCoords);
Timber.d("is EXIF data present:" + imageObj.imageCoordsExists + " from findOther image");
// Only set cache for this point if image has coords
if (imageObj.imageCoordsExists) {
double decLongitude = imageObj.getDecLongitude();
double decLatitude = imageObj.getDecLatitude();
cacheController.setQtPoint(decLongitude, decLatitude);
}
List<String> displayCatList = cacheController.findCategory();
boolean catListEmpty = displayCatList.isEmpty();
// If no categories found in cache, call MediaWiki API to match image coords with nearby Commons categories
if (catListEmpty) {
cacheFound = false;
apiCall.request(decimalCoords)
.subscribeOn(Schedulers.io())
.observeOn(Schedulers.io())
.subscribe(
gpsCategoryModel::setCategoryList,
throwable -> {
Timber.e(throwable);
gpsCategoryModel.clear();
}
);
Timber.d("displayCatList size 0, calling MWAPI %s", displayCatList);
} else {
cacheFound = true;
Timber.d("Cache found, setting categoryList in model to %s", displayCatList);
gpsCategoryModel.setCategoryList(displayCatList);
}
} else {
Timber.d("EXIF: no coords");
}
}
boolean isCacheFound() {
return cacheFound;
}
/**
* Calls the async task that detects if image is fuzzy, too dark, etc
*/
void detectUnwantedPictures() {
String imageMediaFilePath = FileUtils.getPath(context, mediaUri);
DetectUnwantedPicturesAsync detectUnwantedPicturesAsync
= new DetectUnwantedPicturesAsync(new WeakReference<Activity>((Activity) context), imageMediaFilePath);
detectUnwantedPicturesAsync.execute();
}
@Override
public void onPositiveResponse() {
imageObj = tempImageObj;
decimalCoords = imageObj.getCoords(false);// Not necessary to use gps as image already ha EXIF data
Timber.d("EXIF from tempImageObj");
useImageCoords();
}
@Override
public void onNegativeResponse() {
Timber.d("EXIF from imageObj");
useImageCoords();
}
}

View file

@ -15,18 +15,84 @@ import android.provider.MediaStore;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import java.io.BufferedReader;
import java.io.File; import java.io.File;
import java.io.FileDescriptor; import java.io.FileDescriptor;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.math.BigInteger;
import java.nio.channels.FileChannel; import java.nio.channels.FileChannel;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Date; import java.util.Date;
import timber.log.Timber; import timber.log.Timber;
public class FileUtils { public class FileUtils {
/**
* Get SHA1 of file from input stream
*/
static String getSHA1(InputStream is) {
MessageDigest digest;
try {
digest = MessageDigest.getInstance("SHA1");
} catch (NoSuchAlgorithmException e) {
Timber.e(e, "Exception while getting Digest");
return "";
}
byte[] buffer = new byte[8192];
int read;
try {
while ((read = is.read(buffer)) > 0) {
digest.update(buffer, 0, read);
}
byte[] md5sum = digest.digest();
BigInteger bigInt = new BigInteger(1, md5sum);
String output = bigInt.toString(16);
// Fill to 40 chars
output = String.format("%40s", output).replace(' ', '0');
Timber.i("File SHA1: %s", output);
return output;
} catch (IOException e) {
Timber.e(e, "IO Exception");
return "";
} finally {
try {
is.close();
} catch (IOException e) {
Timber.e(e, "Exception on closing MD5 input stream");
}
}
}
/**
* In older devices getPath() may fail depending on the source URI. Creating and using a copy of the file seems to work instead.
* @return path of copy
*/
@Nullable
static String createCopyPath(ParcelFileDescriptor descriptor) {
try {
String copyPath = Environment.getExternalStorageDirectory().toString() + "/CommonsApp/" + new Date().getTime() + ".jpg";
File newFile = new File(Environment.getExternalStorageDirectory().toString() + "/CommonsApp");
newFile.mkdir();
FileUtils.copy(descriptor.getFileDescriptor(), copyPath);
Timber.d("Filepath (copied): %s", copyPath);
return copyPath;
} catch (IOException e) {
Timber.e(e);
return null;
}
}
/** /**
* Get a file path from a Uri. This will get the the path for Storage Access * Get a file path from a Uri. This will get the the path for Storage Access
* Framework Documents, as well as the _data field for the MediaStore and * Framework Documents, as well as the _data field for the MediaStore and
@ -59,7 +125,7 @@ public class FileUtils {
final String id = DocumentsContract.getDocumentId(uri); final String id = DocumentsContract.getDocumentId(uri);
final Uri contentUri = ContentUris.withAppendedId( final Uri contentUri = ContentUris.withAppendedId(
Uri.parse("content://downloads/public_downloads"), Long.valueOf(id)); Uri.parse("content://downloads/document"), Long.valueOf(id));
returnPath = getDataColumn(context, contentUri, null, null); returnPath = getDataColumn(context, contentUri, null, null);
} else if (isMediaDocument(uri)) { // MediaProvider } else if (isMediaDocument(uri)) { // MediaProvider
@ -235,4 +301,80 @@ public class FileUtils {
copy(new FileInputStream(source), new FileOutputStream(destination)); copy(new FileInputStream(source), new FileOutputStream(destination));
} }
/**
* Read and return the content of a resource file as string.
* @param fileName asset file's path (e.g. "/queries/nearby_query.rq")
* @return the content of the file
*/
public static String readFromResource(String fileName) throws IOException {
StringBuilder buffer = new StringBuilder();
BufferedReader reader = null;
try {
InputStream inputStream = FileUtils.class.getResourceAsStream(fileName);
if (inputStream == null) {
throw new FileNotFoundException(fileName);
}
reader = new BufferedReader(new InputStreamReader(inputStream, "UTF-8"));
String line;
while ((line = reader.readLine()) != null) {
buffer.append(line).append("\n");
}
} finally {
if (reader != null) {
reader.close();
}
}
return buffer.toString();
}
/**
* Deletes files.
* @param file context
*/
public static boolean deleteFile(File file) {
boolean deletedAll = true;
if (file != null) {
if (file.isDirectory()) {
String[] children = file.list();
for (String child : children) {
deletedAll = deleteFile(new File(file, child)) && deletedAll;
}
} else {
deletedAll = file.delete();
}
}
return deletedAll;
}
public static File createAndGetAppLogsFile(String logs) {
try {
File commonsAppDirectory = new File(Environment.getExternalStorageDirectory().toString() + "/CommonsApp");
if (!commonsAppDirectory.exists()) {
commonsAppDirectory.mkdir();
}
File logsFile = new File(commonsAppDirectory,"logs.txt");
if (logsFile.exists()) {
//old logs file is useless
logsFile.delete();
}
logsFile.createNewFile();
FileOutputStream outputStream = new FileOutputStream(logsFile);
OutputStreamWriter outputStreamWriter = new OutputStreamWriter(outputStream);
outputStreamWriter.append(logs);
outputStreamWriter.close();
outputStream.flush();
outputStream.close();
return logsFile;
} catch (IOException ioe) {
Timber.e(ioe);
return null;
}
}
} }

View file

@ -0,0 +1,40 @@
package fr.free.nrw.commons.upload;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.inject.Inject;
import javax.inject.Singleton;
@Singleton
public class GpsCategoryModel {
private Set<String> categorySet;
@Inject
public GpsCategoryModel() {
clear();
}
public void clear() {
categorySet = new HashSet<>();
}
public boolean getGpsCatExists() {
return !categorySet.isEmpty();
}
public List<String> getCategoryList() {
return new ArrayList<>(categorySet);
}
public void setCategoryList(List<String> categoryList) {
clear();
categorySet.addAll(categoryList != null ? categoryList : new ArrayList<>());
}
public void add(String categoryString) {
categorySet.add(categoryString);
}
}

View file

@ -47,6 +47,8 @@ import fr.free.nrw.commons.modifications.TemplateRemoveModifier;
import fr.free.nrw.commons.mwapi.MediaWikiApi; import fr.free.nrw.commons.mwapi.MediaWikiApi;
import timber.log.Timber; import timber.log.Timber;
//TODO: We should use this class to see how multiple uploads are handled, and then REMOVE it.
public class MultipleShareActivity extends AuthenticatedActivity public class MultipleShareActivity extends AuthenticatedActivity
implements MediaDetailPagerFragment.MediaDetailProvider, implements MediaDetailPagerFragment.MediaDetailProvider,
AdapterView.OnItemClickListener, AdapterView.OnItemClickListener,
@ -166,7 +168,8 @@ public class MultipleShareActivity extends AuthenticatedActivity
View target = getCurrentFocus(); View target = getCurrentFocus();
if (target != null) { if (target != null) {
InputMethodManager imm = (InputMethodManager) target.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); InputMethodManager imm = (InputMethodManager) target.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(target.getWindowToken(), 0); if (imm != null)
imm.hideSoftInputFromWindow(target.getWindowToken(), 0);
} }
getSupportFragmentManager().beginTransaction() getSupportFragmentManager().beginTransaction()
.add(R.id.uploadsFragmentContainer, categorizationFragment, "categorization") .add(R.id.uploadsFragmentContainer, categorizationFragment, "categorization")
@ -221,8 +224,8 @@ public class MultipleShareActivity extends AuthenticatedActivity
//TODO: 15/10/17 should location permission be explicitly requested if not provided? //TODO: 15/10/17 should location permission be explicitly requested if not provided?
//check if location permission is enabled //check if location permission is enabled
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && ContextCompat.checkSelfPermission(this,Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) {
if (ContextCompat.checkSelfPermission(this,Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) { {
locationPermitted = true; locationPermitted = true;
} }
} }
@ -237,7 +240,7 @@ public class MultipleShareActivity extends AuthenticatedActivity
private void showDetail(int i) { private void showDetail(int i) {
if (mediaDetails == null || !mediaDetails.isVisible()) { if (mediaDetails == null || !mediaDetails.isVisible()) {
mediaDetails = new MediaDetailPagerFragment(true); mediaDetails = new MediaDetailPagerFragment(true, false);
getSupportFragmentManager() getSupportFragmentManager()
.beginTransaction() .beginTransaction()
.replace(R.id.uploadsFragmentContainer, mediaDetails) .replace(R.id.uploadsFragmentContainer, mediaDetails)

View file

@ -1,6 +1,5 @@
package fr.free.nrw.commons.upload; package fr.free.nrw.commons.upload;
import android.app.Activity;
import android.content.Context; import android.content.Context;
import android.graphics.Point; import android.graphics.Point;
import android.net.Uri; import android.net.Uri;
@ -11,14 +10,12 @@ import android.text.Editable;
import android.text.TextUtils; import android.text.TextUtils;
import android.text.TextWatcher; import android.text.TextWatcher;
import android.util.DisplayMetrics; import android.util.DisplayMetrics;
import android.util.Log;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.Menu; import android.view.Menu;
import android.view.MenuInflater; import android.view.MenuInflater;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.view.inputmethod.InputMethodManager;
import android.widget.AdapterView; import android.widget.AdapterView;
import android.widget.BaseAdapter; import android.widget.BaseAdapter;
import android.widget.EditText; import android.widget.EditText;
@ -27,6 +24,8 @@ import android.widget.GridView;
import android.widget.RelativeLayout; import android.widget.RelativeLayout;
import android.widget.TextView; import android.widget.TextView;
import butterknife.BindView;
import butterknife.ButterKnife;
import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder; import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder;
import com.facebook.drawee.view.SimpleDraweeView; import com.facebook.drawee.view.SimpleDraweeView;
@ -34,6 +33,7 @@ import dagger.android.support.AndroidSupportInjection;
import fr.free.nrw.commons.R; import fr.free.nrw.commons.R;
import fr.free.nrw.commons.contributions.Contribution; import fr.free.nrw.commons.contributions.Contribution;
import fr.free.nrw.commons.media.MediaDetailPagerFragment; import fr.free.nrw.commons.media.MediaDetailPagerFragment;
import fr.free.nrw.commons.utils.ViewUtil;
public class MultipleUploadListFragment extends Fragment { public class MultipleUploadListFragment extends Fragment {
@ -41,9 +41,13 @@ public class MultipleUploadListFragment extends Fragment {
void OnMultipleUploadInitiated(); void OnMultipleUploadInitiated();
} }
private GridView photosGrid; @BindView(R.id.multipleShareBackground)
GridView photosGrid;
@BindView(R.id.multipleBaseTitle)
EditText baseTitle;
private PhotoDisplayAdapter photosAdapter; private PhotoDisplayAdapter photosAdapter;
private EditText baseTitle;
private TitleTextWatcher textWatcher = new TitleTextWatcher(); private TitleTextWatcher textWatcher = new TitleTextWatcher();
private Point photoSize; private Point photoSize;
@ -89,9 +93,9 @@ public class MultipleUploadListFragment extends Fragment {
if (view == null) { if (view == null) {
view = LayoutInflater.from(getContext()).inflate(R.layout.layout_upload_item, viewGroup, false); view = LayoutInflater.from(getContext()).inflate(R.layout.layout_upload_item, viewGroup, false);
holder = new UploadHolderView(); holder = new UploadHolderView();
holder.image = (SimpleDraweeView) view.findViewById(R.id.uploadImage); holder.image = view.findViewById(R.id.uploadImage);
holder.title = (TextView) view.findViewById(R.id.uploadTitle); holder.title = view.findViewById(R.id.uploadTitle);
holder.overlay = (RelativeLayout) view.findViewById(R.id.uploadOverlay); holder.overlay = view.findViewById(R.id.uploadOverlay);
holder.image.setLayoutParams(new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, photoSize.y)); holder.image.setLayoutParams(new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, photoSize.y));
holder.image.setHierarchy(GenericDraweeHierarchyBuilder holder.image.setHierarchy(GenericDraweeHierarchyBuilder
@ -129,11 +133,8 @@ public class MultipleUploadListFragment extends Fragment {
super.onStop(); super.onStop();
// FIXME: Stops the keyboard from being shown 'stale' while moving out of this fragment into the next // FIXME: Stops the keyboard from being shown 'stale' while moving out of this fragment into the next
View target = getView().findFocus(); View target = getActivity().getCurrentFocus();
if (target != null) { ViewUtil.hideKeyboard(target);
InputMethodManager imm = (InputMethodManager) target.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(target.getWindowToken(), 0);
}
} }
// FIXME: Wrong result type // FIXME: Wrong result type
@ -169,9 +170,7 @@ public class MultipleUploadListFragment extends Fragment {
@Override @Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_multiple_uploads_list, container, false); View view = inflater.inflate(R.layout.fragment_multiple_uploads_list, container, false);
photosGrid = (GridView) view.findViewById(R.id.multipleShareBackground); ButterKnife.bind(this,view);
baseTitle = (EditText) view.findViewById(R.id.multipleBaseTitle);
photosAdapter = new PhotoDisplayAdapter(); photosAdapter = new PhotoDisplayAdapter();
photosGrid.setAdapter(photosAdapter); photosGrid.setAdapter(photosAdapter);
photosGrid.setOnItemClickListener((AdapterView.OnItemClickListener) getActivity()); photosGrid.setOnItemClickListener((AdapterView.OnItemClickListener) getActivity());
@ -182,18 +181,13 @@ public class MultipleUploadListFragment extends Fragment {
baseTitle.setOnFocusChangeListener((v, hasFocus) -> { baseTitle.setOnFocusChangeListener((v, hasFocus) -> {
if (!hasFocus) { if (!hasFocus) {
hideKeyboard(v); ViewUtil.hideKeyboard(v);
} }
}); });
return view; return view;
} }
public void hideKeyboard(View view) {
InputMethodManager inputMethodManager =(InputMethodManager)getActivity().getSystemService(Activity.INPUT_METHOD_SERVICE);
inputMethodManager.hideSoftInputFromWindow(view.getWindowToken(), 0);
}
@Override @Override
public void onDestroyView() { public void onDestroyView() {
baseTitle.removeTextChangedListener(textWatcher); baseTitle.removeTextChangedListener(textWatcher);

View file

@ -1,249 +0,0 @@
package fr.free.nrw.commons.upload;
import android.content.Context;
import android.net.Uri;
import com.android.volley.Cache;
import com.android.volley.NetworkResponse;
import com.android.volley.Request;
import com.android.volley.RequestQueue;
import com.android.volley.Response;
import com.android.volley.VolleyError;
import com.android.volley.toolbox.HttpHeaderParser;
import com.android.volley.toolbox.JsonRequest;
import com.android.volley.toolbox.Volley;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import timber.log.Timber;
/**
* Uses the Volley library to implement asynchronous calls to the Commons MediaWiki API to match
* GPS coordinates with nearby Commons categories. Parses the results using GSON to obtain a list
* of relevant categories.
*/
public class MwVolleyApi {
private static RequestQueue REQUEST_QUEUE;
private static final Gson GSON = new GsonBuilder().create();
private static Set<String> categorySet;
private static List<String> categoryList;
private static final String MWURL = "https://commons.wikimedia.org/";
private final Context context;
public MwVolleyApi(Context context) {
this.context = context;
categorySet = new HashSet<>();
}
public static List<String> getGpsCat() {
return categoryList;
}
public static void setGpsCat(List<String> cachedList) {
categoryList = new ArrayList<>();
categoryList.addAll(cachedList);
Timber.d("Setting GPS cats from cache: %s", categoryList);
}
public void request(String coords) {
String apiUrl = buildUrl(coords);
Timber.d("URL: %s", apiUrl);
JsonRequest<QueryResponse> request = new QueryRequest(apiUrl,
new LogResponseListener<>(), new LogResponseErrorListener());
getQueue().add(request);
}
/**
* Builds URL with image coords for MediaWiki API calls
* Example URL: https://commons.wikimedia.org/w/api.php?action=query&prop=categories|coordinates|pageprops&format=json&clshow=!hidden&coprop=type|name|dim|country|region|globe&codistancefrompoint=38.11386944444445|13.356263888888888&generator=geosearch&redirects=&ggscoord=38.11386944444445|1.356263888888888&ggsradius=100&ggslimit=10&ggsnamespace=6&ggsprop=type|name|dim|country|region|globe&ggsprimary=all&formatversion=2
* @param coords Coordinates to build query with
* @return URL for API query
*/
private String buildUrl(String coords) {
Uri.Builder builder = Uri.parse(MWURL).buildUpon();
builder.appendPath("w")
.appendPath("api.php")
.appendQueryParameter("action", "query")
.appendQueryParameter("prop", "categories|coordinates|pageprops")
.appendQueryParameter("format", "json")
.appendQueryParameter("clshow", "!hidden")
.appendQueryParameter("coprop", "type|name|dim|country|region|globe")
.appendQueryParameter("codistancefrompoint", coords)
.appendQueryParameter("generator", "geosearch")
.appendQueryParameter("ggscoord", coords)
.appendQueryParameter("ggsradius", "10000")
.appendQueryParameter("ggslimit", "10")
.appendQueryParameter("ggsnamespace", "6")
.appendQueryParameter("ggsprop", "type|name|dim|country|region|globe")
.appendQueryParameter("ggsprimary", "all")
.appendQueryParameter("formatversion", "2");
return builder.toString();
}
private synchronized RequestQueue getQueue() {
if (REQUEST_QUEUE == null) {
REQUEST_QUEUE = Volley.newRequestQueue(context);
}
return REQUEST_QUEUE;
}
private static class LogResponseListener<T> implements Response.Listener<T> {
@Override
public void onResponse(T response) {
Timber.d(response.toString());
}
}
private static class LogResponseErrorListener implements Response.ErrorListener {
@Override
public void onErrorResponse(VolleyError error) {
Timber.e(error.toString());
}
}
private static class QueryRequest extends JsonRequest<QueryResponse> {
public QueryRequest(String url,
Response.Listener<QueryResponse> listener,
Response.ErrorListener errorListener) {
super(Request.Method.GET, url, null, listener, errorListener);
}
@Override
protected Response<QueryResponse> parseNetworkResponse(NetworkResponse response) {
String json = parseString(response);
QueryResponse queryResponse = GSON.fromJson(json, QueryResponse.class);
return Response.success(queryResponse, cacheEntry(response));
}
private Cache.Entry cacheEntry(NetworkResponse response) {
return HttpHeaderParser.parseCacheHeaders(response);
}
private String parseString(NetworkResponse response) {
try {
return new String(response.data, HttpHeaderParser.parseCharset(response.headers));
} catch (UnsupportedEncodingException e) {
return new String(response.data);
}
}
}
public static class GpsCatExists {
private static boolean gpsCatExists;
public static void setGpsCatExists(boolean gpsCat) {
gpsCatExists = gpsCat;
}
public static boolean getGpsCatExists() {
return gpsCatExists;
}
}
private static class QueryResponse {
private Query query = new Query();
private String printSet() {
if (categorySet == null || categorySet.isEmpty()) {
GpsCatExists.setGpsCatExists(false);
Timber.d("gpsCatExists=%b", GpsCatExists.getGpsCatExists());
return "No collection of categories";
} else {
GpsCatExists.setGpsCatExists(true);
Timber.d("gpsCatExists=%b", GpsCatExists.getGpsCatExists());
return "CATEGORIES FOUND" + categorySet.toString();
}
}
@Override
public String toString() {
if (query != null) {
return "query=" + query.toString() + "\n" + printSet();
} else {
return "No pages found";
}
}
}
private static class Query {
private Page [] pages;
@Override
public String toString() {
StringBuilder builder = new StringBuilder("pages=" + "\n");
if (pages != null) {
for (Page page : pages) {
builder.append(page.toString());
builder.append("\n");
}
builder.replace(builder.length() - 1, builder.length(), "");
return builder.toString();
} else {
return "No pages found";
}
}
}
public static class Page {
private int pageid;
private int ns;
private String title;
private Category[] categories;
private Category category;
public Page() {
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder("PAGEID=" + pageid + " ns=" + ns + " title=" + title + "\n" + " CATEGORIES= ");
if (categories == null || categories.length == 0) {
builder.append("no categories exist\n");
} else {
for (Category category : categories) {
builder.append(category.toString());
builder.append("\n");
if (category != null) {
String categoryString = category.toString().replace("Category:", "");
categorySet.add(categoryString);
}
}
}
categoryList = new ArrayList<>(categorySet);
builder.replace(builder.length() - 1, builder.length(), "");
return builder.toString();
}
}
private static class Category {
private String title;
@Override
public String toString() {
return title;
}
}
}

View file

@ -1,50 +1,53 @@
package fr.free.nrw.commons.upload; package fr.free.nrw.commons.upload;
import android.Manifest; import android.Manifest;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.app.Activity; import android.app.Activity;
import android.content.ContentResolver; import android.content.ContentResolver;
import android.content.Context; import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent; import android.content.Intent;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.graphics.Point;
import android.graphics.Rect;
import android.net.Uri; import android.net.Uri;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.os.Environment; import android.os.Environment;
import android.os.ParcelFileDescriptor;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.RequiresApi; import android.support.annotation.RequiresApi;
import android.support.design.widget.FloatingActionButton;
import android.support.design.widget.Snackbar; import android.support.design.widget.Snackbar;
import android.support.graphics.drawable.VectorDrawableCompat; import android.support.graphics.drawable.VectorDrawableCompat;
import android.support.v4.app.ActivityCompat; import android.support.v4.app.ActivityCompat;
import android.support.v4.app.FragmentManager;
import android.support.v4.content.ContextCompat; import android.support.v4.content.ContextCompat;
import android.support.v7.app.AlertDialog; import android.view.KeyEvent;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.widget.TextView; import android.view.animation.DecelerateInterpolator;
import android.widget.FrameLayout;
import android.widget.Toast; import android.widget.Toast;
import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder; import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder;
import com.facebook.drawee.view.SimpleDraweeView; import com.facebook.drawee.view.SimpleDraweeView;
import com.github.chrisbanes.photoview.PhotoView;
import java.io.File;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.lang.ref.WeakReference; import java.lang.ref.WeakReference;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Date;
import java.util.List; import java.util.List;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Named; import javax.inject.Named;
import butterknife.BindView;
import butterknife.ButterKnife; import butterknife.ButterKnife;
import butterknife.OnClick;
import fr.free.nrw.commons.R; import fr.free.nrw.commons.R;
import fr.free.nrw.commons.auth.AuthenticatedActivity; import fr.free.nrw.commons.auth.AuthenticatedActivity;
import fr.free.nrw.commons.auth.SessionManager; import fr.free.nrw.commons.auth.SessionManager;
@ -52,19 +55,19 @@ import fr.free.nrw.commons.caching.CacheController;
import fr.free.nrw.commons.category.CategorizationFragment; import fr.free.nrw.commons.category.CategorizationFragment;
import fr.free.nrw.commons.category.OnCategoriesSaveHandler; import fr.free.nrw.commons.category.OnCategoriesSaveHandler;
import fr.free.nrw.commons.contributions.Contribution; import fr.free.nrw.commons.contributions.Contribution;
import fr.free.nrw.commons.contributions.ContributionsActivity;
import fr.free.nrw.commons.modifications.CategoryModifier; import fr.free.nrw.commons.modifications.CategoryModifier;
import fr.free.nrw.commons.modifications.ModificationsContentProvider; import fr.free.nrw.commons.modifications.ModificationsContentProvider;
import fr.free.nrw.commons.modifications.ModifierSequence; import fr.free.nrw.commons.modifications.ModifierSequence;
import fr.free.nrw.commons.modifications.ModifierSequenceDao; import fr.free.nrw.commons.modifications.ModifierSequenceDao;
import fr.free.nrw.commons.modifications.TemplateRemoveModifier; import fr.free.nrw.commons.modifications.TemplateRemoveModifier;
import fr.free.nrw.commons.mwapi.CategoryApi;
import fr.free.nrw.commons.utils.ImageUtils;
import fr.free.nrw.commons.mwapi.MediaWikiApi; import fr.free.nrw.commons.mwapi.MediaWikiApi;
import fr.free.nrw.commons.utils.ViewUtil;
import timber.log.Timber; import timber.log.Timber;
import static fr.free.nrw.commons.upload.ExistingFileAsync.Result.DUPLICATE_PROCEED; import static fr.free.nrw.commons.upload.ExistingFileAsync.Result.DUPLICATE_PROCEED;
import static fr.free.nrw.commons.upload.ExistingFileAsync.Result.NO_DUPLICATE; import static fr.free.nrw.commons.upload.ExistingFileAsync.Result.NO_DUPLICATE;
import static fr.free.nrw.commons.upload.FileUtils.getSHA1;
/** /**
* Activity for the title/desc screen after image is selected. Also starts processing image * Activity for the title/desc screen after image is selected. Also starts processing image
@ -73,14 +76,13 @@ import static fr.free.nrw.commons.upload.ExistingFileAsync.Result.NO_DUPLICATE;
public class ShareActivity public class ShareActivity
extends AuthenticatedActivity extends AuthenticatedActivity
implements SingleUploadFragment.OnUploadActionInitiated, implements SingleUploadFragment.OnUploadActionInitiated,
OnCategoriesSaveHandler,SimilarImageDialogFragment.onResponse { OnCategoriesSaveHandler {
private static final int REQUEST_PERM_ON_CREATE_STORAGE = 1;
private static final int REQUEST_PERM_ON_CREATE_LOCATION = 2; private static final int REQUEST_PERM_ON_CREATE_LOCATION = 2;
private static final int REQUEST_PERM_ON_CREATE_STORAGE_AND_LOCATION = 3;
private static final int REQUEST_PERM_ON_SUBMIT_STORAGE = 4; private static final int REQUEST_PERM_ON_SUBMIT_STORAGE = 4;
private CategorizationFragment categorizationFragment; //Had to make them class variables, to extract out the click listeners, also I see no harm in this
final Rect startBounds = new Rect();
final Rect finalBounds = new Rect();
final Point globalOffset = new Point();
@Inject @Inject
MediaWikiApi mwApi; MediaWikiApi mwApi;
@Inject @Inject
@ -92,36 +94,55 @@ public class ShareActivity
@Inject @Inject
ModifierSequenceDao modifierSequenceDao; ModifierSequenceDao modifierSequenceDao;
@Inject @Inject
CategoryApi apiCall;
@Inject
@Named("default_preferences") @Named("default_preferences")
SharedPreferences prefs; SharedPreferences prefs;
@Inject
GpsCategoryModel gpsCategoryModel;
@BindView(R.id.container)
FrameLayout flContainer;
@BindView(R.id.backgroundImage)
SimpleDraweeView backgroundImageView;
@BindView(R.id.media_map)
FloatingActionButton mapButton;
@BindView(R.id.media_upload_zoom_in)
FloatingActionButton zoomInButton;
@BindView(R.id.media_upload_zoom_out)
FloatingActionButton zoomOutButton;
@BindView(R.id.main_fab)
FloatingActionButton mainFab;
@BindView(R.id.expanded_image)
PhotoView expandedImageView;
private String source; private String source;
private String mimeType; private String mimeType;
private CategorizationFragment categorizationFragment;
private Uri mediaUri; private Uri mediaUri;
private Contribution contribution; private Contribution contribution;
private SimpleDraweeView backgroundImageView; private GPSExtractor gpsObj;
private boolean cacheFound;
private GPSExtractor imageObj;
private GPSExtractor tempImageObj;
private String decimalCoords; private String decimalCoords;
private FileProcessor fileObj;
private boolean useNewPermissions = false; private boolean useNewPermissions = false;
private boolean storagePermitted = false; private boolean storagePermitted = false;
private boolean locationPermitted = false; private boolean locationPermitted = false;
private String title; private String title;
private String description; private String description;
private String wikiDataEntityId;
private Snackbar snackbar; private Snackbar snackbar;
private boolean duplicateCheckPassed = false; private boolean duplicateCheckPassed = false;
private boolean haveCheckedForOtherImages = false;
private boolean isNearbyUpload = false; private boolean isNearbyUpload = false;
private Animator CurrentAnimator;
private long ShortAnimationDuration;
private boolean isFABOpen = false;
private float startScaleFinal;
private boolean isZoom = false;
/** /**
* Called when user taps the submit button. * Called when user taps the submit button.
* Requests Storage permission, if needed.
*/ */
@Override @Override
public void uploadActionInitiated(String title, String description) { public void uploadActionInitiated(String title, String description) {
@ -130,8 +151,6 @@ public class ShareActivity
this.description = description; this.description = description;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// Check for Storage permission that is required for upload.
// Do not allow user to proceed without permission, otherwise will crash
if (needsToRequestStoragePermission()) { if (needsToRequestStoragePermission()) {
requestPermissions(new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, requestPermissions(new String[]{Manifest.permission.READ_EXTERNAL_STORAGE},
REQUEST_PERM_ON_SUBMIT_STORAGE); REQUEST_PERM_ON_SUBMIT_STORAGE);
@ -143,34 +162,44 @@ public class ShareActivity
} }
} }
/**
* Checks whether storage permissions need to be requested.
* Permissions are needed if the file is not owned by this application, (e.g. shared from the Gallery)
*
* @return true if file is not owned by this application and permission hasn't been granted beforehand
*/
@RequiresApi(16) @RequiresApi(16)
private boolean needsToRequestStoragePermission() { private boolean needsToRequestStoragePermission() {
// We need to ask storage permission when
// the file is not owned by this application, (e.g. shared from the Gallery)
// and permission is not obtained.
return !FileUtils.isSelfOwned(getApplicationContext(), mediaUri) return !FileUtils.isSelfOwned(getApplicationContext(), mediaUri)
&& (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) && (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED); != PackageManager.PERMISSION_GRANTED);
} }
/**
* Called after permission checks are done.
* Gets file metadata for category suggestions, displays toast, caches categories found, calls uploadController
*/
private void uploadBegins() { private void uploadBegins() {
getFileMetadata(locationPermitted); fileObj.processFileCoordinates(locationPermitted);
Toast startingToast = Toast.makeText(this, R.string.uploading_started, Toast.LENGTH_LONG); Toast startingToast = Toast.makeText(this, R.string.uploading_started, Toast.LENGTH_LONG);
startingToast.show(); startingToast.show();
if (!cacheFound) { if (!fileObj.isCacheFound()) {
//Has to be called after apiCall.request() //Has to be called after apiCall.request()
cacheController.cacheCategory(); cacheController.cacheCategory();
Timber.d("Cache the categories found"); Timber.d("Cache the categories found");
} }
uploadController.startUpload(title, mediaUri, description, mimeType, source, decimalCoords, c -> { uploadController.startUpload(title, mediaUri, description, mimeType, source, decimalCoords, wikiDataEntityId, c -> {
ShareActivity.this.contribution = c; ShareActivity.this.contribution = c;
showPostUpload(); showPostUpload();
}); });
} }
/**
* Starts CategorizationFragment after uploadBegins.
*/
private void showPostUpload() { private void showPostUpload() {
if (categorizationFragment == null) { if (categorizationFragment == null) {
categorizationFragment = new CategorizationFragment(); categorizationFragment = new CategorizationFragment();
@ -180,6 +209,11 @@ public class ShareActivity
.commit(); .commit();
} }
/**
* Send categories to modifications queue after they are selected
*
* @param categories categories selected
*/
@Override @Override
public void onCategoriesSave(List<String> categories) { public void onCategoriesSave(List<String> categories) {
if (categories.size() > 0) { if (categories.size() > 0) {
@ -217,9 +251,6 @@ public class ShareActivity
finish(); finish();
} }
protected boolean isNearbyUpload() {
return isNearbyUpload;
}
@Override @Override
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) {
@ -228,7 +259,6 @@ public class ShareActivity
setContentView(R.layout.activity_share); setContentView(R.layout.activity_share);
ButterKnife.bind(this); ButterKnife.bind(this);
initBack(); initBack();
backgroundImageView = (SimpleDraweeView) findViewById(R.id.backgroundImage);
backgroundImageView.setHierarchy(GenericDraweeHierarchyBuilder backgroundImageView.setHierarchy(GenericDraweeHierarchyBuilder
.newInstance(getResources()) .newInstance(getResources())
.setPlaceholderImage(VectorDrawableCompat.create(getResources(), .setPlaceholderImage(VectorDrawableCompat.create(getResources(),
@ -237,7 +267,54 @@ public class ShareActivity
R.drawable.ic_error_outline_black_24dp, getTheme())) R.drawable.ic_error_outline_black_24dp, getTheme()))
.build()); .build());
//Receive intent from ContributionController.java when user selects picture to upload receiveImageIntent();
if (savedInstanceState != null) {
contribution = savedInstanceState.getParcelable("contribution");
}
requestAuthToken();
Timber.d("Uri: %s", mediaUri.toString());
Timber.d("Ext storage dir: %s", Environment.getExternalStorageDirectory());
useNewPermissions = false;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
useNewPermissions = true;
if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) {
locationPermitted = true;
}
}
// Check location permissions if M or newer for category suggestions, request via snackbar if not present
if (!locationPermitted) {
requestPermissionUsingSnackBar(
getString(R.string.location_permission_rationale),
new String[]{Manifest.permission.ACCESS_FINE_LOCATION},
REQUEST_PERM_ON_CREATE_LOCATION);
}
SingleUploadFragment shareView = (SingleUploadFragment) getSupportFragmentManager().findFragmentByTag("shareView");
categorizationFragment = (CategorizationFragment) getSupportFragmentManager().findFragmentByTag("categorization");
if (shareView == null && categorizationFragment == null) {
shareView = new SingleUploadFragment();
getSupportFragmentManager()
.beginTransaction()
.add(R.id.single_upload_fragment_container, shareView, "shareView")
.commitAllowingStateLoss();
}
uploadController.prepareService();
ContentResolver contentResolver = this.getContentResolver();
fileObj = new FileProcessor(mediaUri, contentResolver, this);
checkIfFileExists();
gpsObj = fileObj.processFileCoordinates(locationPermitted);
decimalCoords = fileObj.getDecimalCoords();
}
/**
* Receive intent from ContributionController.java when user selects picture to upload
*/
private void receiveImageIntent() {
Intent intent = getIntent(); Intent intent = getIntent();
if (Intent.ACTION_SEND.equals(intent.getAction())) { if (Intent.ACTION_SEND.equals(intent.getAction())) {
@ -257,174 +334,100 @@ public class ShareActivity
if (mediaUri != null) { if (mediaUri != null) {
backgroundImageView.setImageURI(mediaUri); backgroundImageView.setImageURI(mediaUri);
} }
if (savedInstanceState != null) {
contribution = savedInstanceState.getParcelable("contribution");
}
requestAuthToken();
Timber.d("Uri: %s", mediaUri.toString());
Timber.d("Ext storage dir: %s", Environment.getExternalStorageDirectory());
useNewPermissions = false;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
useNewPermissions = true;
if (!needsToRequestStoragePermission()) {
storagePermitted = true;
}
if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) {
locationPermitted = true;
}
}
// Check storage permissions if marshmallow or newer
if (useNewPermissions && (!storagePermitted || !locationPermitted)) {
if (!storagePermitted && !locationPermitted) {
String permissionRationales =
getResources().getString(R.string.read_storage_permission_rationale) + "\n"
+ getResources().getString(R.string.location_permission_rationale);
snackbar = requestPermissionUsingSnackBar(
permissionRationales,
new String[]{
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.ACCESS_FINE_LOCATION},
REQUEST_PERM_ON_CREATE_STORAGE_AND_LOCATION);
View snackbarView = snackbar.getView();
TextView textView = (TextView) snackbarView.findViewById(android.support.design.R.id.snackbar_text);
textView.setMaxLines(3);
} else if (!storagePermitted) {
requestPermissionUsingSnackBar(
getString(R.string.read_storage_permission_rationale),
new String[]{Manifest.permission.READ_EXTERNAL_STORAGE},
REQUEST_PERM_ON_CREATE_STORAGE);
} else if (!locationPermitted) {
requestPermissionUsingSnackBar(
getString(R.string.location_permission_rationale),
new String[]{Manifest.permission.ACCESS_FINE_LOCATION},
REQUEST_PERM_ON_CREATE_LOCATION);
}
}
performPreUploadProcessingOfFile();
SingleUploadFragment shareView = (SingleUploadFragment) getSupportFragmentManager().findFragmentByTag("shareView");
categorizationFragment = (CategorizationFragment) getSupportFragmentManager().findFragmentByTag("categorization");
if (shareView == null && categorizationFragment == null) {
shareView = new SingleUploadFragment();
getSupportFragmentManager()
.beginTransaction()
.add(R.id.single_upload_fragment_container, shareView, "shareView")
.commitAllowingStateLoss();
}
uploadController.prepareService();
} }
/**
* Function to display the zoom and map FAB
*/
private void showFABMenu() {
isFABOpen = true;
if (gpsObj != null && gpsObj.imageCoordsExists)
mapButton.setVisibility(View.VISIBLE);
zoomInButton.setVisibility(View.VISIBLE);
mainFab.animate().rotationBy(180);
mapButton.animate().translationY(-getResources().getDimension(R.dimen.second_fab));
zoomInButton.animate().translationY(-getResources().getDimension(R.dimen.first_fab));
}
/**
* Function to close the zoom and map FAB
*/
private void closeFABMenu() {
isFABOpen = false;
mainFab.animate().rotationBy(-180);
mapButton.animate().translationY(0);
zoomInButton.animate().translationY(0).setListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animator) {
}
@Override
public void onAnimationEnd(Animator animator) {
if (!isFABOpen) {
mapButton.setVisibility(View.GONE);
zoomInButton.setVisibility(View.GONE);
}
}
@Override
public void onAnimationCancel(Animator animator) {
}
@Override
public void onAnimationRepeat(Animator animator) {
}
});
}
/**
* Checks if upload was initiated via Nearby
*
* @return true if upload was initiated via Nearby
*/
protected boolean isNearbyUpload() {
return isNearbyUpload;
}
/**
* Handles BOTH snackbar permission request (for location) and submit button permission request (for storage)
*
* @param requestCode type of request
* @param permissions permissions requested
* @param grantResults grant results
*/
@Override @Override
public void onRequestPermissionsResult(int requestCode, public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
@NonNull String[] permissions, @NonNull int[] grantResults) {
switch (requestCode) { switch (requestCode) {
case REQUEST_PERM_ON_CREATE_STORAGE: {
if (grantResults.length >= 1
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
backgroundImageView.setImageURI(mediaUri);
storagePermitted = true;
performPreUploadProcessingOfFile();
}
return;
}
case REQUEST_PERM_ON_CREATE_LOCATION: { case REQUEST_PERM_ON_CREATE_LOCATION: {
if (grantResults.length >= 1 if (grantResults.length >= 1 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
locationPermitted = true; locationPermitted = true;
performPreUploadProcessingOfFile(); checkIfFileExists();
}
return;
}
case REQUEST_PERM_ON_CREATE_STORAGE_AND_LOCATION: {
if (grantResults.length >= 2
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
backgroundImageView.setImageURI(mediaUri);
storagePermitted = true;
performPreUploadProcessingOfFile();
}
if (grantResults.length >= 2
&& grantResults[1] == PackageManager.PERMISSION_GRANTED) {
locationPermitted = true;
performPreUploadProcessingOfFile();
} }
return; return;
} }
// Storage (from submit button) - this needs to be separate from (1) because only the // Storage (from submit button) - this needs to be separate from (1) because only the
// submit button should bring user to next screen // submit button should bring user to next screen
case REQUEST_PERM_ON_SUBMIT_STORAGE: { case REQUEST_PERM_ON_SUBMIT_STORAGE: {
if (grantResults.length >= 1 if (grantResults.length >= 1 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
//It is OK to call this at both (1) and (4) because if perm had been granted at //It is OK to call this at both (1) and (4) because if perm had been granted at
//snackbar, user should not be prompted at submit button //snackbar, user should not be prompted at submit button
performPreUploadProcessingOfFile(); checkIfFileExists();
//Uploading only begins if storage permission granted from arrow icon //Uploading only begins if storage permission granted from arrow icon
uploadBegins(); uploadBegins();
snackbar.dismiss(); snackbar.dismiss();
} }
return;
} }
} }
} }
private void performPreUploadProcessingOfFile() { /**
if (!useNewPermissions || storagePermitted) { * Displays Snackbar to ask for location permissions
if (!duplicateCheckPassed) { */
//Test SHA1 of image to see if it matches SHA1 of a file on Commons private Snackbar requestPermissionUsingSnackBar(String rationale, final String[] perms, final int code) {
try {
InputStream inputStream = getContentResolver().openInputStream(mediaUri);
Timber.d("Input stream created from %s", mediaUri.toString());
String fileSHA1 = getSHA1(inputStream);
Timber.d("File SHA1 is: %s", fileSHA1);
ExistingFileAsync fileAsyncTask =
new ExistingFileAsync(new WeakReference<Activity>(this), fileSHA1, new WeakReference<Context>(this), result -> {
Timber.d("%s duplicate check: %s", mediaUri.toString(), result);
duplicateCheckPassed = (result == DUPLICATE_PROCEED
|| result == NO_DUPLICATE);
/*
TODO: 16/9/17 should we run DetectUnwantedPicturesAsync if DUPLICATE_PROCEED is returned? Since that means
we are processing images that are already on server???...
*/
if (duplicateCheckPassed) {
//image can be uploaded, so now check if its a useless picture or not
performUnwantedPictureDetectionProcess();
}
},mwApi);
fileAsyncTask.execute();
} catch (IOException e) {
Timber.d(e, "IO Exception: ");
}
}
getFileMetadata(locationPermitted);
} else {
Timber.w("not ready for preprocessing: useNewPermissions=%s storage=%s location=%s",
useNewPermissions, storagePermitted, locationPermitted);
}
}
private void performUnwantedPictureDetectionProcess() {
String imageMediaFilePath = FileUtils.getPath(this,mediaUri);
DetectUnwantedPicturesAsync detectUnwantedPicturesAsync
= new DetectUnwantedPicturesAsync(new WeakReference<Activity>(this)
, imageMediaFilePath);
detectUnwantedPicturesAsync.execute();
}
private Snackbar requestPermissionUsingSnackBar(String rationale,
final String[] perms,
final int code) {
Snackbar snackbar = Snackbar.make(findViewById(android.R.id.content), rationale, Snackbar snackbar = Snackbar.make(findViewById(android.R.id.content), rationale,
Snackbar.LENGTH_INDEFINITE).setAction(R.string.ok, Snackbar.LENGTH_INDEFINITE).setAction(R.string.ok,
view -> ActivityCompat.requestPermissions(ShareActivity.this, perms, code)); view -> ActivityCompat.requestPermissions(ShareActivity.this, perms, code));
@ -432,202 +435,44 @@ public class ShareActivity
return snackbar; return snackbar;
} }
@Nullable
private String getPathOfMediaOrCopy() {
String filePath = FileUtils.getPath(getApplicationContext(), mediaUri);
Timber.d("Filepath: " + filePath);
if (filePath == null) {
// in older devices getPath() may fail depending on the source URI
// creating and using a copy of the file seems to work instead.
// TODO: there might be a more proper solution than this
String copyPath = null;
try {
ParcelFileDescriptor descriptor
= getContentResolver().openFileDescriptor(mediaUri, "r");
if (descriptor != null) {
boolean useExtStorage = prefs.getBoolean("useExternalStorage", true);
if (useExtStorage) {
copyPath = Environment.getExternalStorageDirectory().toString()
+ "/CommonsApp/" + new Date().getTime() + ".jpg";
File newFile = new File(Environment.getExternalStorageDirectory().toString() + "/CommonsApp");
newFile.mkdir();
FileUtils.copy(
descriptor.getFileDescriptor(),
copyPath);
Timber.d("Filepath (copied): %s", copyPath);
return copyPath;
}
copyPath = getApplicationContext().getCacheDir().getAbsolutePath()
+ "/" + new Date().getTime() + ".jpg";
FileUtils.copy(
descriptor.getFileDescriptor(),
copyPath);
Timber.d("Filepath (copied): %s", copyPath);
return copyPath;
}
} catch (IOException e) {
Timber.w(e, "Error in file " + copyPath);
return null;
}
}
return filePath;
}
/** /**
* Gets coordinates for category suggestions, either from EXIF data or user location * Check if file user wants to upload already exists on Commons
*
* @param gpsEnabled if true use GPS
*/ */
private void getFileMetadata(boolean gpsEnabled) { private void checkIfFileExists() {
Timber.d("Calling GPSExtractor"); if (!useNewPermissions || storagePermitted) {
try { if (!duplicateCheckPassed) {
if (imageObj == null) { //Test SHA1 of image to see if it matches SHA1 of a file on Commons
ParcelFileDescriptor descriptor
= getContentResolver().openFileDescriptor(mediaUri, "r");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
if (descriptor != null) {
imageObj = new GPSExtractor(descriptor.getFileDescriptor(), this, prefs);
}
} else {
String filePath = getPathOfMediaOrCopy();
if (filePath != null) {
imageObj = new GPSExtractor(filePath, this, prefs);
}
}
}
if (imageObj != null) {
// Gets image coords from exif data or user location
decimalCoords = imageObj.getCoords(gpsEnabled);
if(decimalCoords==null || !imageObj.imageCoordsExists){
// Check if the location is from GPS or EXIF
// Find other photos taken around the same time which has gps coordinates
Timber.d("EXIF:false");
Timber.d("EXIF call"+(imageObj==tempImageObj));
if(!haveCheckedForOtherImages)
findOtherImages(gpsEnabled);// Do not do repeat the process
}
else {
// As the selected image has GPS data in EXIF go ahead with the same.
useImageCoords();
}
}
} catch (FileNotFoundException e) {
Timber.w("File not found: " + mediaUri, e);
}
}
private void findOtherImages(boolean gpsEnabled) {
Timber.d("filePath"+getPathOfMediaOrCopy());
String filePath = getPathOfMediaOrCopy();
long timeOfCreation = new File(filePath).lastModified();//Time when the original image was created
File folder = new File(filePath.substring(0,filePath.lastIndexOf('/')));
File[] files = folder.listFiles();
Timber.d("folderTime Number:"+files.length);
for(File file : files){
if(file.lastModified()-timeOfCreation<=(120*1000) && file.lastModified()-timeOfCreation>=-(120*1000)){
//Make sure the photos were taken within 20seconds
Timber.d("fild date:"+file.lastModified()+ " time of creation"+timeOfCreation);
tempImageObj = null;//Temporary GPSExtractor to extract coords from these photos
ParcelFileDescriptor descriptor
= null;
try { try {
descriptor = getContentResolver().openFileDescriptor(Uri.parse(file.getAbsolutePath()), "r"); InputStream inputStream = getContentResolver().openInputStream(mediaUri);
} catch (FileNotFoundException e) { String fileSHA1 = getSHA1(inputStream);
e.printStackTrace(); Timber.d("Input stream created from %s", mediaUri.toString());
} Timber.d("File SHA1 is: %s", fileSHA1);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
if (descriptor != null) {
tempImageObj = new GPSExtractor(descriptor.getFileDescriptor(),this, prefs);
}
} else {
if (filePath != null) {
tempImageObj = new GPSExtractor(file.getAbsolutePath(), this, prefs);
}
}
if(tempImageObj!=null){
Timber.d("not null fild EXIF"+tempImageObj.imageCoordsExists +" coords"+tempImageObj.getCoords(gpsEnabled));
if(tempImageObj.getCoords(gpsEnabled)!=null && tempImageObj.imageCoordsExists){
// Current image has gps coordinates and it's not current gps locaiton
Timber.d("This fild has image coords:"+ file.getAbsolutePath());
// Create a dialog fragment for the suggestion
FragmentManager fragmentManager = getSupportFragmentManager();
SimilarImageDialogFragment newFragment = new SimilarImageDialogFragment();
Bundle args = new Bundle();
args.putString("originalImagePath",filePath);
args.putString("possibleImagePath",file.getAbsolutePath());
newFragment.setArguments(args);
newFragment.show(fragmentManager, "dialog");
break;
}
ExistingFileAsync fileAsyncTask =
new ExistingFileAsync(new WeakReference<Activity>(this), fileSHA1, new WeakReference<Context>(this), result -> {
Timber.d("%s duplicate check: %s", mediaUri.toString(), result);
duplicateCheckPassed = (result == DUPLICATE_PROCEED || result == NO_DUPLICATE);
if (duplicateCheckPassed) {
//image is not a duplicate, so now check if its a unwanted picture or not
fileObj.detectUnwantedPictures();
}
}, mwApi);
fileAsyncTask.execute();
} catch (IOException e) {
Timber.e(e, "IO Exception: ");
} }
} }
} else {
Timber.w("not ready for preprocessing: useNewPermissions=%s storage=%s location=%s",
useNewPermissions, storagePermitted, locationPermitted);
} }
haveCheckedForOtherImages = true; //Finished checking for other images
return;
}
@Override
public void onPostiveResponse() {
imageObj = tempImageObj;
decimalCoords = imageObj.getCoords(false);// Not necessary to use gps as image already ha EXIF data
Timber.d("EXIF from tempImageObj");
useImageCoords();
}
@Override
public void onNegativeResponse() {
Timber.d("EXIF from imageObj");
useImageCoords();
}
/**
* Initiates retrieval of image coordinates or user coordinates, and caching of coordinates.
* Then initiates the calls to MediaWiki API through an instance of MwVolleyApi.
*/
public void useImageCoords() {
if (decimalCoords != null) {
Timber.d("Decimal coords of image: %s", decimalCoords);
Timber.d("is EXIF data present:"+imageObj.imageCoordsExists+" from findOther image:"+(imageObj==tempImageObj));
// Only set cache for this point if image has coords
if (imageObj.imageCoordsExists) {
double decLongitude = imageObj.getDecLongitude();
double decLatitude = imageObj.getDecLatitude();
cacheController.setQtPoint(decLongitude, decLatitude);
}
MwVolleyApi apiCall = new MwVolleyApi(this);
List<String> displayCatList = cacheController.findCategory();
boolean catListEmpty = displayCatList.isEmpty();
// If no categories found in cache, call MediaWiki API to match image coords with nearby Commons categories
if (catListEmpty) {
cacheFound = false;
apiCall.request(decimalCoords);
Timber.d("displayCatList size 0, calling MWAPI %s", displayCatList);
} else {
cacheFound = true;
Timber.d("Cache found, setting categoryList in MwVolleyApi to %s", displayCatList);
MwVolleyApi.setGpsCat(displayCatList);
}
}else{
Timber.d("EXIF: no coords");
}
} }
@Override @Override
public void onPause() { public void onPause() {
super.onPause(); super.onPause();
try { try {
imageObj.unregisterLocationManager(); gpsObj.unregisterLocationManager();
Timber.d("Unregistered locationManager"); Timber.d("Unregistered locationManager");
} catch (NullPointerException e) { } catch (NullPointerException e) {
Timber.d("locationManager does not exist, not unregistered"); Timber.d("locationManager does not exist, not unregistered");
@ -654,40 +499,157 @@ public class ShareActivity
return super.onOptionsItemSelected(item); return super.onOptionsItemSelected(item);
} }
// Get SHA1 of file from input stream /**
private String getSHA1(InputStream is) { * Allows zooming in to the image about to be uploaded. Called when zoom FAB is tapped
*/
private void zoomImageFromThumb(final View thumbView, Uri imageuri) {
// If there's an animation in progress, cancel it immediately and proceed with this one.
if (CurrentAnimator != null) {
CurrentAnimator.cancel();
}
isZoom = true;
ViewUtil.hideKeyboard(ShareActivity.this.findViewById(R.id.titleEdit | R.id.descEdit));
closeFABMenu();
mainFab.setVisibility(View.GONE);
MessageDigest digest; InputStream input = null;
try { try {
digest = MessageDigest.getInstance("SHA1"); input = this.getContentResolver().openInputStream(imageuri);
} catch (NoSuchAlgorithmException e) { } catch (FileNotFoundException e) {
Timber.e(e, "Exception while getting Digest"); e.printStackTrace();
return "";
} }
byte[] buffer = new byte[8192]; Zoom zoomObj = new Zoom(thumbView, flContainer, this.getContentResolver());
int read; Bitmap scaledImage = zoomObj.createScaledImage(input, imageuri);
try {
while ((read = is.read(buffer)) > 0) {
digest.update(buffer, 0, read);
}
byte[] md5sum = digest.digest();
BigInteger bigInt = new BigInteger(1, md5sum);
String output = bigInt.toString(16);
// Fill to 40 chars
output = String.format("%40s", output).replace(' ', '0');
Timber.i("File SHA1: %s", output);
return output; // Load the high-resolution "zoomed-in" image.
} catch (IOException e) { expandedImageView.setImageBitmap(scaledImage);
Timber.e(e, "IO Exception"); float startScale = zoomObj.adjustStartEndBounds(startBounds, finalBounds, globalOffset);
return "";
} finally { // Hide the thumbnail and show the zoomed-in view. When the animation
try { // begins, it will position the zoomed-in view in the place of the
is.close(); // thumbnail.
} catch (IOException e) { thumbView.setAlpha(0f);
Timber.e(e, "Exception on closing MD5 input stream"); expandedImageView.setVisibility(View.VISIBLE);
zoomOutButton.setVisibility(View.VISIBLE);
zoomInButton.setVisibility(View.GONE);
// Set the pivot point for SCALE_X and SCALE_Y transformations
// to the top-left corner of the zoomed-in view (the default
// is the center of the view).
expandedImageView.setPivotX(0f);
expandedImageView.setPivotY(0f);
// Construct and run the parallel animation of the four translation and
// scale properties (X, Y, SCALE_X, and SCALE_Y).
AnimatorSet set = new AnimatorSet();
set.play(ObjectAnimator.ofFloat(expandedImageView, View.X, startBounds.left, finalBounds.left))
.with(ObjectAnimator.ofFloat(expandedImageView, View.Y, startBounds.top, finalBounds.top))
.with(ObjectAnimator.ofFloat(expandedImageView, View.SCALE_X, startScale, 1f))
.with(ObjectAnimator.ofFloat(expandedImageView, View.SCALE_Y, startScale, 1f));
set.setDuration(ShortAnimationDuration);
set.setInterpolator(new DecelerateInterpolator());
set.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
CurrentAnimator = null;
} }
@Override
public void onAnimationCancel(Animator animation) {
CurrentAnimator = null;
}
});
set.start();
CurrentAnimator = set;
// Upon clicking the zoomed-in image, it should zoom back down
// to the original bounds and show the thumbnail instead of
// the expanded image.
startScaleFinal = startScale;
}
/**
* Called when user taps the ^ FAB button, expands to show Zoom and Map
*/
@OnClick(R.id.main_fab)
public void onMainFabClicked() {
if (!isFABOpen) {
showFABMenu();
} else {
closeFABMenu();
} }
} }
@OnClick(R.id.media_upload_zoom_in)
public void onZoomInFabClicked() {
try {
zoomImageFromThumb(backgroundImageView, mediaUri);
} catch (Exception e) {
Timber.e(e);
}
}
@OnClick(R.id.media_upload_zoom_out)
public void onZoomOutFabClicked() {
if (CurrentAnimator != null) {
CurrentAnimator.cancel();
}
isZoom = false;
zoomOutButton.setVisibility(View.GONE);
mainFab.setVisibility(View.VISIBLE);
// Animate the four positioning/sizing properties in parallel,
// back to their original values.
AnimatorSet set = new AnimatorSet();
set.play(ObjectAnimator.ofFloat(expandedImageView, View.X, startBounds.left))
.with(ObjectAnimator.ofFloat(expandedImageView, View.Y, startBounds.top))
.with(ObjectAnimator.ofFloat(expandedImageView, View.SCALE_X, startScaleFinal))
.with(ObjectAnimator.ofFloat(expandedImageView, View.SCALE_Y, startScaleFinal));
set.setDuration(ShortAnimationDuration);
set.setInterpolator(new DecelerateInterpolator());
set.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
//background image view is thumbView
backgroundImageView.setAlpha(1f);
expandedImageView.setVisibility(View.GONE);
CurrentAnimator = null;
}
@Override
public void onAnimationCancel(Animator animation) {
//background image view is thumbView
backgroundImageView.setAlpha(1f);
expandedImageView.setVisibility(View.GONE);
CurrentAnimator = null;
}
});
set.start();
CurrentAnimator = set;
}
@OnClick(R.id.media_map)
public void onFabShowMapsClicked() {
if (gpsObj != null && gpsObj.imageCoordsExists) {
Uri gmmIntentUri = Uri.parse("google.streetview:cbll=" + gpsObj.getDecLatitude() + "," + gpsObj.getDecLongitude());
Intent mapIntent = new Intent(Intent.ACTION_VIEW, gmmIntentUri);
mapIntent.setPackage("com.google.android.apps.maps");
startActivity(mapIntent);
}
}
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
switch (keyCode) {
case KeyEvent.KEYCODE_BACK:
if(isZoom) {
onZoomOutFabClicked();
return true;
}
}
return super.onKeyDown(keyCode,event);
}
} }

View file

@ -13,6 +13,9 @@ import android.view.ViewGroup;
import android.view.Window; import android.view.Window;
import android.widget.Button; import android.widget.Button;
import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.OnClick;
import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder; import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder;
import com.facebook.drawee.view.SimpleDraweeView; import com.facebook.drawee.view.SimpleDraweeView;
import com.facebook.imagepipeline.listener.RequestListener; import com.facebook.imagepipeline.listener.RequestListener;
@ -29,29 +32,33 @@ import fr.free.nrw.commons.R;
*/ */
public class SimilarImageDialogFragment extends DialogFragment { public class SimilarImageDialogFragment extends DialogFragment {
@BindView(R.id.orginalImage)
SimpleDraweeView originalImage; SimpleDraweeView originalImage;
@BindView(R.id.possibleImage)
SimpleDraweeView possibleImage; SimpleDraweeView possibleImage;
@BindView(R.id.postive_button)
Button positiveButton; Button positiveButton;
@BindView(R.id.negative_button)
Button negativeButton; Button negativeButton;
onResponse mOnResponse;//Implemented interface from shareActivity onResponse mOnResponse;//Implemented interface from shareActivity
Boolean gotResponse = false; Boolean gotResponse = false;
public SimilarImageDialogFragment() { public SimilarImageDialogFragment() {
} }
public interface onResponse{ public interface onResponse{
public void onPostiveResponse(); public void onPositiveResponse();
public void onNegativeResponse(); public void onNegativeResponse();
} }
@Override @Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_similar_image_dialog, container, false); View view = inflater.inflate(R.layout.fragment_similar_image_dialog, container, false);
ButterKnife.bind(this,view);
Set<RequestListener> requestListeners = new HashSet<>(); Set<RequestListener> requestListeners = new HashSet<>();
requestListeners.add(new RequestLoggingListener()); requestListeners.add(new RequestLoggingListener());
originalImage =(SimpleDraweeView) view.findViewById(R.id.orginalImage);
possibleImage =(SimpleDraweeView) view.findViewById(R.id.possibleImage);
positiveButton = (Button) view.findViewById(R.id.postive_button);
negativeButton = (Button) view.findViewById(R.id.negative_button);
originalImage.setHierarchy(GenericDraweeHierarchyBuilder originalImage.setHierarchy(GenericDraweeHierarchyBuilder
.newInstance(getResources()) .newInstance(getResources())
.setPlaceholderImage(VectorDrawableCompat.create(getResources(), .setPlaceholderImage(VectorDrawableCompat.create(getResources(),
@ -70,22 +77,6 @@ public class SimilarImageDialogFragment extends DialogFragment {
originalImage.setImageURI(Uri.fromFile(new File(getArguments().getString("originalImagePath")))); originalImage.setImageURI(Uri.fromFile(new File(getArguments().getString("originalImagePath"))));
possibleImage.setImageURI(Uri.fromFile(new File(getArguments().getString("possibleImagePath")))); possibleImage.setImageURI(Uri.fromFile(new File(getArguments().getString("possibleImagePath"))));
negativeButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
mOnResponse.onNegativeResponse();
gotResponse = true;
dismiss();
}
});
positiveButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
mOnResponse.onPostiveResponse();
gotResponse = true;
dismiss();
}
});
return view; return view;
} }
@ -105,8 +96,23 @@ public class SimilarImageDialogFragment extends DialogFragment {
@Override @Override
public void onDismiss(DialogInterface dialog) { public void onDismiss(DialogInterface dialog) {
// I user dismisses dialog by pressing outside the dialog. // I user dismisses dialog by pressing outside the dialog.
if(!gotResponse) if (!gotResponse) {
mOnResponse.onNegativeResponse(); mOnResponse.onNegativeResponse();
}
super.onDismiss(dialog); super.onDismiss(dialog);
} }
@OnClick(R.id.negative_button)
public void onNegativeButtonClicked() {
mOnResponse.onNegativeResponse();
gotResponse = true;
dismiss();
}
@OnClick(R.id.postive_button)
public void onPositiveButtonClicked() {
mOnResponse.onPositiveResponse();
gotResponse = true;
dismiss();
}
} }

View file

@ -1,21 +1,18 @@
package fr.free.nrw.commons.upload; package fr.free.nrw.commons.upload;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.graphics.Color; import android.graphics.Color;
import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.v4.view.ViewCompat; import android.support.v4.view.ViewCompat;
import android.support.v7.app.AlertDialog; import android.support.v7.app.AlertDialog;
import android.text.Editable; import android.text.Editable;
import android.text.Html;
import android.text.TextWatcher; import android.text.TextWatcher;
import android.util.Log; import android.text.method.LinkMovementMethod;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.Menu; import android.view.Menu;
import android.view.MenuInflater; import android.view.MenuInflater;
@ -23,7 +20,6 @@ import android.view.MenuItem;
import android.view.MotionEvent; import android.view.MotionEvent;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.view.inputmethod.InputMethodManager;
import android.widget.AdapterView; import android.widget.AdapterView;
import android.widget.ArrayAdapter; import android.widget.ArrayAdapter;
import android.widget.Button; import android.widget.Button;
@ -46,9 +42,9 @@ import fr.free.nrw.commons.R;
import fr.free.nrw.commons.Utils; import fr.free.nrw.commons.Utils;
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment; import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
import fr.free.nrw.commons.settings.Prefs; import fr.free.nrw.commons.settings.Prefs;
import fr.free.nrw.commons.utils.ViewUtil;
import timber.log.Timber; import timber.log.Timber;
import static android.view.MotionEvent.ACTION_DOWN;
import static android.view.MotionEvent.ACTION_UP; import static android.view.MotionEvent.ACTION_UP;
public class SingleUploadFragment extends CommonsDaggerSupportFragment { public class SingleUploadFragment extends CommonsDaggerSupportFragment {
@ -59,6 +55,7 @@ public class SingleUploadFragment extends CommonsDaggerSupportFragment {
@BindView(R.id.share_license_summary) TextView licenseSummaryView; @BindView(R.id.share_license_summary) TextView licenseSummaryView;
@BindView(R.id.licenseSpinner) Spinner licenseSpinner; @BindView(R.id.licenseSpinner) Spinner licenseSpinner;
@Inject @Named("default_preferences") SharedPreferences prefs; @Inject @Named("default_preferences") SharedPreferences prefs;
@Inject @Named("direct_nearby_upload_prefs") SharedPreferences directPrefs; @Inject @Named("direct_nearby_upload_prefs") SharedPreferences directPrefs;
@ -166,13 +163,13 @@ public class SingleUploadFragment extends CommonsDaggerSupportFragment {
titleEdit.setOnFocusChangeListener((v, hasFocus) -> { titleEdit.setOnFocusChangeListener((v, hasFocus) -> {
if (!hasFocus) { if (!hasFocus) {
hideKeyboard(v); ViewUtil.hideKeyboard(v);
} }
}); });
descEdit.setOnFocusChangeListener((v, hasFocus) -> { descEdit.setOnFocusChangeListener((v, hasFocus) -> {
if(!hasFocus){ if(!hasFocus){
hideKeyboard(v); ViewUtil.hideKeyboard(v);
} }
}); });
@ -181,12 +178,6 @@ public class SingleUploadFragment extends CommonsDaggerSupportFragment {
return rootView; return rootView;
} }
public void hideKeyboard(View view) {
Log.i("hide", "hideKeyboard: ");
InputMethodManager inputMethodManager =(InputMethodManager)getActivity().getSystemService(Activity.INPUT_METHOD_SERVICE);
inputMethodManager.hideSoftInputFromWindow(view.getWindowToken(), 0);
}
@Override @Override
public void onDestroyView() { public void onDestroyView() {
titleEdit.removeTextChangedListener(textWatcher); titleEdit.removeTextChangedListener(textWatcher);
@ -222,21 +213,9 @@ public class SingleUploadFragment extends CommonsDaggerSupportFragment {
setLicenseSummary(license); setLicenseSummary(license);
prefs.edit() prefs.edit()
.putString(Prefs.DEFAULT_LICENSE, license) .putString(Prefs.DEFAULT_LICENSE, license)
.commit(); .apply();
} }
@OnTouch(R.id.share_license_summary)
boolean showLicence(View view, MotionEvent motionEvent) {
if (motionEvent.getActionMasked() == ACTION_DOWN) {
Intent intent = new Intent();
intent.setAction(Intent.ACTION_VIEW);
intent.setData(Uri.parse(licenseUrlFor(license)));
startActivity(intent);
return true;
} else {
return false;
}
}
@OnClick(R.id.titleDescButton) @OnClick(R.id.titleDescButton)
void setTitleDescButton() { void setTitleDescButton() {
@ -294,8 +273,10 @@ public class SingleUploadFragment extends CommonsDaggerSupportFragment {
@SuppressLint("StringFormatInvalid") @SuppressLint("StringFormatInvalid")
private void setLicenseSummary(String license) { private void setLicenseSummary(String license) {
licenseSummaryView.setText(getString(R.string.share_license_summary, getString(Utils.licenseNameFor(license)))); String licenseHyperLink = "<a href='" + licenseUrlFor(license)+"'>"+ getString(Utils.licenseNameFor(license)) + "</a><br>";
} licenseSummaryView.setMovementMethod(LinkMovementMethod.getInstance());
licenseSummaryView.setText(Html.fromHtml(getString(R.string.share_license_summary, licenseHyperLink)));
}
@Override @Override
public void onActivityCreated(Bundle savedInstanceState) { public void onActivityCreated(Bundle savedInstanceState) {
@ -309,11 +290,8 @@ public class SingleUploadFragment extends CommonsDaggerSupportFragment {
super.onStop(); super.onStop();
// FIXME: Stops the keyboard from being shown 'stale' while moving out of this fragment into the next // FIXME: Stops the keyboard from being shown 'stale' while moving out of this fragment into the next
View target = getView().findFocus(); View target = getActivity().getCurrentFocus();
if (target != null) { ViewUtil.hideKeyboard(target);
InputMethodManager imm = (InputMethodManager) target.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(target.getWindowToken(), 0);
}
} }
@NonNull @NonNull
@ -354,6 +332,7 @@ public class SingleUploadFragment extends CommonsDaggerSupportFragment {
} }
} }
private void showInfoAlert (int titleStringID, int messageStringID){ private void showInfoAlert (int titleStringID, int messageStringID){
new AlertDialog.Builder(getContext()) new AlertDialog.Builder(getContext())
.setTitle(titleStringID) .setTitle(titleStringID)

View file

@ -91,7 +91,7 @@ public class UploadController {
* @param decimalCoords the coordinates in decimal. (e.g. "37.51136|-77.602615") * @param decimalCoords the coordinates in decimal. (e.g. "37.51136|-77.602615")
* @param onComplete the progress tracker * @param onComplete the progress tracker
*/ */
public void startUpload(String title, Uri mediaUri, String description, String mimeType, String source, String decimalCoords, ContributionUploadProgress onComplete) { public void startUpload(String title, Uri mediaUri, String description, String mimeType, String source, String decimalCoords, String wikiDataEntityId, ContributionUploadProgress onComplete) {
Contribution contribution; Contribution contribution;
//TODO: Modify this to include coords //TODO: Modify this to include coords
@ -101,6 +101,7 @@ public class UploadController {
contribution.setTag("mimeType", mimeType); contribution.setTag("mimeType", mimeType);
contribution.setSource(source); contribution.setSource(source);
contribution.setWikiDataEntityId(wikiDataEntityId);
//Calls the next overloaded method //Calls the next overloaded method
startUpload(contribution, onComplete); startUpload(contribution, onComplete);

View file

@ -7,7 +7,6 @@ import android.app.PendingIntent;
import android.content.ContentResolver; import android.content.ContentResolver;
import android.content.ContentValues; import android.content.ContentValues;
import android.content.Intent; import android.content.Intent;
import android.content.SharedPreferences;
import android.graphics.BitmapFactory; import android.graphics.BitmapFactory;
import android.os.Bundle; import android.os.Bundle;
import android.support.v4.app.NotificationCompat; import android.support.v4.app.NotificationCompat;
@ -23,7 +22,6 @@ import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Named;
import fr.free.nrw.commons.HandlerService; import fr.free.nrw.commons.HandlerService;
import fr.free.nrw.commons.R; import fr.free.nrw.commons.R;
@ -36,6 +34,7 @@ import fr.free.nrw.commons.contributions.ContributionsContentProvider;
import fr.free.nrw.commons.modifications.ModificationsContentProvider; import fr.free.nrw.commons.modifications.ModificationsContentProvider;
import fr.free.nrw.commons.mwapi.MediaWikiApi; import fr.free.nrw.commons.mwapi.MediaWikiApi;
import fr.free.nrw.commons.mwapi.UploadResult; import fr.free.nrw.commons.mwapi.UploadResult;
import fr.free.nrw.commons.wikidata.WikidataEditService;
import timber.log.Timber; import timber.log.Timber;
public class UploadService extends HandlerService<Contribution> { public class UploadService extends HandlerService<Contribution> {
@ -49,8 +48,8 @@ public class UploadService extends HandlerService<Contribution> {
public static final String EXTRA_CAMPAIGN = EXTRA_PREFIX + ".campaign"; public static final String EXTRA_CAMPAIGN = EXTRA_PREFIX + ".campaign";
@Inject MediaWikiApi mwApi; @Inject MediaWikiApi mwApi;
@Inject WikidataEditService wikidataEditService;
@Inject SessionManager sessionManager; @Inject SessionManager sessionManager;
@Inject @Named("default_preferences") SharedPreferences prefs;
@Inject ContributionDao contributionDao; @Inject ContributionDao contributionDao;
private NotificationManager notificationManager; private NotificationManager notificationManager;
@ -137,6 +136,7 @@ public class UploadService extends HandlerService<Contribution> {
@Override @Override
public void queue(int what, Contribution contribution) { public void queue(int what, Contribution contribution) {
Timber.d("Upload service queue has contribution with wiki data entity id as %s", contribution.getWikiDataEntityId());
switch (what) { switch (what) {
case ACTION_UPLOAD_FILE: case ACTION_UPLOAD_FILE:
@ -231,10 +231,10 @@ public class UploadService extends HandlerService<Contribution> {
Timber.d("Successfully revalidated token!"); Timber.d("Successfully revalidated token!");
} else { } else {
Timber.d("Unable to revalidate :("); Timber.d("Unable to revalidate :(");
// TODO: Put up a new notification, ask them to re-login
stopForeground(true); stopForeground(true);
Toast failureToast = Toast.makeText(this, R.string.authentication_failed, Toast.LENGTH_LONG); Toast failureToast = Toast.makeText(this, R.string.authentication_failed, Toast.LENGTH_LONG);
failureToast.show(); failureToast.show();
sessionManager.forceLogin(this);
return; return;
} }
} }
@ -253,6 +253,7 @@ public class UploadService extends HandlerService<Contribution> {
if (!resultStatus.equals("Success")) { if (!resultStatus.equals("Success")) {
showFailedNotification(contribution); showFailedNotification(contribution);
} else { } else {
wikidataEditService.createClaimWithLogging(contribution.getWikiDataEntityId(), filename);
contribution.setFilename(uploadResult.getCanonicalFilename()); contribution.setFilename(uploadResult.getCanonicalFilename());
contribution.setImageUrl(uploadResult.getImageUrl()); contribution.setImageUrl(uploadResult.getImageUrl());
contribution.setState(Contribution.STATE_COMPLETED); contribution.setState(Contribution.STATE_COMPLETED);

View file

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

View file

@ -0,0 +1,15 @@
package fr.free.nrw.commons.utils;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import fr.free.nrw.commons.category.QueryContinue;
public class ContinueUtils {
public static QueryContinue getQueryContinue(Node document) {
Element continueElement = (Element) document;
return new QueryContinue(continueElement.getAttribute("continue"),
continueElement.getAttribute("gcmcontinue"));
}
}

View file

@ -1,92 +0,0 @@
package fr.free.nrw.commons.utils;
import android.os.Environment;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import timber.log.Timber;
public class FileUtils {
/**
* Read and return the content of a resource file as string.
*
* @param fileName asset file's path (e.g. "/queries/nearby_query.rq")
* @return the content of the file
*/
public static String readFromResource(String fileName) throws IOException {
StringBuilder buffer = new StringBuilder();
BufferedReader reader = null;
try {
InputStream inputStream = FileUtils.class.getResourceAsStream(fileName);
if (inputStream == null) {
throw new FileNotFoundException(fileName);
}
reader = new BufferedReader(new InputStreamReader(inputStream, "UTF-8"));
String line;
while ((line = reader.readLine()) != null) {
buffer.append(line).append("\n");
}
} finally {
if (reader != null) {
reader.close();
}
}
return buffer.toString();
}
/**
* Deletes files.
* @param file context
*/
public static boolean deleteFile(File file) {
boolean deletedAll = true;
if (file != null) {
if (file.isDirectory()) {
String[] children = file.list();
for (String child : children) {
deletedAll = deleteFile(new File(file, child)) && deletedAll;
}
} else {
deletedAll = file.delete();
}
}
return deletedAll;
}
public static File createAndGetAppLogsFile(String logs) {
try {
File commonsAppDirectory = new File(Environment.getExternalStorageDirectory().toString() + "/CommonsApp");
if (!commonsAppDirectory.exists()) {
commonsAppDirectory.mkdir();
}
File logsFile = new File(commonsAppDirectory,"logs.txt");
if (logsFile.exists()) {
//old logs file is useless
logsFile.delete();
}
logsFile.createNewFile();
FileOutputStream outputStream = new FileOutputStream(logsFile);
OutputStreamWriter outputStreamWriter = new OutputStreamWriter(outputStream);
outputStreamWriter.append(logs);
outputStreamWriter.close();
outputStream.flush();
outputStream.close();
return logsFile;
} catch (IOException ioe) {
Timber.e(ioe);
return null;
}
}
}

View file

@ -1,10 +1,27 @@
package fr.free.nrw.commons.utils; package fr.free.nrw.commons.utils;
import android.app.WallpaperManager;
import android.content.Context;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.BitmapRegionDecoder; import android.graphics.BitmapRegionDecoder;
import android.graphics.Color; import android.graphics.Color;
import android.graphics.Rect; import android.graphics.Rect;
import android.net.Uri;
import android.support.annotation.Nullable;
import com.facebook.common.executors.CallerThreadExecutor;
import com.facebook.common.references.CloseableReference;
import com.facebook.datasource.DataSource;
import com.facebook.drawee.backends.pipeline.Fresco;
import com.facebook.imagepipeline.core.ImagePipeline;
import com.facebook.imagepipeline.datasource.BaseBitmapDataSubscriber;
import com.facebook.imagepipeline.image.CloseableImage;
import com.facebook.imagepipeline.request.ImageRequest;
import com.facebook.imagepipeline.request.ImageRequestBuilder;
import java.io.IOException;
import fr.free.nrw.commons.R;
import timber.log.Timber; import timber.log.Timber;
/** /**
@ -132,4 +149,52 @@ public class ImageUtils {
return isImageDark; return isImageDark;
} }
/**
* Downloads the image from the URL and sets it as the phone's wallpaper
* Fails silently if download or setting wallpaper fails.
* @param context
* @param imageUrl
*/
public static void setWallpaperFromImageUrl(Context context, Uri imageUrl) {
Timber.d("Trying to set wallpaper from url %s", imageUrl.toString());
ImageRequest imageRequest = ImageRequestBuilder
.newBuilderWithSource(imageUrl)
.setAutoRotateEnabled(true)
.build();
ImagePipeline imagePipeline = Fresco.getImagePipeline();
final DataSource<CloseableReference<CloseableImage>>
dataSource = imagePipeline.fetchDecodedImage(imageRequest, context);
dataSource.subscribe(new BaseBitmapDataSubscriber() {
@Override
public void onNewResultImpl(@Nullable Bitmap bitmap) {
if (dataSource.isFinished() && bitmap != null){
Timber.d("Bitmap loaded from url %s", imageUrl.toString());
setWallpaper(context, Bitmap.createBitmap(bitmap));
dataSource.close();
}
}
@Override
public void onFailureImpl(DataSource dataSource) {
Timber.d("Error getting bitmap from image url %s", imageUrl.toString());
if (dataSource != null) {
dataSource.close();
}
}
}, CallerThreadExecutor.getInstance());
}
private static void setWallpaper(Context context, Bitmap bitmap) {
WallpaperManager wallpaperManager = WallpaperManager.getInstance(context);
try {
wallpaperManager.setBitmap(bitmap);
ViewUtil.showLongToast(context, context.getString(R.string.wallpaper_set_successfully));
} catch (IOException e) {
Timber.e(e,"Error setting wallpaper");
}
}
} }

View file

@ -9,7 +9,7 @@ public class NetworkUtils {
public static boolean isInternetConnectionEstablished(Context context) { public static boolean isInternetConnectionEstablished(Context context) {
ConnectivityManager cm = ConnectivityManager cm =
(ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE); (ConnectivityManager)context.getApplicationContext().getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo activeNetwork = cm.getActiveNetworkInfo(); NetworkInfo activeNetwork = cm.getActiveNetworkInfo();
return activeNetwork != null && return activeNetwork != null &&

View file

@ -1,6 +1,7 @@
package fr.free.nrw.commons.utils; package fr.free.nrw.commons.utils;
import java.util.Comparator; import java.util.Comparator;
import java.util.Locale;
import info.debatty.java.stringsimilarity.Levenshtein; import info.debatty.java.stringsimilarity.Levenshtein;
@ -28,8 +29,8 @@ public class StringSortingUtils {
} }
private static double calculateSimilarity(String firstString, String secondString) { private static double calculateSimilarity(String firstString, String secondString) {
String longer = firstString.toLowerCase(); String longer = firstString.toLowerCase(Locale.getDefault());
String shorter = secondString.toLowerCase(); String shorter = secondString.toLowerCase(Locale.getDefault());
if (firstString.length() < secondString.length()) { if (firstString.length() < secondString.length()) {
longer = secondString; longer = secondString;

View file

@ -5,10 +5,15 @@ import android.content.Context;
import android.support.design.widget.Snackbar; import android.support.design.widget.Snackbar;
import android.view.Display; import android.view.Display;
import android.view.View; import android.view.View;
import android.view.inputmethod.InputMethodManager;
import android.widget.Toast; import android.widget.Toast;
public class ViewUtil { public class ViewUtil {
public static final String SHOWCASE_VIEW_ID_1 = "SHOWCASE_VIEW_ID_1";
public static final String SHOWCASE_VIEW_ID_2 = "SHOWCASE_VIEW_ID_2";
public static final String SHOWCASE_VIEW_ID_3 = "SHOWCASE_VIEW_ID_3";
public static void showSnackbar(View view, int messageResourceId) { public static void showSnackbar(View view, int messageResourceId) {
Snackbar.make(view, messageResourceId, Snackbar.LENGTH_SHORT).show(); Snackbar.make(view, messageResourceId, Snackbar.LENGTH_SHORT).show();
} }
@ -27,4 +32,14 @@ public class ViewUtil {
} }
} }
public static void hideKeyboard(View view){
if (view != null) {
InputMethodManager manager = (InputMethodManager) view.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
view.clearFocus();
if (manager != null) {
manager.hideSoftInputFromWindow(view.getWindowToken(), 0);
}
}
}
} }

View file

@ -0,0 +1,80 @@
package fr.free.nrw.commons.widget;
import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProvider;
import android.content.Context;
import android.widget.RemoteViews;
import com.prof.rssparser.Article;
import com.prof.rssparser.Parser;
import com.squareup.picasso.Picasso;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import java.util.ArrayList;
import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.R;
/**
* Implementation of App Widget functionality.
*/
public class PicOfDayAppWidget extends AppWidgetProvider {
static void updateAppWidget(Context context, AppWidgetManager appWidgetManager,
int appWidgetId) {
// Construct the RemoteViews object
RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.pic_of_day_app_widget);
String urlString = BuildConfig.WIKIMEDIA_API_POTD;
Parser parser = new Parser();
parser.execute(urlString);
parser.onFinish(new Parser.OnTaskCompleted() {
@Override
public void onTaskCompleted(ArrayList<Article> list) {
String desc = list.get(list.size() - 1).getDescription();
if (desc != null) {
Document document = Jsoup.parse(desc);
Elements elements = document.select("img");
String imageUrl = elements.get(0).attr("src");
if (imageUrl != null && imageUrl.length() > 0) {
Picasso.get().load(imageUrl).into(views, R.id.appwidget_image, new int[]{appWidgetId});
}
}
}
@Override
public void onError() {
}
});
// Instruct the widget manager to update the widget
appWidgetManager.updateAppWidget(appWidgetId, views);
}
@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
// There may be multiple widgets active, so update all of them
for (int appWidgetId : appWidgetIds) {
updateAppWidget(context, appWidgetManager, appWidgetId);
}
}
@Override
public void onEnabled(Context context) {
// Enter relevant functionality for when the first widget is created
}
@Override
public void onDisabled(Context context) {
// Enter relevant functionality for when the last widget is disabled
}
}

View file

@ -0,0 +1,16 @@
package fr.free.nrw.commons.wikidata;
public abstract class WikidataEditListener {
protected WikidataP18EditListener wikidataP18EditListener;
public abstract void onSuccessfulWikidataEdit();
public void setAuthenticationStateListener(WikidataP18EditListener wikidataP18EditListener) {
this.wikidataP18EditListener = wikidataP18EditListener;
}
public interface WikidataP18EditListener {
void onWikidataEditSuccessful();
}
}

View file

@ -0,0 +1,20 @@
package fr.free.nrw.commons.wikidata;
/**
* Listener for wikidata edits
*/
public class WikidataEditListenerImpl extends WikidataEditListener {
public WikidataEditListenerImpl() {
}
/**
* Fired when wikidata P18 edit is successful. If there's an active listener, then it is fired
*/
@Override
public void onSuccessfulWikidataEdit() {
if (wikidataP18EditListener != null) {
wikidataP18EditListener.onWikidataEditSuccessful();
}
}
}

View file

@ -0,0 +1,134 @@
package fr.free.nrw.commons.wikidata;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.SharedPreferences;
import java.util.Locale;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import fr.free.nrw.commons.utils.ViewUtil;
import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers;
import timber.log.Timber;
/**
* This class is meant to handle the Wikidata edits made through the app
* It will talk with MediaWikiApi to make necessary API calls, log the edits and fire listeners
* on successful edits
*/
@Singleton
public class WikidataEditService {
private final Context context;
private final MediaWikiApi mediaWikiApi;
private final WikidataEditListener wikidataEditListener;
private final SharedPreferences directPrefs;
@Inject
public WikidataEditService(Context context,
MediaWikiApi mediaWikiApi,
WikidataEditListener wikidataEditListener,
@Named("direct_nearby_upload_prefs") SharedPreferences directPrefs) {
this.context = context;
this.mediaWikiApi = mediaWikiApi;
this.wikidataEditListener = wikidataEditListener;
this.directPrefs = directPrefs;
}
/**
* Create a P18 claim and log the edit with custom tag
* @param wikidataEntityId
* @param fileName
*/
public void createClaimWithLogging(String wikidataEntityId, String fileName) {
if(wikidataEntityId == null
|| fileName == null) {
return;
}
editWikidataProperty(wikidataEntityId, fileName);
}
/**
* Edits the wikidata entity by adding the P18 property to it.
* Adding the P18 edit requires calling the wikidata API to create a claim against the entity
*
* @param wikidataEntityId
* @param fileName
*/
@SuppressLint("CheckResult")
private void editWikidataProperty(String wikidataEntityId, String fileName) {
Timber.d("Upload successful with wiki data entity id as %s", wikidataEntityId);
Timber.d("Attempting to edit Wikidata property %s", wikidataEntityId);
Observable.fromCallable(() -> {
String propertyValue = getFileName(fileName);
return mediaWikiApi.wikidatCreateClaim(wikidataEntityId, "P18", "value", propertyValue);
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(revisionId -> handleClaimResult(wikidataEntityId, revisionId), throwable -> {
Timber.e(throwable, "Error occurred while making claim");
ViewUtil.showLongToast(context, context.getString(R.string.wikidata_edit_failure));
});
}
private void handleClaimResult(String wikidataEntityId, String revisionId) {
if (revisionId != null) {
wikidataEditListener.onSuccessfulWikidataEdit();
showSuccessToast();
logEdit(revisionId);
} else {
Timber.d("Unable to make wiki data edit for entity %s", wikidataEntityId);
ViewUtil.showLongToast(context, context.getString(R.string.wikidata_edit_failure));
}
}
/**
* Log the Wikidata edit by adding Wikimedia Commons App tag to the edit
* @param revisionId
*/
@SuppressLint("CheckResult")
private void logEdit(String revisionId) {
Observable.fromCallable(() -> mediaWikiApi.addWikidataEditTag(revisionId))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(result -> {
if (result) {
Timber.d("Wikidata edit was tagged successfully");
} else {
Timber.d("Wikidata edit couldn't be tagged");
}
}, throwable -> {
Timber.e(throwable, "Error occurred while adding tag to the edit");
});
}
/**
* Show a success toast when the edit is made successfully
*/
private void showSuccessToast() {
String title = directPrefs.getString("Title", "");
String successStringTemplate = context.getString(R.string.successful_wikidata_edit);
String successMessage = String.format(Locale.getDefault(), successStringTemplate, title);
ViewUtil.showLongToast(context, successMessage);
}
/**
* Formats and returns the filename as accepted by the wiki base API
* https://www.mediawiki.org/wiki/Wikibase/API#wbcreateclaim
*
* @param fileName
* @return
*/
private String getFileName(String fileName) {
fileName = String.format("\"%s\"", fileName.replace("File:", ""));
Timber.d("Wikidata property name is %s", fileName);
return fileName;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 422 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 412 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 257 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 249 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 486 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 470 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 737 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 731 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 954 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 925 B

View file

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M7.41,15.41L12,10.83l4.59,4.58L18,14l-6,-6 -6,6z"/>
</vector>

View file

@ -1,5 +1,5 @@
<vector android:alpha="0.84" android:height="32dp" <vector android:alpha="0.84" android:height="24dp"
android:viewportHeight="24.0" android:viewportWidth="24.0" android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="32dp" xmlns:android="http://schemas.android.com/apk/res/android"> android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#ffffffff" android:pathData="M18,16.08c-0.76,0 -1.44,0.3 -1.96,0.77L8.91,12.7c0.05,-0.23 0.09,-0.46 0.09,-0.7s-0.04,-0.47 -0.09,-0.7l7.05,-4.11c0.54,0.5 1.25,0.81 2.04,0.81 1.66,0 3,-1.34 3,-3s-1.34,-3 -3,-3 -3,1.34 -3,3c0,0.24 0.04,0.47 0.09,0.7L8.04,9.81C7.5,9.31 6.79,9 6,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3c0.79,0 1.5,-0.31 2.04,-0.81l7.12,4.16c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.61 1.31,2.92 2.92,2.92 1.61,0 2.92,-1.31 2.92,-2.92s-1.31,-2.92 -2.92,-2.92z"/> <path android:fillColor="#ffffffff" android:pathData="M18,16.08c-0.76,0 -1.44,0.3 -1.96,0.77L8.91,12.7c0.05,-0.23 0.09,-0.46 0.09,-0.7s-0.04,-0.47 -0.09,-0.7l7.05,-4.11c0.54,0.5 1.25,0.81 2.04,0.81 1.66,0 3,-1.34 3,-3s-1.34,-3 -3,-3 -3,1.34 -3,3c0,0.24 0.04,0.47 0.09,0.7L8.04,9.81C7.5,9.31 6.79,9 6,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3c0.79,0 1.5,-0.31 2.04,-0.81l7.12,4.16c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.61 1.31,2.92 2.92,2.92 1.61,0 2.92,-1.31 2.92,-2.92s-1.31,-2.92 -2.92,-2.92z"/>
</vector> </vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M12,17.27L18.18,21l-1.64,-7.03L22,9.24l-7.19,-0.61L12,2 9.19,8.63 2,9.24l5.46,4.73L5.82,21z"/>
</vector>

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