Merge branch 'master' of git://github.com/commons-app/apps-android-commons
|
|
@ -19,12 +19,13 @@ android:
|
|||
components:
|
||||
- tools
|
||||
- platform-tools
|
||||
- build-tools-26.0.2
|
||||
- build-tools-27.0.0
|
||||
- extra-google-m2repository
|
||||
- extra-android-m2repository
|
||||
- ${ANDROID_TARGET}
|
||||
- android-25
|
||||
- android-26
|
||||
- android-27
|
||||
- sys-img-${ANDROID_ABI}-${ANDROID_TARGET}
|
||||
licenses:
|
||||
- 'android-sdk-license-.+'
|
||||
|
|
|
|||
|
|
@ -1,5 +1,14 @@
|
|||
# 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
|
||||
- New Nearby Places UI with direct uploads (and associated category suggestions)
|
||||
- Added two-factor authentication login
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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.}
|
||||
|
||||
## Tests performed
|
||||
## Tests performed (required)
|
||||
|
||||
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
|
||||
## Screenshots showing what changed (optional)
|
||||
|
||||
{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._
|
||||
|
|
@ -7,10 +7,12 @@ apply from: 'quality.gradle'
|
|||
apply plugin: 'com.getkeepsafe.dexcount'
|
||||
|
||||
dependencies {
|
||||
implementation 'com.squareup.picasso:picasso:2.71828'
|
||||
implementation 'com.prof.rssparser:rssparser:1.1'
|
||||
implementation 'com.github.nicolas-raoul:Quadtree:ac16ea8035bf07'
|
||||
implementation 'fr.avianey.com.viewpagerindicator:library:2.4.1.1@aar'
|
||||
implementation 'in.yuvi:http.fluent:1.3'
|
||||
implementation 'com.android.volley:volley:1.0.0'
|
||||
implementation 'com.github.chrisbanes:PhotoView:2.0.0'
|
||||
implementation 'ch.acra:acra:4.9.2'
|
||||
implementation 'org.mediawiki:api:1.3'
|
||||
implementation 'commons-codec:commons-codec:1.10'
|
||||
|
|
@ -18,69 +20,59 @@ dependencies {
|
|||
implementation 'com.google.code.gson:gson:2.8.1'
|
||||
implementation 'com.jakewharton.timber:timber:4.5.1'
|
||||
implementation 'info.debatty:java-string-similarity:0.24'
|
||||
implementation ('com.mapbox.mapboxsdk:mapbox-android-sdk:5.4.1@aar'){
|
||||
transitive=true
|
||||
implementation 'com.borjabravo:readmoretextview:2.1.0'
|
||||
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:appcompat-v7:$SUPPORT_LIB_VERSION"
|
||||
implementation "com.android.support:design:$SUPPORT_LIB_VERSION"
|
||||
implementation "com.android.support:customtabs:$SUPPORT_LIB_VERSION"
|
||||
|
||||
implementation "com.android.support:cardview-v7:$SUPPORT_LIB_VERSION"
|
||||
|
||||
implementation "com.jakewharton:butterknife:$BUTTERKNIFE_VERSION"
|
||||
kapt "com.jakewharton:butterknife-compiler:$BUTTERKNIFE_VERSION"
|
||||
|
||||
implementation 'com.squareup.okhttp3:okhttp:3.8.1'
|
||||
implementation 'com.squareup.okhttp3:okhttp:3.9.1'
|
||||
implementation 'com.squareup.okio:okio:1.13.0'
|
||||
|
||||
implementation 'io.reactivex.rxjava2:rxandroid:2.0.1'
|
||||
// Because RxAndroid releases are few and far between, it is recommended you also
|
||||
// explicitly depend on RxJava's latest version for bug fixes and new features.
|
||||
implementation 'com.android.support:multidex:1.0.3'
|
||||
|
||||
testImplementation "org.robolectric:multidex:3.4.2"
|
||||
|
||||
implementation 'io.reactivex.rxjava2:rxjava:2.1.2'
|
||||
implementation 'com.jakewharton.rxbinding2:rxbinding: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-design:2.0.0'
|
||||
|
||||
implementation 'org.jsoup:jsoup:1.11.3'
|
||||
implementation 'com.facebook.fresco:fresco:1.5.0'
|
||||
implementation 'com.facebook.stetho:stetho:1.5.0'
|
||||
|
||||
implementation "com.google.dagger:dagger:$DAGGER_VERSION"
|
||||
implementation "com.google.dagger:dagger-android-support:$DAGGER_VERSION"
|
||||
|
||||
kapt "com.google.dagger:dagger-android-processor:$DAGGER_VERSION"
|
||||
kapt "com.google.dagger:dagger-compiler:$DAGGER_VERSION"
|
||||
|
||||
testImplementation 'org.robolectric:multidex:3.4.2'
|
||||
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 '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'
|
||||
|
||||
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.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"
|
||||
releaseImplementation "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 {
|
||||
|
|
@ -91,8 +83,8 @@ android {
|
|||
|
||||
defaultConfig {
|
||||
applicationId 'fr.free.nrw.commons'
|
||||
versionCode 83
|
||||
versionName '2.7.0'
|
||||
versionCode 85
|
||||
versionName '2.7.2'
|
||||
setProperty("archivesBaseName", "app-commons-v$versionName-" + getBranchName())
|
||||
|
||||
minSdkVersion project.minSdkVersion
|
||||
|
|
@ -121,7 +113,7 @@ android {
|
|||
buildTypes {
|
||||
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.
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt', 'proguard-glide.txt'
|
||||
}
|
||||
debug {
|
||||
applicationIdSuffix ".debug"
|
||||
|
|
@ -133,7 +125,9 @@ android {
|
|||
flavorDimensions 'tier'
|
||||
productFlavors {
|
||||
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", "WIKIDATA_API_HOST", "\"https://www.wikidata.org/w/api.php\""
|
||||
buildConfigField "String", "WIKIMEDIA_FORGE_API_HOST", "\"https://tools.wmflabs.org/\""
|
||||
buildConfigField "String", "IMAGE_URL_BASE", "\"https://upload.wikimedia.org/wikipedia/commons\""
|
||||
buildConfigField "String", "HOME_URL", "\"https://commons.wikimedia.org/wiki/\""
|
||||
|
|
@ -149,7 +143,9 @@ android {
|
|||
|
||||
beta {
|
||||
// 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", "WIKIDATA_API_HOST", "\"https://www.wikidata.org/w/api.php\""
|
||||
buildConfigField "String", "WIKIMEDIA_FORGE_API_HOST", "\"https://tools.wmflabs.org/\""
|
||||
buildConfigField "String", "IMAGE_URL_BASE", "\"https://upload.beta.wmflabs.org/wikipedia/commons\""
|
||||
buildConfigField "String", "HOME_URL", "\"https://commons.wikimedia.beta.wmflabs.org/wiki/\""
|
||||
|
|
|
|||
BIN
app/libs/java-json.jar
Normal file
9
app/proguard-glide.txt
Normal 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
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
-dontobfuscate
|
||||
-keep class org.apache.http.** { *; }
|
||||
-dontwarn org.apache.http.**
|
||||
-keep class fr.free.nrw.commons.upload.MwVolleyApi$Page {*;}
|
||||
-keep class android.support.v7.widget.ShareActionProvider { *; }
|
||||
|
|
@ -18,7 +18,7 @@ task checkstyle(type: Checkstyle) {
|
|||
reports {
|
||||
html {
|
||||
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
|
||||
html.enabled = true
|
||||
xml {
|
||||
destination "${project.buildDir}/reports/pmd/pmd.xml"
|
||||
destination file("${project.buildDir}/reports/pmd/pmd.xml")
|
||||
}
|
||||
html {
|
||||
destination "${project.buildDir}/reports/pmd/pmd.html"
|
||||
destination file("${project.buildDir}/reports/pmd/pmd.html")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
<uses-permission android:name="android.permission.MANAGE_DOCUMENTS" />
|
||||
<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.SET_WALLPAPER"/>
|
||||
|
||||
<!-- Needed only if your app targets Android 5.0 (API level 21) or higher. -->
|
||||
<uses-feature android:name="android.hardware.location.gps" />
|
||||
|
|
@ -91,6 +92,11 @@
|
|||
android:name=".notification.NotificationActivity"
|
||||
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
|
||||
|
|
@ -159,6 +165,16 @@
|
|||
android:label="@string/provider_categories"
|
||||
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>
|
||||
|
||||
</manifest>
|
||||
|
|
|
|||
|
|
@ -9,9 +9,6 @@ import android.os.Bundle;
|
|||
import android.text.Html;
|
||||
import android.text.SpannableString;
|
||||
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.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
|
|
@ -20,7 +17,6 @@ import android.widget.ArrayAdapter;
|
|||
import android.widget.LinearLayout;
|
||||
import android.widget.Spinner;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import butterknife.BindView;
|
||||
import butterknife.ButterKnife;
|
||||
|
|
@ -28,8 +24,6 @@ import butterknife.OnClick;
|
|||
import fr.free.nrw.commons.theme.NavigationBaseActivity;
|
||||
import fr.free.nrw.commons.ui.widget.HtmlTextView;
|
||||
|
||||
import static android.widget.Toast.LENGTH_SHORT;
|
||||
|
||||
/**
|
||||
* Represents about screen of this app
|
||||
*/
|
||||
|
|
@ -135,9 +129,10 @@ public class AboutActivity extends NavigationBaseActivity {
|
|||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
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();
|
||||
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");
|
||||
startActivity(Intent.createChooser(sendIntent, "Share app via..."));
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
package fr.free.nrw.commons;
|
||||
|
||||
import android.app.Application;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
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.di.ApplicationlessInjection;
|
||||
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.schedulers.Schedulers;
|
||||
import timber.log.Timber;
|
||||
|
|
|
|||
|
|
@ -61,8 +61,8 @@ public class MediaDataExtractor {
|
|||
}
|
||||
|
||||
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){
|
||||
Timber.d(e.getMessage());
|
||||
|
|
|
|||
|
|
@ -178,6 +178,7 @@ public class Utils {
|
|||
}
|
||||
|
||||
public static void handleWebUrl(Context context, Uri url) {
|
||||
Timber.d("Launching web url %s", url.toString());
|
||||
Intent browserIntent = new Intent(Intent.ACTION_VIEW, url);
|
||||
if (browserIntent.resolveActivity(context.getPackageManager()) == null) {
|
||||
Toast toast = Toast.makeText(context, context.getString(R.string.no_web_browser), LENGTH_SHORT);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package fr.free.nrw.commons;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.view.PagerAdapter;
|
||||
import android.view.LayoutInflater;
|
||||
|
|
@ -9,6 +10,7 @@ import android.widget.TextView;
|
|||
|
||||
import butterknife.ButterKnife;
|
||||
import butterknife.OnClick;
|
||||
import butterknife.Optional;
|
||||
|
||||
public class WelcomePagerAdapter extends PagerAdapter {
|
||||
static final int[] PAGE_LAYOUTS = new int[]{
|
||||
|
|
@ -20,6 +22,7 @@ public class WelcomePagerAdapter extends PagerAdapter {
|
|||
};
|
||||
private static final int PAGE_FINAL = 4;
|
||||
private Callback callback;
|
||||
private ViewGroup container;
|
||||
|
||||
/**
|
||||
* Changes callback to provided one
|
||||
|
|
@ -53,6 +56,7 @@ public class WelcomePagerAdapter extends PagerAdapter {
|
|||
|
||||
@Override
|
||||
public Object instantiateItem(ViewGroup container, int position) {
|
||||
this.container=container;
|
||||
LayoutInflater inflater = LayoutInflater.from(container.getContext());
|
||||
ViewGroup layout = (ViewGroup) inflater.inflate(PAGE_LAYOUTS[position], container, false);
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@ import android.accounts.Account;
|
|||
import android.accounts.AccountAuthenticatorActivity;
|
||||
import android.accounts.AccountAuthenticatorResponse;
|
||||
import android.accounts.AccountManager;
|
||||
import android.app.Activity;
|
||||
import android.app.ProgressDialog;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.net.Uri;
|
||||
|
|
@ -19,18 +19,16 @@ import android.support.v4.content.ContextCompat;
|
|||
import android.support.v7.app.AppCompatDelegate;
|
||||
import android.text.Editable;
|
||||
import android.text.TextWatcher;
|
||||
import android.util.Log;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Locale;
|
||||
|
||||
import javax.inject.Inject;
|
||||
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.theme.NavigationBaseActivity;
|
||||
import fr.free.nrw.commons.ui.widget.HtmlTextView;
|
||||
import fr.free.nrw.commons.utils.ViewUtil;
|
||||
import io.reactivex.Observable;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
|
|
@ -85,6 +84,10 @@ public class LoginActivity extends AccountAuthenticatorActivity {
|
|||
private LoginTextWatcher textWatcher = new LoginTextWatcher();
|
||||
|
||||
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";
|
||||
|
||||
@Override
|
||||
|
|
@ -106,14 +109,14 @@ public class LoginActivity extends AccountAuthenticatorActivity {
|
|||
usernameEdit.addTextChangedListener(textWatcher);
|
||||
usernameEdit.setOnFocusChangeListener((v, hasFocus) -> {
|
||||
if (!hasFocus) {
|
||||
hideKeyboard(v);
|
||||
ViewUtil.hideKeyboard(v);
|
||||
}
|
||||
});
|
||||
|
||||
passwordEdit.addTextChangedListener(textWatcher);
|
||||
passwordEdit.setOnFocusChangeListener((v, hasFocus) -> {
|
||||
if (!hasFocus) {
|
||||
hideKeyboard(v);
|
||||
ViewUtil.hideKeyboard(v);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -125,13 +128,18 @@ public class LoginActivity extends AccountAuthenticatorActivity {
|
|||
|
||||
forgotPasswordText.setOnClickListener(view -> forgotPassword());
|
||||
|
||||
if(BuildConfig.FLAVOR == "beta"){
|
||||
if(BuildConfig.FLAVOR.equals("beta")){
|
||||
loginCredentials.setText(getString(R.string.login_credential));
|
||||
} else {
|
||||
loginCredentials.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
public static void startYourself(Context context) {
|
||||
Intent intent = new Intent(context, LoginActivity.class);
|
||||
context.startActivity(intent);
|
||||
}
|
||||
|
||||
private void forgotPassword() {
|
||||
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\\"));
|
||||
}
|
||||
|
||||
public void hideKeyboard(View view) {
|
||||
InputMethodManager inputMethodManager =(InputMethodManager)this.getSystemService(Activity.INPUT_METHOD_SERVICE);
|
||||
inputMethodManager.hideSoftInputFromWindow(view.getWindowToken(), 0);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected void onPostCreate(Bundle savedInstanceState) {
|
||||
super.onPostCreate(savedInstanceState);
|
||||
|
|
@ -160,7 +162,10 @@ public class LoginActivity extends AccountAuthenticatorActivity {
|
|||
WelcomeActivity.startYourself(this);
|
||||
prefs.edit().putBoolean("firstrun", false).apply();
|
||||
}
|
||||
if (sessionManager.getCurrentAccount() != null) {
|
||||
|
||||
if (sessionManager.getCurrentAccount() != null
|
||||
&& sessionManager.isUserLoggedIn()
|
||||
&& sessionManager.getCachedAuthCookie() != null) {
|
||||
startMainActivity();
|
||||
}
|
||||
}
|
||||
|
|
@ -215,6 +220,8 @@ public class LoginActivity extends AccountAuthenticatorActivity {
|
|||
handlePassResult(username, password);
|
||||
} else {
|
||||
loginCurrentlyInProgress = false;
|
||||
errorMessageShown = true;
|
||||
resultantError = result;
|
||||
handleOtherResults(result);
|
||||
}
|
||||
}
|
||||
|
|
@ -266,18 +273,18 @@ public class LoginActivity extends AccountAuthenticatorActivity {
|
|||
if (result.equals("NetworkFailure")) {
|
||||
// Matches NetworkFailure which is created by the doInBackground method
|
||||
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
|
||||
showMessageAndCancelDialog(R.string.login_failed_username);
|
||||
showMessageAndCancelDialog(R.string.login_failed_wrong_credentials);
|
||||
emptySensitiveEditFields();
|
||||
} else if (result.toLowerCase().contains("wrongpassword".toLowerCase())) {
|
||||
} else if (result.toLowerCase(Locale.getDefault()).contains("wrongpassword".toLowerCase())) {
|
||||
// Matches wrongpassword, wrongpasswordempty
|
||||
showMessageAndCancelDialog(R.string.login_failed_password);
|
||||
showMessageAndCancelDialog(R.string.login_failed_wrong_credentials);
|
||||
emptySensitiveEditFields();
|
||||
} else if (result.toLowerCase().contains("throttle".toLowerCase())) {
|
||||
} else if (result.toLowerCase(Locale.getDefault()).contains("throttle".toLowerCase())) {
|
||||
// Matches unknown throttle error codes
|
||||
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
|
||||
showMessageAndCancelDialog(R.string.login_failed_blocked);
|
||||
} else if (result.equals("2FA")) {
|
||||
|
|
@ -341,15 +348,22 @@ public class LoginActivity extends AccountAuthenticatorActivity {
|
|||
protected void onSaveInstanceState(Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
outState.putBoolean(LOGING_IN, loginCurrentlyInProgress);
|
||||
outState.putBoolean(ERROR_MESSAGE_SHOWN, errorMessageShown);
|
||||
outState.putString(RESULTANT_ERROR, resultantError);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onRestoreInstanceState(Bundle savedInstanceState) {
|
||||
super.onRestoreInstanceState(savedInstanceState);
|
||||
loginCurrentlyInProgress = savedInstanceState.getBoolean(LOGING_IN, false);
|
||||
errorMessageShown = savedInstanceState.getBoolean(ERROR_MESSAGE_SHOWN, false);
|
||||
if(loginCurrentlyInProgress){
|
||||
performLogin();
|
||||
}
|
||||
if(errorMessageShown){
|
||||
resultantError = savedInstanceState.getString(RESULTANT_ERROR);
|
||||
handleOtherResults(resultantError);
|
||||
}
|
||||
}
|
||||
|
||||
public void askUserForTwoFactorAuth() {
|
||||
|
|
@ -361,8 +375,10 @@ public class LoginActivity extends AccountAuthenticatorActivity {
|
|||
|
||||
public void showMessageAndCancelDialog(@StringRes int resId) {
|
||||
showMessage(resId, R.color.secondaryDarkColor);
|
||||
if(progressDialog != null){
|
||||
progressDialog.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
public void showSuccessAndDismissDialog() {
|
||||
showMessage(R.string.login_success, R.color.primaryDarkColor);
|
||||
|
|
|
|||
|
|
@ -61,13 +61,11 @@ public class SessionManager {
|
|||
}
|
||||
|
||||
public String getAuthCookie() {
|
||||
boolean isLoggedIn = sharedPreferences.getBoolean("isUserLoggedIn", false);
|
||||
|
||||
if (!isLoggedIn) {
|
||||
if (!isUserLoggedIn()) {
|
||||
Timber.e("User is not logged in");
|
||||
return null;
|
||||
} else {
|
||||
String authCookie = sharedPreferences.getString("getAuthCookie", null);
|
||||
String authCookie = getCachedAuthCookie();
|
||||
if (authCookie == null) {
|
||||
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() {
|
||||
AccountManager accountManager = AccountManager.get(context);
|
||||
Account[] allAccounts = accountManager.getAccountsByType(ACCOUNT_TYPE);
|
||||
|
|
|
|||
|
|
@ -7,18 +7,25 @@ import java.util.ArrayList;
|
|||
import java.util.Arrays;
|
||||
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;
|
||||
|
||||
@Singleton
|
||||
public class CacheController {
|
||||
|
||||
private final GpsCategoryModel gpsCategoryModel;
|
||||
private final QuadTree<List<String>> quadTree;
|
||||
private double x, y;
|
||||
private QuadTree<List<String>> quadTree;
|
||||
private double xMinus, xPlus, yMinus, yPlus;
|
||||
|
||||
private static final int EARTH_RADIUS = 6378137;
|
||||
|
||||
public CacheController() {
|
||||
@Inject
|
||||
CacheController(GpsCategoryModel gpsCategoryModel) {
|
||||
this.gpsCategoryModel = gpsCategoryModel;
|
||||
quadTree = new QuadTree<>(-180, -90, +180, +90);
|
||||
}
|
||||
|
||||
|
|
@ -31,8 +38,8 @@ public class CacheController {
|
|||
|
||||
public void cacheCategory() {
|
||||
List<String> pointCatList = new ArrayList<>();
|
||||
if (MwVolleyApi.GpsCatExists.getGpsCatExists()) {
|
||||
pointCatList.addAll(MwVolleyApi.getGpsCat());
|
||||
if (gpsCategoryModel.getGpsCatExists()) {
|
||||
pointCatList.addAll(gpsCategoryModel.getCategoryList());
|
||||
Timber.d("Categories being cached: %s", pointCatList);
|
||||
} else {
|
||||
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
|
||||
public void convertCoordRange() {
|
||||
private void convertCoordRange() {
|
||||
//Position, decimal degrees
|
||||
double lat = y;
|
||||
double lon = x;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package fr.free.nrw.commons.category;
|
||||
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Bundle;
|
||||
import android.support.v7.app.AlertDialog;
|
||||
|
|
@ -10,14 +9,12 @@ import android.support.v7.widget.RecyclerView;
|
|||
import android.text.Editable;
|
||||
import android.text.TextUtils;
|
||||
import android.text.TextWatcher;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
|
|
@ -42,9 +39,9 @@ import butterknife.ButterKnife;
|
|||
import fr.free.nrw.commons.R;
|
||||
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
|
||||
import fr.free.nrw.commons.mwapi.MediaWikiApi;
|
||||
import fr.free.nrw.commons.upload.MwVolleyApi;
|
||||
import fr.free.nrw.commons.upload.SingleUploadFragment;
|
||||
import fr.free.nrw.commons.upload.GpsCategoryModel;
|
||||
import fr.free.nrw.commons.utils.StringSortingUtils;
|
||||
import fr.free.nrw.commons.utils.ViewUtil;
|
||||
import io.reactivex.Observable;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
|
|
@ -76,6 +73,7 @@ public class CategorizationFragment extends CommonsDaggerSupportFragment {
|
|||
@Inject @Named("prefs") SharedPreferences prefsPrefs;
|
||||
@Inject @Named("direct_nearby_upload_prefs") SharedPreferences directPrefs;
|
||||
@Inject CategoryDao categoryDao;
|
||||
@Inject GpsCategoryModel gpsCategoryModel;
|
||||
|
||||
private RVRendererAdapter<CategoryItem> categoriesAdapter;
|
||||
private OnCategoriesSaveHandler onCategoriesSaveHandler;
|
||||
|
|
@ -118,7 +116,7 @@ public class CategorizationFragment extends CommonsDaggerSupportFragment {
|
|||
|
||||
categoriesFilter.setOnFocusChangeListener((v, hasFocus) -> {
|
||||
if (!hasFocus) {
|
||||
hideKeyboard(v);
|
||||
ViewUtil.hideKeyboard(v);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -130,11 +128,6 @@ public class CategorizationFragment extends CommonsDaggerSupportFragment {
|
|||
return rootView;
|
||||
}
|
||||
|
||||
public void hideKeyboard(View view) {
|
||||
InputMethodManager inputMethodManager = (InputMethodManager) getActivity().getSystemService(Activity.INPUT_METHOD_SERVICE);
|
||||
inputMethodManager.hideSoftInputFromWindow(view.getWindowToken(), 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
categoriesFilter.removeTextChangedListener(textWatcher);
|
||||
|
|
@ -261,7 +254,6 @@ public class CategorizationFragment extends CommonsDaggerSupportFragment {
|
|||
}
|
||||
|
||||
private Observable<CategoryItem> defaultCategories() {
|
||||
|
||||
Observable<CategoryItem> directCat = directCategories();
|
||||
if (hasDirectCategories) {
|
||||
Timber.d("Image has direct Cat");
|
||||
|
|
@ -295,9 +287,7 @@ public class CategorizationFragment extends CommonsDaggerSupportFragment {
|
|||
}
|
||||
|
||||
private Observable<CategoryItem> gpsCategories() {
|
||||
return Observable.fromIterable(
|
||||
MwVolleyApi.GpsCatExists.getGpsCatExists()
|
||||
? MwVolleyApi.getGpsCat() : new ArrayList<>())
|
||||
return Observable.fromIterable(gpsCategoryModel.getCategoryList())
|
||||
.map(name -> new CategoryItem(name, false));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -105,6 +105,7 @@ public class CategoryDao {
|
|||
return items;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
Category fromCursor(Cursor cursor) {
|
||||
// Hardcoding column positions!
|
||||
return new Category(
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -45,6 +45,7 @@ public class Contribution extends Media {
|
|||
private long transferred;
|
||||
private String decimalCoords;
|
||||
private boolean isMultiple;
|
||||
private String wikiDataEntityId;
|
||||
|
||||
public Contribution(Uri contentUri, String filename, Uri localUri, String imageUrl, Date timestamp,
|
||||
int state, long dataLength, Date dateUploaded, long transferred,
|
||||
|
|
@ -222,4 +223,17 @@ public class Contribution extends Media {
|
|||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@ public class ContributionController {
|
|||
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();
|
||||
Timber.d("handleImagePicked() called with onActivityResult()");
|
||||
Intent shareIntent = new Intent(activity, ShareActivity.class);
|
||||
|
|
@ -102,9 +102,6 @@ public class ContributionController {
|
|||
shareIntent.setType(activity.getContentResolver().getType(imageData));
|
||||
shareIntent.putExtra(EXTRA_STREAM, imageData);
|
||||
shareIntent.putExtra(EXTRA_SOURCE, SOURCE_GALLERY);
|
||||
if (isDirectUpload) {
|
||||
shareIntent.putExtra("isDirectUpload", true);
|
||||
}
|
||||
break;
|
||||
case SELECT_FROM_CAMERA:
|
||||
//FIXME: Find out appropriate mime type
|
||||
|
|
@ -113,9 +110,6 @@ public class ContributionController {
|
|||
shareIntent.setType("image/jpeg");
|
||||
shareIntent.putExtra(EXTRA_STREAM, lastGeneratedCaptureUri);
|
||||
shareIntent.putExtra(EXTRA_SOURCE, SOURCE_CAMERA);
|
||||
if (isDirectUpload) {
|
||||
shareIntent.putExtra("isDirectUpload", true);
|
||||
}
|
||||
|
||||
break;
|
||||
default:
|
||||
|
|
@ -123,6 +117,10 @@ public class ContributionController {
|
|||
}
|
||||
Timber.i("Image selected");
|
||||
try {
|
||||
shareIntent.putExtra("isDirectUpload", isDirectUpload);
|
||||
if (wikiDataEntityId != null && !wikiDataEntityId.equals("")) {
|
||||
shareIntent.putExtra("wikiDataEntityId", wikiDataEntityId);
|
||||
}
|
||||
activity.startActivity(shareIntent);
|
||||
} catch (SecurityException e) {
|
||||
Timber.e(e, "Security Exception");
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import android.net.Uri;
|
|||
import android.os.RemoteException;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
|
|
|
|||
|
|
@ -117,7 +117,7 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment {
|
|||
if (resultCode == RESULT_OK) {
|
||||
Timber.d("OnActivityResult() parameters: Req code: %d Result code: %d Data: %s",
|
||||
requestCode, resultCode, data);
|
||||
controller.handleImagePicked(requestCode, data, false);
|
||||
controller.handleImagePicked(requestCode, data, false, null);
|
||||
} else {
|
||||
Timber.e("OnActivityResult() parameters: Req code: %d Result code: %d Data: %s",
|
||||
requestCode, resultCode, data);
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ public class DeleteTask extends AsyncTask<Void, Integer, Boolean> {
|
|||
|
||||
String logPageString = "\n{{Commons:Deletion requests/" + media.getFilename() +
|
||||
"}}\n";
|
||||
SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd");
|
||||
SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd", Locale.getDefault());
|
||||
String date = sdf.format(calendar.getTime());
|
||||
|
||||
String userPageString = "\n{{subst:idw|" + media.getFilename() +
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import fr.free.nrw.commons.WelcomeActivity;
|
|||
import fr.free.nrw.commons.auth.LoginActivity;
|
||||
import fr.free.nrw.commons.auth.SignupActivity;
|
||||
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.notification.NotificationActivity;
|
||||
import fr.free.nrw.commons.settings.SettingsActivity;
|
||||
|
|
@ -46,4 +47,7 @@ public abstract class ActivityBuilderModule {
|
|||
|
||||
@ContributesAndroidInjector
|
||||
abstract NotificationActivity bindNotificationActivity();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract CategoryImagesActivity bindFeaturedImagesActivity();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,17 +9,18 @@ import dagger.android.support.AndroidSupportInjectionModule;
|
|||
import fr.free.nrw.commons.CommonsApplication;
|
||||
import fr.free.nrw.commons.MediaWikiImageView;
|
||||
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.delete.DeleteTask;
|
||||
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.upload.FileProcessor;
|
||||
import fr.free.nrw.commons.settings.SettingsFragment;
|
||||
|
||||
|
||||
@Singleton
|
||||
@Component(modules = {
|
||||
CommonsApplicationModule.class,
|
||||
NetworkingModule.class,
|
||||
AndroidInjectionModule.class,
|
||||
AndroidSupportInjectionModule.class,
|
||||
ActivityBuilderModule.class,
|
||||
|
|
@ -47,6 +48,8 @@ public interface CommonsApplicationComponent extends AndroidInjector<Application
|
|||
|
||||
void inject(PlaceRenderer placeRenderer);
|
||||
|
||||
void inject(FileProcessor fileProcessor);
|
||||
|
||||
@Component.Builder
|
||||
@SuppressWarnings({"WeakerAccess", "unused"})
|
||||
interface Builder {
|
||||
|
|
|
|||
|
|
@ -11,17 +11,16 @@ import javax.inject.Singleton;
|
|||
|
||||
import dagger.Module;
|
||||
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.SessionManager;
|
||||
import fr.free.nrw.commons.caching.CacheController;
|
||||
import fr.free.nrw.commons.data.DBOpenHelper;
|
||||
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.nearby.NearbyPlaces;
|
||||
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 fr.free.nrw.commons.contributions.ContributionsContentProvider.CONTRIBUTION_AUTHORITY;
|
||||
|
|
@ -31,7 +30,6 @@ import static fr.free.nrw.commons.modifications.ModificationsContentProvider.MOD
|
|||
@SuppressWarnings({"WeakerAccess", "unused"})
|
||||
public class CommonsApplicationModule {
|
||||
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;
|
||||
|
||||
|
|
@ -85,6 +83,17 @@ public class CommonsApplicationModule {
|
|||
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
|
||||
@Named("direct_nearby_upload_prefs")
|
||||
public SharedPreferences providesDirectNearbyUploadPreferences(Context context) {
|
||||
|
|
@ -104,24 +113,12 @@ public class CommonsApplicationModule {
|
|||
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
|
||||
@Singleton
|
||||
public LocationServiceManager provideLocationServiceManager(Context context) {
|
||||
return new LocationServiceManager(context);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
public CacheController provideCacheController() {
|
||||
return new CacheController();
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
public DBOpenHelper provideDBOpenHelper(Context context) {
|
||||
|
|
@ -139,4 +136,10 @@ public class CommonsApplicationModule {
|
|||
public LruCache<String, String> provideLruCache() {
|
||||
return new LruCache<>(1024);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
public WikidataEditListener provideWikidataEditListener() {
|
||||
return new WikidataEditListenerImpl();
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ import dagger.Module;
|
|||
import dagger.android.ContributesAndroidInjector;
|
||||
import fr.free.nrw.commons.category.CategorizationFragment;
|
||||
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.MediaDetailPagerFragment;
|
||||
import fr.free.nrw.commons.nearby.NearbyListFragment;
|
||||
|
|
@ -47,4 +48,7 @@ public abstract class FragmentBuilderModule {
|
|||
@ContributesAndroidInjector
|
||||
abstract SingleUploadFragment bindSingleUploadFragment();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract CategoryImagesListFragment bindFeaturedImagesListFragment();
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
36
app/src/main/java/fr/free/nrw/commons/glide/SvgDecoder.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
package fr.free.nrw.commons.location;
|
||||
|
||||
import android.Manifest;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
|
|
@ -10,9 +11,10 @@ import android.location.LocationManager;
|
|||
import android.os.Bundle;
|
||||
import android.support.v4.app.ActivityCompat;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
|
@ -29,6 +31,7 @@ public class LocationServiceManager implements LocationListener {
|
|||
private Location lastLocation;
|
||||
private final List<LocationUpdateListener> locationListeners = new CopyOnWriteArrayList<>();
|
||||
private boolean isLocationManagerRegistered = false;
|
||||
private Set<Activity> locationExplanationDisplayed = new HashSet<>();
|
||||
|
||||
/**
|
||||
* Constructs a new instance of LocationServiceManager.
|
||||
|
|
@ -51,7 +54,6 @@ public class LocationServiceManager implements LocationListener {
|
|||
|
||||
/**
|
||||
* Returns whether the location permission is granted.
|
||||
*
|
||||
* @return true if the location permission is granted
|
||||
*/
|
||||
public boolean isLocationPermissionGranted() {
|
||||
|
|
@ -73,10 +75,41 @@ public class LocationServiceManager implements LocationListener {
|
|||
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) {
|
||||
return !activity.isFinishing() &&
|
||||
ActivityCompat.shouldShowRequestPermissionRationale(activity,
|
||||
Manifest.permission.ACCESS_FINE_LOCATION);
|
||||
if (activity.isFinishing()) {
|
||||
return false;
|
||||
}
|
||||
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() {
|
||||
|
|
@ -90,10 +123,11 @@ public class LocationServiceManager implements LocationListener {
|
|||
* Registers a LocationManager to listen for current location.
|
||||
*/
|
||||
public void registerLocationManager() {
|
||||
if (!isLocationManagerRegistered)
|
||||
if (!isLocationManagerRegistered) {
|
||||
isLocationManagerRegistered = requestLocationUpdatesFromProvider(LocationManager.NETWORK_PROVIDER)
|
||||
&& requestLocationUpdatesFromProvider(LocationManager.GPS_PROVIDER);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests location updates from the specified provider.
|
||||
|
|
@ -125,7 +159,7 @@ public class LocationServiceManager implements LocationListener {
|
|||
* @return LOCATION_SIGNIFICANTLY_CHANGED if location changed significantly
|
||||
* LOCATION_SLIGHTLY_CHANGED if location changed slightly
|
||||
*/
|
||||
protected LocationChangeType isBetterLocation(Location location, Location currentBestLocation) {
|
||||
private LocationChangeType isBetterLocation(Location location, Location currentBestLocation) {
|
||||
|
||||
if (currentBestLocation == null) {
|
||||
// A new location is always better than no location
|
||||
|
|
@ -249,6 +283,8 @@ public class LocationServiceManager implements LocationListener {
|
|||
public enum LocationChangeType{
|
||||
LOCATION_SIGNIFICANTLY_CHANGED, //Went out of borders of nearby markers
|
||||
LOCATION_SLIGHTLY_CHANGED, //User might be walking or driving
|
||||
LOCATION_NOT_CHANGED
|
||||
LOCATION_NOT_CHANGED,
|
||||
PERMISSION_JUST_GRANTED,
|
||||
MAP_UPDATED
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import android.os.AsyncTask;
|
|||
import android.os.Bundle;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.text.Editable;
|
||||
import android.text.TextUtils;
|
||||
import android.text.TextWatcher;
|
||||
import android.util.TypedValue;
|
||||
import android.view.LayoutInflater;
|
||||
|
|
@ -22,6 +23,9 @@ import android.widget.ScrollView;
|
|||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import butterknife.BindView;
|
||||
import butterknife.ButterKnife;
|
||||
import butterknife.OnClick;
|
||||
import java.io.IOException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
|
|
@ -45,19 +49,23 @@ import fr.free.nrw.commons.mwapi.MediaWikiApi;
|
|||
import fr.free.nrw.commons.ui.widget.CompatTextView;
|
||||
import timber.log.Timber;
|
||||
|
||||
import static android.view.View.GONE;
|
||||
import static android.view.View.VISIBLE;
|
||||
import static android.widget.Toast.LENGTH_SHORT;
|
||||
|
||||
public class MediaDetailFragment extends CommonsDaggerSupportFragment {
|
||||
|
||||
private boolean editable;
|
||||
private boolean isCategoryImage;
|
||||
private MediaDetailPagerFragment.MediaDetailProvider detailProvider;
|
||||
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();
|
||||
|
||||
Bundle state = new Bundle();
|
||||
state.putBoolean("editable", editable);
|
||||
state.putBoolean("isCategoryImage", isCategoryImage);
|
||||
state.putInt("index", index);
|
||||
state.putInt("listIndex", 0);
|
||||
state.putInt("listTop", 0);
|
||||
|
|
@ -72,21 +80,37 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
|
|||
@Inject
|
||||
MediaWikiApi mwApi;
|
||||
|
||||
|
||||
private MediaWikiImageView image;
|
||||
private MediaDetailSpacer spacer;
|
||||
private int initialListTop = 0;
|
||||
|
||||
private TextView title;
|
||||
private TextView desc;
|
||||
private TextView license;
|
||||
private TextView coordinates;
|
||||
private TextView uploadedDate;
|
||||
private TextView seeMore;
|
||||
private LinearLayout nominatedforDeletion;
|
||||
private LinearLayout categoryContainer;
|
||||
private Button delete;
|
||||
private ScrollView scrollView;
|
||||
@BindView(R.id.mediaDetailImage)
|
||||
MediaWikiImageView image;
|
||||
@BindView(R.id.mediaDetailSpacer)
|
||||
MediaDetailSpacer spacer;
|
||||
@BindView(R.id.mediaDetailTitle)
|
||||
TextView title;
|
||||
@BindView(R.id.mediaDetailDesc)
|
||||
TextView desc;
|
||||
@BindView(R.id.mediaDetailAuthor)
|
||||
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 boolean categoriesLoaded = false;
|
||||
private boolean categoriesPresent = false;
|
||||
|
|
@ -96,11 +120,15 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
|
|||
private AsyncTask<Void, Void, Boolean> detailFetchTask;
|
||||
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
|
||||
public void onSaveInstanceState(Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
outState.putInt("index", index);
|
||||
outState.putBoolean("editable", editable);
|
||||
outState.putBoolean("isCategoryImage", isCategoryImage);
|
||||
|
||||
getScrollPosition();
|
||||
outState.putInt("listTop", initialListTop);
|
||||
|
|
@ -116,32 +144,28 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
|
|||
|
||||
if (savedInstanceState != null) {
|
||||
editable = savedInstanceState.getBoolean("editable");
|
||||
isCategoryImage = savedInstanceState.getBoolean("isCategoryImage");
|
||||
index = savedInstanceState.getInt("index");
|
||||
initialListTop = savedInstanceState.getInt("listTop");
|
||||
} else {
|
||||
editable = getArguments().getBoolean("editable");
|
||||
isCategoryImage = getArguments().getBoolean("isCategoryImage");
|
||||
index = getArguments().getInt("index");
|
||||
initialListTop = 0;
|
||||
}
|
||||
|
||||
categoryNames = new ArrayList<>();
|
||||
categoryNames.add(getString(R.string.detail_panel_cats_loading));
|
||||
|
||||
final View view = inflater.inflate(R.layout.fragment_media_detail, container, false);
|
||||
|
||||
image = (MediaWikiImageView) view.findViewById(R.id.mediaDetailImage);
|
||||
scrollView = (ScrollView) view.findViewById(R.id.mediaDetailScrollView);
|
||||
ButterKnife.bind(this,view);
|
||||
|
||||
// Detail consists of a list view with main pane in header view, plus category list.
|
||||
spacer = (MediaDetailSpacer) view.findViewById(R.id.mediaDetailSpacer);
|
||||
title = (TextView) view.findViewById(R.id.mediaDetailTitle);
|
||||
desc = (TextView) view.findViewById(R.id.mediaDetailDesc);
|
||||
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);
|
||||
if (isCategoryImage){
|
||||
authorLayout.setVisibility(VISIBLE);
|
||||
} else {
|
||||
authorLayout.setVisibility(GONE);
|
||||
}
|
||||
|
||||
licenseList = new LicenseList(getActivity());
|
||||
|
||||
|
|
@ -179,7 +203,7 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
|
|||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
Media media = detailProvider.getMediaAtPosition(index);
|
||||
media = detailProvider.getMediaAtPosition(index);
|
||||
if (media == null) {
|
||||
// Ask the detail provider to ping us when we're ready
|
||||
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!");
|
||||
detailProvider.unregisterDataSetObserver(dataObserver);
|
||||
dataObserver = null;
|
||||
displayMediaDetails(detailProvider.getMediaAtPosition(index));
|
||||
media=detailProvider.getMediaAtPosition(index);
|
||||
displayMediaDetails();
|
||||
}
|
||||
};
|
||||
detailProvider.registerDataSetObserver(dataObserver);
|
||||
} else {
|
||||
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
|
||||
image.setMedia(media);
|
||||
|
||||
|
|
@ -239,7 +264,6 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
|
|||
if (success) {
|
||||
extractor.fill(media);
|
||||
setTextFields(media);
|
||||
setOnClickListeners(media);
|
||||
} else {
|
||||
Timber.d("Failed to load photo details.");
|
||||
}
|
||||
|
|
@ -290,23 +314,41 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
|
|||
}
|
||||
rebuildCatList();
|
||||
|
||||
if(media.getCreator() == null || media.getCreator().equals("")) {
|
||||
authorLayout.setVisibility(GONE);
|
||||
} else {
|
||||
author.setText(media.getCreator());
|
||||
}
|
||||
|
||||
checkDeletion(media);
|
||||
}
|
||||
|
||||
private void setOnClickListeners(final Media media) {
|
||||
if (licenseLink(media) != null) {
|
||||
license.setOnClickListener(v -> openWebBrowser(licenseLink(media)));
|
||||
@OnClick(R.id.mediaDetailLicense)
|
||||
public void onMediaDetailLicenceClicked(){
|
||||
if (!TextUtils.isEmpty(licenseLink(media))) {
|
||||
openWebBrowser(licenseLink(media));
|
||||
} else {
|
||||
if(isCategoryImage) {
|
||||
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();
|
||||
}
|
||||
if (media.getCoordinates() != null) {
|
||||
coordinates.setOnClickListener(v -> openMap(media.getCoordinates()));
|
||||
}
|
||||
if (delete.getVisibility() == View.VISIBLE) {
|
||||
delete.setOnClickListener(v -> {
|
||||
delete.setEnabled(false);
|
||||
delete.setTextColor(getResources().getColor(R.color.deleteButtonLight));
|
||||
}
|
||||
|
||||
@OnClick(R.id.mediaDetailCoordinates)
|
||||
public void onMediaDetailCoordinatesClicked(){
|
||||
if (media.getCoordinates() != null) {
|
||||
openMap(media.getCoordinates());
|
||||
}
|
||||
}
|
||||
|
||||
@OnClick(R.id.nominateDeletion)
|
||||
public void onDeleteButtonClicked(){
|
||||
//Reviewer correct me if i have misunderstood something over here
|
||||
//But how does this if (delete.getVisibility() == View.VISIBLE) {
|
||||
// enableDeleteButton(true); makes sense ?
|
||||
AlertDialog.Builder alert = new AlertDialog.Builder(getActivity());
|
||||
alert.setMessage("Why should this file be deleted?");
|
||||
final EditText input = new EditText(getActivity());
|
||||
|
|
@ -317,6 +359,7 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
|
|||
String reason = input.getText().toString();
|
||||
DeleteTask deleteTask = new DeleteTask(getActivity(), media, reason);
|
||||
deleteTask.execute();
|
||||
enableDeleteButton(false);
|
||||
}
|
||||
});
|
||||
alert.setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
|
||||
|
|
@ -349,12 +392,21 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
|
|||
});
|
||||
d.show();
|
||||
d.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false);
|
||||
});
|
||||
}
|
||||
if (nominatedforDeletion.getVisibility() == View.VISIBLE){
|
||||
seeMore.setOnClickListener(v -> {
|
||||
|
||||
@OnClick(R.id.seeMore)
|
||||
public void onSeeMoreClicked(){
|
||||
if(nominatedForDeletion.getVisibility()== VISIBLE) {
|
||||
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()) {
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
@ -449,12 +501,11 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
|
|||
|
||||
private void checkDeletion(Media media){
|
||||
if (media.getRequestedDeletion()){
|
||||
delete.setVisibility(View.GONE);
|
||||
nominatedforDeletion.setVisibility(View.VISIBLE);
|
||||
}
|
||||
else{
|
||||
delete.setVisibility(View.VISIBLE);
|
||||
nominatedforDeletion.setVisibility(View.GONE);
|
||||
delete.setVisibility(GONE);
|
||||
nominatedForDeletion.setVisibility(VISIBLE);
|
||||
} else if (!isCategoryImage) {
|
||||
delete.setVisibility(VISIBLE);
|
||||
nominatedForDeletion.setVisibility(GONE);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -26,6 +26,8 @@ import android.view.View;
|
|||
import android.view.ViewGroup;
|
||||
import android.widget.Toast;
|
||||
|
||||
import butterknife.BindView;
|
||||
import butterknife.ButterKnife;
|
||||
import javax.inject.Inject;
|
||||
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.di.CommonsDaggerSupportFragment;
|
||||
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.content.Context.DOWNLOAD_SERVICE;
|
||||
|
|
@ -53,16 +57,19 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple
|
|||
@Named("default_preferences")
|
||||
SharedPreferences prefs;
|
||||
|
||||
private ViewPager pager;
|
||||
@BindView(R.id.mediaDetailsPager)
|
||||
ViewPager pager;
|
||||
private Boolean editable;
|
||||
private boolean isFeaturedImage;
|
||||
|
||||
public MediaDetailPagerFragment() {
|
||||
this(false);
|
||||
this(false, false);
|
||||
}
|
||||
|
||||
@SuppressLint("ValidFragment")
|
||||
public MediaDetailPagerFragment(Boolean editable) {
|
||||
public MediaDetailPagerFragment(Boolean editable, boolean isFeaturedImage) {
|
||||
this.editable = editable;
|
||||
this.isFeaturedImage = isFeaturedImage;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -70,7 +77,7 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple
|
|||
ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
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);
|
||||
|
||||
final MediaDetailAdapter adapter = new MediaDetailAdapter(getChildFragmentManager());
|
||||
|
|
@ -96,6 +103,7 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple
|
|||
super.onSaveInstanceState(outState);
|
||||
outState.putInt("current-page", pager.getCurrentItem());
|
||||
outState.putBoolean("editable", editable);
|
||||
outState.putBoolean("isFeaturedImage", isFeaturedImage);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -103,6 +111,7 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple
|
|||
super.onCreate(savedInstanceState);
|
||||
if (savedInstanceState != null) {
|
||||
editable = savedInstanceState.getBoolean("editable");
|
||||
isFeaturedImage = savedInstanceState.getBoolean("isFeaturedImage");
|
||||
}
|
||||
setHasOptionsMenu(true);
|
||||
}
|
||||
|
|
@ -133,6 +142,10 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple
|
|||
// Download
|
||||
downloadMedia(m);
|
||||
return true;
|
||||
case R.id.menu_set_as_wallpaper:
|
||||
// Set wallpaper
|
||||
setWallpaper(m);
|
||||
return true;
|
||||
case R.id.menu_retry_current_image:
|
||||
// Retry
|
||||
((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.
|
||||
* 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
|
||||
pager.postDelayed(() -> getActivity().supportInvalidateOptionsMenu(), 5);
|
||||
}
|
||||
return MediaDetailFragment.forMedia(i, editable);
|
||||
return MediaDetailFragment.forMedia(i, editable, isFeaturedImage);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ import android.support.annotation.VisibleForTesting;
|
|||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
|
||||
import org.apache.http.HttpResponse;
|
||||
import org.apache.http.conn.ClientConnectionManager;
|
||||
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.mediawiki.api.ApiResult;
|
||||
import org.mediawiki.api.MWApi;
|
||||
import org.w3c.dom.Element;
|
||||
import org.w3c.dom.Node;
|
||||
import org.w3c.dom.NodeList;
|
||||
|
||||
import java.io.IOException;
|
||||
|
|
@ -38,7 +42,10 @@ import java.util.Locale;
|
|||
import java.util.concurrent.Callable;
|
||||
|
||||
import fr.free.nrw.commons.BuildConfig;
|
||||
import fr.free.nrw.commons.Media;
|
||||
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.NotificationUtils;
|
||||
import in.yuvi.http.fluent.Http;
|
||||
|
|
@ -46,6 +53,8 @@ import io.reactivex.Observable;
|
|||
import io.reactivex.Single;
|
||||
import timber.log.Timber;
|
||||
|
||||
import static fr.free.nrw.commons.utils.ContinueUtils.getQueryContinue;
|
||||
|
||||
/**
|
||||
* @author Addshore
|
||||
*/
|
||||
|
|
@ -55,10 +64,18 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
|
|||
private static final String THUMB_SIZE = "640";
|
||||
private AbstractHttpClient httpClient;
|
||||
private MWApi api;
|
||||
private MWApi wikidataApi;
|
||||
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;
|
||||
BasicHttpParams params = new BasicHttpParams();
|
||||
SchemeRegistry schemeRegistry = new SchemeRegistry();
|
||||
|
|
@ -69,7 +86,10 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
|
|||
params.setParameter(CoreProtocolPNames.USER_AGENT, getUserAgent());
|
||||
httpClient = new DefaultHttpClient(cm, params);
|
||||
api = new MWApi(apiURL, httpClient);
|
||||
this.sharedPreferences = sharedPreferences;
|
||||
wikidataApi = new MWApi(wikidatApiURL, httpClient);
|
||||
this.defaultPreferences = defaultPreferences;
|
||||
this.categoryPreferences = categoryPreferences;
|
||||
this.gson = gson;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -160,7 +180,7 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
|
|||
}
|
||||
|
||||
private void setAuthCookieOnLogin(boolean isLoggedIn) {
|
||||
SharedPreferences.Editor editor = sharedPreferences.edit();
|
||||
SharedPreferences.Editor editor = defaultPreferences.edit();
|
||||
if (isLoggedIn) {
|
||||
editor.putBoolean("isUserLoggedIn", true);
|
||||
editor.putString("getAuthCookie", api.getAuthCookie());
|
||||
|
|
@ -191,6 +211,15 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
|
|||
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
|
||||
public boolean fileExistsWithName(String fileName) throws IOException {
|
||||
return api.action("query")
|
||||
|
|
@ -336,6 +365,98 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
|
|||
}).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
|
||||
@NonNull
|
||||
public Observable<String> searchTitles(String title, int searchCatsLimit) {
|
||||
|
|
@ -429,8 +550,8 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
|
|||
.param("notprop", "list")
|
||||
.param("format", "xml")
|
||||
.param("meta", "notifications")
|
||||
// .param("meta", "notifications")
|
||||
.param("notformat", "model")
|
||||
.param("notwikis", "wikidatawiki|commonswiki|enwiki")
|
||||
.get()
|
||||
.getNode("/api/query/notifications/list");
|
||||
} catch (IOException e) {
|
||||
|
|
@ -448,6 +569,83 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
|
|||
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
|
||||
public boolean existingFile(String fileSha1) throws IOException {
|
||||
return api.action("query")
|
||||
|
|
@ -496,6 +694,7 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
|
|||
String resultStatus = result.getString("/api/upload/@result");
|
||||
if (!resultStatus.equals("Success")) {
|
||||
String errorCode = result.getString("/api/error/@code");
|
||||
Timber.e(errorCode);
|
||||
return new UploadResult(resultStatus, errorCode);
|
||||
} else {
|
||||
Date dateUploaded = parseMWDate(result.getString("/api/upload/imageinfo/@timestamp"));
|
||||
|
|
|
|||
101
app/src/main/java/fr/free/nrw/commons/mwapi/CategoryApi.java
Normal 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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
|
@ -7,6 +7,7 @@ import java.io.IOException;
|
|||
import java.io.InputStream;
|
||||
import java.util.List;
|
||||
|
||||
import fr.free.nrw.commons.Media;
|
||||
import fr.free.nrw.commons.notification.Notification;
|
||||
import io.reactivex.Observable;
|
||||
import io.reactivex.Single;
|
||||
|
|
@ -26,6 +27,10 @@ public interface MediaWikiApi {
|
|||
|
||||
String getEditToken() throws IOException;
|
||||
|
||||
String getWikidataCsrfToken() throws IOException;
|
||||
|
||||
String getCentralAuthToken() throws IOException;
|
||||
|
||||
boolean fileExistsWithName(String fileName) throws IOException;
|
||||
|
||||
boolean pageExists(String pageName) throws IOException;
|
||||
|
|
@ -34,6 +39,8 @@ public interface MediaWikiApi {
|
|||
|
||||
boolean logEvents(LogBuilder[] logBuilders);
|
||||
|
||||
List<Media> getCategoryImages(String categoryName);
|
||||
|
||||
@NonNull
|
||||
UploadResult uploadFile(String filename, InputStream file, long dataLength, String pageContents, String editSummary, ProgressListener progressListener) throws IOException;
|
||||
|
||||
|
|
@ -46,6 +53,12 @@ public interface MediaWikiApi {
|
|||
@Nullable
|
||||
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
|
||||
MediaResult fetchMediaByFilename(String filename) throws IOException;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
17
app/src/main/java/fr/free/nrw/commons/mwapi/model/Page.java
Normal 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];
|
||||
}
|
||||
}
|
||||
|
|
@ -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:", "") : "";
|
||||
}
|
||||
}
|
||||
10
app/src/main/java/fr/free/nrw/commons/mwapi/model/Query.java
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
package fr.free.nrw.commons.mwapi.model;
|
||||
|
||||
public class Query {
|
||||
public Page[] pages;
|
||||
|
||||
public Query() {
|
||||
pages = new Page[0];
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
package fr.free.nrw.commons.nearby;
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Build;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
|
|
|
|||
|
|
@ -4,23 +4,24 @@ import android.content.BroadcastReceiver;
|
|||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.graphics.Typeface;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.design.widget.BottomSheetBehavior;
|
||||
|
||||
import android.support.v4.app.FragmentTransaction;
|
||||
import android.support.v7.app.AlertDialog;
|
||||
import android.util.Log;
|
||||
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
|
|
@ -28,28 +29,34 @@ import com.google.gson.GsonBuilder;
|
|||
import java.util.List;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
|
||||
import butterknife.BindView;
|
||||
import butterknife.ButterKnife;
|
||||
|
||||
import fr.free.nrw.commons.R;
|
||||
import fr.free.nrw.commons.location.LatLng;
|
||||
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.theme.NavigationBaseActivity;
|
||||
import fr.free.nrw.commons.utils.NetworkUtils;
|
||||
import fr.free.nrw.commons.utils.UriSerializer;
|
||||
|
||||
import fr.free.nrw.commons.utils.ViewUtil;
|
||||
import fr.free.nrw.commons.wikidata.WikidataEditListener;
|
||||
import io.reactivex.Observable;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.disposables.Disposable;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
|
||||
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;
|
||||
|
||||
|
|
@ -62,13 +69,18 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
|
|||
LinearLayout bottomSheetDetails;
|
||||
@BindView(R.id.transparentView)
|
||||
View transparentView;
|
||||
@BindView(R.id.fab_recenter)
|
||||
View fabRecenter;
|
||||
|
||||
@Inject
|
||||
LocationServiceManager locationManager;
|
||||
@Inject
|
||||
NearbyController nearbyController;
|
||||
@Inject WikidataEditListener wikidataEditListener;
|
||||
|
||||
private LatLng curLatLang;
|
||||
@Inject
|
||||
@Named("application_preferences") SharedPreferences applicationPrefs;
|
||||
private LatLng curLatLng;
|
||||
private Bundle bundle;
|
||||
private Disposable placesDisposable;
|
||||
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 static final String TAG_RETAINED_MAP_FRAGMENT = NearbyMapFragment.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 BroadcastReceiver broadcastReceiver;
|
||||
|
||||
private boolean isListShowcaseAdded = false;
|
||||
private boolean isMapShowCaseAdded = false;
|
||||
|
||||
private LatLng lastKnownLocation;
|
||||
|
||||
private MaterialShowcaseView secondSingleShowCaseView;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
|
@ -92,6 +112,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
|
|||
|
||||
initBottomSheetBehaviour();
|
||||
initDrawer();
|
||||
wikidataEditListener.setAuthenticationStateListener(this);
|
||||
}
|
||||
|
||||
private void resumeFragment() {
|
||||
|
|
@ -131,16 +152,55 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
|
|||
MenuInflater inflater = getMenuInflater();
|
||||
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);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
|
||||
// Handle item selection
|
||||
switch (item.getItemId()) {
|
||||
case R.id.action_display_list:
|
||||
if(bottomSheetBehavior.getState()==BottomSheetBehavior.STATE_COLLAPSED || bottomSheetBehavior.getState()==BottomSheetBehavior.STATE_HIDDEN){
|
||||
bottomSheetBehaviorForDetails.setState(BottomSheetBehavior.STATE_HIDDEN);
|
||||
bottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
|
||||
}else if(bottomSheetBehavior.getState()==BottomSheetBehavior.STATE_EXPANDED){
|
||||
bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
|
||||
}
|
||||
|
||||
return true;
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
|
|
@ -158,7 +218,11 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
|
|||
switch (requestCode) {
|
||||
case LOCATION_REQUEST: {
|
||||
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 {
|
||||
//If permission not granted, go to page that says Nearby Places cannot be displayed
|
||||
hideProgressBar();
|
||||
|
|
@ -218,7 +282,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
|
|||
private void checkLocationPermission() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
if (locationManager.isLocationPermissionGranted()) {
|
||||
refreshView(LocationServiceManager.LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED);
|
||||
refreshView(LOCATION_SIGNIFICANTLY_CHANGED);
|
||||
} else {
|
||||
// Should we show an explanation?
|
||||
if (locationManager.isPermissionExplanationRequired(this)) {
|
||||
|
|
@ -244,7 +308,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
|
|||
}
|
||||
}
|
||||
} 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);
|
||||
if (requestCode == 1) {
|
||||
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() {
|
||||
super.onStart();
|
||||
locationManager.addLocationListener(this);
|
||||
locationManager.registerLocationManager();
|
||||
registerLocationUpdates();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -312,8 +376,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
|
|||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
if (NetworkUtils.isInternetConnectionEstablished(NearbyActivity.this)) {
|
||||
refreshView(LocationServiceManager
|
||||
.LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED);
|
||||
refreshView(LOCATION_SIGNIFICANTLY_CHANGED);
|
||||
} else {
|
||||
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
|
||||
*/
|
||||
private void refreshView(LocationServiceManager.LocationChangeType locationChangeType) {
|
||||
private void refreshView(LocationChangeType locationChangeType) {
|
||||
if (lockNearbyView) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -339,38 +402,91 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
|
|||
return;
|
||||
}
|
||||
|
||||
locationManager.registerLocationManager();
|
||||
registerLocationUpdates();
|
||||
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;
|
||||
}
|
||||
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");
|
||||
return;
|
||||
}
|
||||
|
||||
if (locationChangeType
|
||||
.equals(LocationServiceManager.LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED)) {
|
||||
if (locationChangeType.equals(LOCATION_SIGNIFICANTLY_CHANGED)
|
||||
|| locationChangeType.equals(PERMISSION_JUST_GRANTED)
|
||||
|| locationChangeType.equals(MAP_UPDATED)) {
|
||||
progressBar.setVisibility(View.VISIBLE);
|
||||
placesDisposable = Observable.fromCallable(() -> nearbyController
|
||||
.loadAttractionsFromLocation(curLatLang))
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(this::populatePlaces);
|
||||
} else if (locationChangeType
|
||||
.equals(LocationServiceManager.LocationChangeType.LOCATION_SLIGHTLY_CHANGED)) {
|
||||
|
||||
//TODO: This hack inserts curLatLng before populatePlaces is called (see #1440). Ideally a proper fix should be found
|
||||
Gson gson = new GsonBuilder()
|
||||
.registerTypeAdapter(Uri.class, new UriSerializer())
|
||||
.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);
|
||||
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) {
|
||||
List<Place> placeList = nearbyPlacesInfo.placeList;
|
||||
LatLng[] boundaryCoordinates = nearbyPlacesInfo.boundaryCoordinates;
|
||||
|
|
@ -378,20 +494,20 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
|
|||
.registerTypeAdapter(Uri.class, new UriSerializer())
|
||||
.create();
|
||||
String gsonPlaceList = gson.toJson(placeList);
|
||||
String gsonCurLatLng = gson.toJson(curLatLang);
|
||||
String gsonCurLatLng = gson.toJson(curLatLng);
|
||||
String gsonBoundaryCoordinates = gson.toJson(boundaryCoordinates);
|
||||
|
||||
if (placeList.size() == 0) {
|
||||
ViewUtil.showSnackbar(findViewById(R.id.container), R.string.no_nearby);
|
||||
}
|
||||
|
||||
bundle.clear();
|
||||
bundle.putString("PlaceList", gsonPlaceList);
|
||||
bundle.putString("CurLatLng", gsonCurLatLng);
|
||||
//bundle.putString("CurLatLng", gsonCurLatLng);
|
||||
bundle.putString("BoundaryCoord", gsonBoundaryCoordinates);
|
||||
|
||||
// First time to init fragments
|
||||
if (nearbyMapFragment == null) {
|
||||
Timber.d("Init map fragment for the first time");
|
||||
lockNearbyView(true);
|
||||
setMapFragment();
|
||||
setListFragment();
|
||||
|
|
@ -399,9 +515,49 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
|
|||
lockNearbyView(false);
|
||||
} else {
|
||||
// There are fragments, just update the map and list
|
||||
Timber.d("Map fragment already exists, just update the map and list");
|
||||
updateMapFragment(false);
|
||||
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) {
|
||||
|
|
@ -411,7 +567,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
|
|||
locationManager.removeLocationListener(this);
|
||||
} else {
|
||||
lockNearbyView = false;
|
||||
locationManager.registerLocationManager();
|
||||
registerLocationUpdates();
|
||||
locationManager.addLocationListener(this);
|
||||
}
|
||||
}
|
||||
|
|
@ -457,34 +613,39 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
|
|||
|
||||
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)
|
||||
/*
|
||||
* If we are close to nearby places boundaries, we need a significant update to
|
||||
* get new nearby places. Check order is south, north, west, east
|
||||
* */
|
||||
if (nearbyMapFragment.boundaryCoordinates != null
|
||||
&& (curLatLang.getLatitude() <= nearbyMapFragment.boundaryCoordinates[0].getLatitude()
|
||||
|| curLatLang.getLatitude() >= nearbyMapFragment.boundaryCoordinates[1].getLatitude()
|
||||
|| curLatLang.getLongitude() <= nearbyMapFragment.boundaryCoordinates[2].getLongitude()
|
||||
|| curLatLang.getLongitude() >= nearbyMapFragment.boundaryCoordinates[3].getLongitude())) {
|
||||
&& (curLatLng.getLatitude() <= nearbyMapFragment.boundaryCoordinates[0].getLatitude()
|
||||
|| curLatLng.getLatitude() >= nearbyMapFragment.boundaryCoordinates[1].getLatitude()
|
||||
|| curLatLng.getLongitude() <= nearbyMapFragment.boundaryCoordinates[2].getLongitude()
|
||||
|| curLatLng.getLongitude() >= nearbyMapFragment.boundaryCoordinates[3].getLongitude())) {
|
||||
// populate places
|
||||
placesDisposable = Observable.fromCallable(() -> nearbyController
|
||||
.loadAttractionsFromLocation(curLatLang))
|
||||
.loadAttractionsFromLocation(curLatLng))
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(this::populatePlaces);
|
||||
nearbyMapFragment.setArguments(bundle);
|
||||
.subscribe(this::populatePlaces,
|
||||
throwable -> {
|
||||
Timber.d(throwable);
|
||||
showErrorMessage(getString(R.string.error_fetching_nearby_places));
|
||||
progressBar.setVisibility(View.GONE);
|
||||
});
|
||||
nearbyMapFragment.setBundleForUpdtes(bundle);
|
||||
nearbyMapFragment.updateMapSignificantly();
|
||||
updateListFragment();
|
||||
return;
|
||||
}
|
||||
|
||||
if (isSlightUpdate) {
|
||||
nearbyMapFragment.setArguments(bundle);
|
||||
nearbyMapFragment.setBundleForUpdtes(bundle);
|
||||
nearbyMapFragment.updateMapSlightly();
|
||||
} else {
|
||||
nearbyMapFragment.setArguments(bundle);
|
||||
nearbyMapFragment.setBundleForUpdtes(bundle);
|
||||
nearbyMapFragment.updateMapSignificantly();
|
||||
updateListFragment();
|
||||
}
|
||||
|
|
@ -498,7 +659,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
|
|||
}
|
||||
|
||||
private void updateListFragment() {
|
||||
nearbyListFragment.setArguments(bundle);
|
||||
nearbyListFragment.setBundleForUpdates(bundle);
|
||||
nearbyListFragment.updateNearbyListSignificantly();
|
||||
}
|
||||
|
||||
|
|
@ -528,15 +689,24 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
|
|||
|
||||
@Override
|
||||
public void onLocationChangedSignificantly(LatLng latLng) {
|
||||
refreshView(LocationServiceManager.LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED);
|
||||
refreshView(LOCATION_SIGNIFICANTLY_CHANGED);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLocationChangedSlightly(LatLng latLng) {
|
||||
refreshView(LocationServiceManager.LocationChangeType.LOCATION_SLIGHTLY_CHANGED);
|
||||
refreshView(LOCATION_SLIGHTLY_CHANGED);
|
||||
}
|
||||
|
||||
public void prepareViewsForSheetPosition(int bottomSheetState) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
private void showErrorMessage(String message) {
|
||||
ViewUtil.showLongToast(NearbyActivity.this, message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWikidataEditSuccessful() {
|
||||
refreshView(MAP_UPDATED);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import android.support.graphics.drawable.VectorDrawableCompat;
|
|||
|
||||
import com.mapbox.mapboxsdk.annotations.IconFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
|
|
@ -44,7 +45,7 @@ public class NearbyController {
|
|||
* @return NearbyPlacesInfo a variable holds Place list without distance information
|
||||
* 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);
|
||||
NearbyPlacesInfo nearbyPlacesInfo = new NearbyPlacesInfo();
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package fr.free.nrw.commons.nearby;
|
|||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
|
|
@ -21,6 +22,9 @@ import java.lang.reflect.Type;
|
|||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
|
||||
import dagger.android.support.AndroidSupportInjection;
|
||||
import dagger.android.support.DaggerFragment;
|
||||
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;
|
||||
|
||||
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>>() {
|
||||
}.getType();
|
||||
private static final Type CUR_LAT_LNG_TYPE = new TypeToken<LatLng>() {
|
||||
|
|
@ -45,6 +51,11 @@ public class NearbyListFragment extends DaggerFragment {
|
|||
private RecyclerView recyclerView;
|
||||
private ContributionController controller;
|
||||
|
||||
|
||||
@Inject
|
||||
@Named("direct_nearby_upload_prefs")
|
||||
SharedPreferences directPrefs;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
|
@ -80,9 +91,11 @@ public class NearbyListFragment extends DaggerFragment {
|
|||
}
|
||||
|
||||
public void updateNearbyListSignificantly() {
|
||||
Bundle bundle = this.getArguments();
|
||||
adapterFactory.updateAdapterData(getPlaceListFromBundle(bundle),
|
||||
(RVRendererAdapter<Place>) recyclerView.getAdapter());
|
||||
try {
|
||||
adapterFactory.updateAdapterData(getPlaceListFromBundle(bundleForUpdates), (RVRendererAdapter<Place>) recyclerView.getAdapter());
|
||||
} catch (NullPointerException e) {
|
||||
Timber.e("Null pointer exception from calling recyclerView.getAdapter()");
|
||||
}
|
||||
}
|
||||
|
||||
private List<Place> getPlaceListFromBundle(Bundle bundle) {
|
||||
|
|
@ -133,11 +146,15 @@ public class NearbyListFragment extends DaggerFragment {
|
|||
if (resultCode == RESULT_OK) {
|
||||
Timber.d("OnActivityResult() parameters: Req code: %d Result code: %d Data: %s",
|
||||
requestCode, resultCode, data);
|
||||
controller.handleImagePicked(requestCode, data, true);
|
||||
controller.handleImagePicked(requestCode, data, true, directPrefs.getString("WikiDataEntityId", null));
|
||||
} else {
|
||||
Timber.e("OnActivityResult() parameters: Req code: %d Result code: %d Data: %s",
|
||||
requestCode, resultCode, data);
|
||||
}
|
||||
}
|
||||
|
||||
public void setBundleForUpdates(Bundle bundleForUpdates) {
|
||||
this.bundleForUpdates = bundleForUpdates;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -7,6 +7,7 @@ import android.content.Intent;
|
|||
import android.content.SharedPreferences;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Typeface;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
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.ViewUtil;
|
||||
import timber.log.Timber;
|
||||
import uk.co.deanwild.materialshowcaseview.MaterialShowcaseView;
|
||||
|
||||
import static android.app.Activity.RESULT_OK;
|
||||
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
|
||||
|
||||
public class NearbyMapFragment extends DaggerFragment {
|
||||
|
||||
private MapView mapView;
|
||||
public MapView mapView;
|
||||
private List<NearbyBaseMarker> baseMarkerOptions;
|
||||
private fr.free.nrw.commons.location.LatLng curLatLng;
|
||||
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_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
|
||||
@Named("prefs")
|
||||
SharedPreferences prefs;
|
||||
|
|
@ -124,6 +132,7 @@ public class NearbyMapFragment extends DaggerFragment {
|
|||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
Timber.d("Nearby map fragment created");
|
||||
|
||||
controller = new ContributionController(this);
|
||||
directUpload = new DirectUpload(this, controller);
|
||||
|
|
@ -149,17 +158,20 @@ public class NearbyMapFragment extends DaggerFragment {
|
|||
getActivity());
|
||||
boundaryCoordinates = gson.fromJson(gsonBoundaryCoordinates, gsonBoundaryCoordinatesType);
|
||||
}
|
||||
if (curLatLng != null) {
|
||||
Mapbox.getInstance(getActivity(),
|
||||
getString(R.string.mapbox_commons_app_token));
|
||||
MapboxTelemetry.getInstance().setTelemetryEnabled(false);
|
||||
}
|
||||
setRetainInstance(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
|
||||
Timber.d("onCreateView called");
|
||||
if (curLatLng != null) {
|
||||
Timber.d("curLatLng found, setting up map view...");
|
||||
setupMapView(savedInstanceState);
|
||||
}
|
||||
|
||||
|
|
@ -192,14 +204,12 @@ public class NearbyMapFragment extends DaggerFragment {
|
|||
}
|
||||
|
||||
public void updateMapSlightly() {
|
||||
// Get arguments from bundle for new location
|
||||
Bundle bundle = this.getArguments();
|
||||
if (mapboxMap != null) {
|
||||
Gson gson = new GsonBuilder()
|
||||
.registerTypeAdapter(Uri.class, new UriDeserializer())
|
||||
.create();
|
||||
if (bundle != null) {
|
||||
String gsonLatLng = bundle.getString("CurLatLng");
|
||||
if (bundleForUpdtes != null) {
|
||||
String gsonLatLng = bundleForUpdtes.getString("CurLatLng");
|
||||
Type curLatLngType = new TypeToken<fr.free.nrw.commons.location.LatLng>() {}.getType();
|
||||
curLatLng = gson.fromJson(gsonLatLng, curLatLngType);
|
||||
}
|
||||
|
|
@ -209,17 +219,15 @@ public class NearbyMapFragment extends DaggerFragment {
|
|||
}
|
||||
|
||||
public void updateMapSignificantly() {
|
||||
|
||||
Bundle bundle = this.getArguments();
|
||||
if (mapboxMap != null) {
|
||||
if (bundle != null) {
|
||||
if (bundleForUpdtes != null) {
|
||||
Gson gson = new GsonBuilder()
|
||||
.registerTypeAdapter(Uri.class, new UriDeserializer())
|
||||
.create();
|
||||
|
||||
String gsonPlaceList = bundle.getString("PlaceList");
|
||||
String gsonLatLng = bundle.getString("CurLatLng");
|
||||
String gsonBoundaryCoordinates = bundle.getString("BoundaryCoord");
|
||||
String gsonPlaceList = bundleForUpdtes.getString("PlaceList");
|
||||
String gsonLatLng = bundleForUpdtes.getString("CurLatLng");
|
||||
String gsonBoundaryCoordinates = bundleForUpdtes.getString("BoundaryCoord");
|
||||
Type listType = new TypeToken<List<Place>>() {}.getType();
|
||||
List<Place> placeList = gson.fromJson(gsonPlaceList, listType);
|
||||
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) {
|
||||
MapboxMapOptions options = new MapboxMapOptions()
|
||||
.compassGravity(Gravity.BOTTOM | Gravity.LEFT)
|
||||
.compassMargins(new int[]{12, 0, 0, 24})
|
||||
.styleUrl(Style.OUTDOORS)
|
||||
.logoEnabled(false)
|
||||
.attributionEnabled(false)
|
||||
|
|
@ -471,6 +481,7 @@ public class NearbyMapFragment extends DaggerFragment {
|
|||
mapView.getMapAsync(new OnMapReadyCallback() {
|
||||
@Override
|
||||
public void onMapReady(MapboxMap mapboxMap) {
|
||||
((NearbyActivity)getActivity()).setMapViewTutorialShowCase();
|
||||
NearbyMapFragment.this.mapboxMap = mapboxMap;
|
||||
updateMapSignificantly();
|
||||
}
|
||||
|
|
@ -514,6 +525,7 @@ public class NearbyMapFragment extends DaggerFragment {
|
|||
private void addNearbyMarkerstoMapBoxMap() {
|
||||
|
||||
mapboxMap.addMarkers(baseMarkerOptions);
|
||||
|
||||
mapboxMap.setOnInfoWindowCloseListener(marker -> {
|
||||
if (marker == selected) {
|
||||
bottomSheetDetailsBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
|
||||
|
|
@ -529,6 +541,7 @@ public class NearbyMapFragment extends DaggerFragment {
|
|||
});
|
||||
|
||||
mapboxMap.setOnMarkerClickListener(marker -> {
|
||||
|
||||
if (marker instanceof NearbyMarker) {
|
||||
this.selected = marker;
|
||||
NearbyMarker nearbyMarker = (NearbyMarker) marker;
|
||||
|
|
@ -536,6 +549,7 @@ public class NearbyMapFragment extends DaggerFragment {
|
|||
passInfoToSheet(place);
|
||||
bottomSheetListBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
|
||||
bottomSheetDetailsBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
|
||||
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
|
@ -629,7 +643,19 @@ public class NearbyMapFragment extends DaggerFragment {
|
|||
addAnchorToSmallFABs(fabGallery, getActivity().findViewById(R.id.empty_view).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 -> {
|
||||
//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) {
|
||||
startActivity(mapIntent);
|
||||
}
|
||||
|
|
@ -705,6 +731,7 @@ public class NearbyMapFragment extends DaggerFragment {
|
|||
editor.putString("Title", place.getName());
|
||||
editor.putString("Desc", place.getLongDescription());
|
||||
editor.putString("Category", place.getCategory());
|
||||
editor.putString("WikiDataEntityId", place.getWikiDataEntityId());
|
||||
editor.apply();
|
||||
}
|
||||
|
||||
|
|
@ -740,7 +767,7 @@ public class NearbyMapFragment extends DaggerFragment {
|
|||
if (resultCode == RESULT_OK) {
|
||||
Timber.d("OnActivityResult() parameters: Req code: %d Result code: %d Data: %s",
|
||||
requestCode, resultCode, data);
|
||||
controller.handleImagePicked(requestCode, data, true);
|
||||
controller.handleImagePicked(requestCode, data, true, directPrefs.getString("WikiDataEntityId", null));
|
||||
} else {
|
||||
Timber.e("OnActivityResult() parameters: Req code: %d Result code: %d Data: %s",
|
||||
requestCode, resultCode, data);
|
||||
|
|
@ -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
|
||||
public void onStart() {
|
||||
if (mapView != null) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -17,7 +17,7 @@ import java.util.regex.Pattern;
|
|||
|
||||
import fr.free.nrw.commons.Utils;
|
||||
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;
|
||||
|
||||
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();
|
||||
|
||||
try {
|
||||
// increase the radius gradually to find a satisfactory number of nearby places
|
||||
while (radius <= MAX_RADIUS) {
|
||||
places = getFromWikidataQuery(curLatLng, lang, radius);
|
||||
|
|
@ -54,13 +53,6 @@ public class NearbyPlaces {
|
|||
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
|
||||
if (radius > MAX_RADIUS) {
|
||||
radius = MAX_RADIUS;
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package fr.free.nrw.commons.nearby;
|
|||
import android.graphics.Bitmap;
|
||||
import android.net.Uri;
|
||||
import android.support.annotation.DrawableRes;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
|
@ -50,6 +51,20 @@ public class Place {
|
|||
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() {
|
||||
return !(siteLinks == null || Uri.EMPTY.equals(siteLinks.getWikipediaLink()));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,7 +25,6 @@ import javax.inject.Named;
|
|||
|
||||
import butterknife.BindView;
|
||||
import butterknife.ButterKnife;
|
||||
import fr.free.nrw.commons.CommonsApplication;
|
||||
import fr.free.nrw.commons.R;
|
||||
import fr.free.nrw.commons.Utils;
|
||||
import fr.free.nrw.commons.contributions.ContributionController;
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ import android.widget.RelativeLayout;
|
|||
|
||||
import com.pedrogomez.renderers.RVRendererAdapter;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
|
|
@ -26,6 +25,7 @@ import butterknife.BindView;
|
|||
import butterknife.ButterKnife;
|
||||
import fr.free.nrw.commons.R;
|
||||
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.utils.NetworkUtils;
|
||||
import fr.free.nrw.commons.utils.ViewUtil;
|
||||
|
|
@ -46,6 +46,8 @@ public class NotificationActivity extends NavigationBaseActivity {
|
|||
@BindView(R.id.container) RelativeLayout relativeLayout;
|
||||
|
||||
@Inject NotificationController controller;
|
||||
@Inject
|
||||
MediaWikiApi mediaWikiApi;
|
||||
|
||||
private static final String TAG_NOTIFICATION_WORKER_FRAGMENT = "NotificationWorkerFragment";
|
||||
private NotificationWorkerFragment mNotificationWorkerFragment;
|
||||
|
|
@ -81,7 +83,6 @@ public class NotificationActivity extends NavigationBaseActivity {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
@SuppressLint("CheckResult")
|
||||
private void addNotifications() {
|
||||
Timber.d("Add notifications");
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
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.View;
|
||||
import android.view.ViewGroup;
|
||||
|
|
@ -8,17 +9,23 @@ import android.widget.ImageView;
|
|||
import android.widget.TextView;
|
||||
|
||||
import com.borjabravo.readmoretextview.ReadMoreTextView;
|
||||
import com.bumptech.glide.RequestBuilder;
|
||||
import com.pedrogomez.renderers.Renderer;
|
||||
|
||||
import butterknife.BindView;
|
||||
import butterknife.ButterKnife;
|
||||
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.
|
||||
*/
|
||||
|
||||
public class NotificationRenderer extends Renderer<Notification> {
|
||||
private RequestBuilder<PictureDrawable> requestBuilder;
|
||||
|
||||
@BindView(R.id.title) ReadMoreTextView title;
|
||||
@BindView(R.id.time) TextView time;
|
||||
@BindView(R.id.icon) ImageView icon;
|
||||
|
|
@ -41,23 +48,32 @@ public class NotificationRenderer extends Renderer<Notification> {
|
|||
protected View inflate(LayoutInflater layoutInflater, ViewGroup viewGroup) {
|
||||
View inflatedView = layoutInflater.inflate(R.layout.item_notification, viewGroup, false);
|
||||
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;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void render() {
|
||||
Notification notification = getContent();
|
||||
String str = notification.notificationText.trim();
|
||||
str = str.concat(" ");
|
||||
title.setText(str);
|
||||
setTitle(notification.notificationText);
|
||||
time.setText(notification.date);
|
||||
switch (notification.notificationType) {
|
||||
case THANK_YOU_EDIT:
|
||||
icon.setImageResource(R.drawable.ic_edit_black_24dp);
|
||||
break;
|
||||
default:
|
||||
icon.setImageResource(R.drawable.round_icon_unknown);
|
||||
requestBuilder.load(notification.iconUrl).into(icon);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans up the notification text and sets it as the title
|
||||
* 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{
|
||||
|
|
|
|||
|
|
@ -16,12 +16,13 @@ import javax.annotation.Nullable;
|
|||
import fr.free.nrw.commons.BuildConfig;
|
||||
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;
|
||||
|
||||
public class NotificationUtils {
|
||||
|
||||
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) {
|
||||
if (document == null || !document.hasAttributes()) {
|
||||
|
|
@ -31,6 +32,32 @@ public class NotificationUtils {
|
|||
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) {
|
||||
Element element = (Element) document;
|
||||
String type = element.getAttribute("type");
|
||||
|
|
@ -68,10 +95,17 @@ public class NotificationUtils {
|
|||
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) {
|
||||
return isCommonsNotification(node)
|
||||
&& !getNotificationType(node).equals(UNKNOWN)
|
||||
&& !getNotificationType(node).equals(THANK_YOU_EDIT);
|
||||
return (isCommonsNotification(node)
|
||||
|| isWikidataNotification(node)
|
||||
|| isWikipediaNotification(node))
|
||||
&& !getNotificationType(node).equals(UNKNOWN);
|
||||
}
|
||||
|
||||
public static boolean isBundledNotification(Node document) {
|
||||
|
|
@ -97,7 +131,7 @@ public class NotificationUtils {
|
|||
|
||||
switch (type) {
|
||||
case THANK_YOU_EDIT:
|
||||
notificationText = context.getString(R.string.notifications_thank_you_edit);
|
||||
notificationText = getThankYouEditDescription(document);
|
||||
break;
|
||||
case EDIT_USER_TALK:
|
||||
notificationText = getNotificationText(document);
|
||||
|
|
@ -146,6 +180,16 @@ public class NotificationUtils {
|
|||
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) {
|
||||
String format = "%s%s";
|
||||
Node iconUrl = getNode(getModel(document), "iconUrl");
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -3,13 +3,10 @@ package fr.free.nrw.commons.settings;
|
|||
import android.Manifest;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.ResolveInfo;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
|
|
@ -24,8 +21,6 @@ import android.support.v4.content.FileProvider;
|
|||
import android.widget.Toast;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import javax.inject.Inject;
|
||||
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.Utils;
|
||||
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 {
|
||||
|
||||
|
|
@ -102,6 +97,11 @@ public class SettingsFragment extends PreferenceFragment {
|
|||
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");
|
||||
sendLogsPreference.setOnPreferenceClickListener(preference -> {
|
||||
//first we need to check if we have the necessary permissions
|
||||
|
|
@ -128,8 +128,8 @@ public class SettingsFragment extends PreferenceFragment {
|
|||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
if (requestCode == REQUEST_CODE_WRITE_EXTERNAL_STORAGE) {
|
||||
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
if (requestCode == REQUEST_CODE_WRITE_EXTERNAL_STORAGE && grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
{
|
||||
sendAppLogsViaEmail();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,11 +23,11 @@ import fr.free.nrw.commons.AboutActivity;
|
|||
import fr.free.nrw.commons.BuildConfig;
|
||||
import fr.free.nrw.commons.CommonsApplication;
|
||||
import fr.free.nrw.commons.R;
|
||||
import fr.free.nrw.commons.Utils;
|
||||
import fr.free.nrw.commons.WelcomeActivity;
|
||||
import fr.free.nrw.commons.auth.AccountUtil;
|
||||
import fr.free.nrw.commons.auth.LoginActivity;
|
||||
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.notification.NotificationActivity;
|
||||
import fr.free.nrw.commons.settings.SettingsActivity;
|
||||
|
|
@ -36,6 +36,8 @@ import timber.log.Timber;
|
|||
public abstract class NavigationBaseActivity extends BaseActivity
|
||||
implements NavigationView.OnNavigationItemSelectedListener {
|
||||
|
||||
private static final String FEATURED_IMAGES_CATEGORY = "Category:Featured_pictures_on_Wikimedia_Commons";
|
||||
|
||||
@BindView(R.id.toolbar)
|
||||
Toolbar toolbar;
|
||||
@BindView(R.id.navigation_view)
|
||||
|
|
@ -154,6 +156,10 @@ public abstract class NavigationBaseActivity extends BaseActivity
|
|||
drawerLayout.closeDrawer(navigationView);
|
||||
NotificationActivity.startYourself(this);
|
||||
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:
|
||||
Timber.e("Unknown option [%s] selected from the navigation menu", itemId);
|
||||
return false;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,8 @@
|
|||
package fr.free.nrw.commons.upload;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.graphics.BitmapRegionDecoder;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.support.v7.app.AlertDialog;
|
||||
|
||||
|
|
|
|||
263
app/src/main/java/fr/free/nrw/commons/upload/FileProcessor.java
Normal 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();
|
||||
}
|
||||
}
|
||||
|
|
@ -15,18 +15,84 @@ import android.provider.MediaStore;
|
|||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.FileDescriptor;
|
||||
import java.io.FileInputStream;
|
||||
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 java.math.BigInteger;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Date;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
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
|
||||
* 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 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);
|
||||
} else if (isMediaDocument(uri)) { // MediaProvider
|
||||
|
|
@ -235,4 +301,80 @@ public class FileUtils {
|
|||
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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -47,6 +47,8 @@ import fr.free.nrw.commons.modifications.TemplateRemoveModifier;
|
|||
import fr.free.nrw.commons.mwapi.MediaWikiApi;
|
||||
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
|
||||
implements MediaDetailPagerFragment.MediaDetailProvider,
|
||||
AdapterView.OnItemClickListener,
|
||||
|
|
@ -166,6 +168,7 @@ public class MultipleShareActivity extends AuthenticatedActivity
|
|||
View target = getCurrentFocus();
|
||||
if (target != null) {
|
||||
InputMethodManager imm = (InputMethodManager) target.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||
if (imm != null)
|
||||
imm.hideSoftInputFromWindow(target.getWindowToken(), 0);
|
||||
}
|
||||
getSupportFragmentManager().beginTransaction()
|
||||
|
|
@ -221,8 +224,8 @@ public class MultipleShareActivity extends AuthenticatedActivity
|
|||
|
||||
//TODO: 15/10/17 should location permission be explicitly requested if not provided?
|
||||
//check if location permission is enabled
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
if (ContextCompat.checkSelfPermission(this,Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && ContextCompat.checkSelfPermission(this,Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) {
|
||||
{
|
||||
locationPermitted = true;
|
||||
}
|
||||
}
|
||||
|
|
@ -237,7 +240,7 @@ public class MultipleShareActivity extends AuthenticatedActivity
|
|||
|
||||
private void showDetail(int i) {
|
||||
if (mediaDetails == null || !mediaDetails.isVisible()) {
|
||||
mediaDetails = new MediaDetailPagerFragment(true);
|
||||
mediaDetails = new MediaDetailPagerFragment(true, false);
|
||||
getSupportFragmentManager()
|
||||
.beginTransaction()
|
||||
.replace(R.id.uploadsFragmentContainer, mediaDetails)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
package fr.free.nrw.commons.upload;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.graphics.Point;
|
||||
import android.net.Uri;
|
||||
|
|
@ -11,14 +10,12 @@ import android.text.Editable;
|
|||
import android.text.TextUtils;
|
||||
import android.text.TextWatcher;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.BaseAdapter;
|
||||
import android.widget.EditText;
|
||||
|
|
@ -27,6 +24,8 @@ import android.widget.GridView;
|
|||
import android.widget.RelativeLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import butterknife.BindView;
|
||||
import butterknife.ButterKnife;
|
||||
import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder;
|
||||
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.contributions.Contribution;
|
||||
import fr.free.nrw.commons.media.MediaDetailPagerFragment;
|
||||
import fr.free.nrw.commons.utils.ViewUtil;
|
||||
|
||||
public class MultipleUploadListFragment extends Fragment {
|
||||
|
||||
|
|
@ -41,9 +41,13 @@ public class MultipleUploadListFragment extends Fragment {
|
|||
void OnMultipleUploadInitiated();
|
||||
}
|
||||
|
||||
private GridView photosGrid;
|
||||
@BindView(R.id.multipleShareBackground)
|
||||
GridView photosGrid;
|
||||
|
||||
@BindView(R.id.multipleBaseTitle)
|
||||
EditText baseTitle;
|
||||
|
||||
private PhotoDisplayAdapter photosAdapter;
|
||||
private EditText baseTitle;
|
||||
private TitleTextWatcher textWatcher = new TitleTextWatcher();
|
||||
|
||||
private Point photoSize;
|
||||
|
|
@ -89,9 +93,9 @@ public class MultipleUploadListFragment extends Fragment {
|
|||
if (view == null) {
|
||||
view = LayoutInflater.from(getContext()).inflate(R.layout.layout_upload_item, viewGroup, false);
|
||||
holder = new UploadHolderView();
|
||||
holder.image = (SimpleDraweeView) view.findViewById(R.id.uploadImage);
|
||||
holder.title = (TextView) view.findViewById(R.id.uploadTitle);
|
||||
holder.overlay = (RelativeLayout) view.findViewById(R.id.uploadOverlay);
|
||||
holder.image = view.findViewById(R.id.uploadImage);
|
||||
holder.title = view.findViewById(R.id.uploadTitle);
|
||||
holder.overlay = view.findViewById(R.id.uploadOverlay);
|
||||
|
||||
holder.image.setLayoutParams(new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, photoSize.y));
|
||||
holder.image.setHierarchy(GenericDraweeHierarchyBuilder
|
||||
|
|
@ -129,11 +133,8 @@ public class MultipleUploadListFragment extends Fragment {
|
|||
super.onStop();
|
||||
|
||||
// FIXME: Stops the keyboard from being shown 'stale' while moving out of this fragment into the next
|
||||
View target = getView().findFocus();
|
||||
if (target != null) {
|
||||
InputMethodManager imm = (InputMethodManager) target.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||
imm.hideSoftInputFromWindow(target.getWindowToken(), 0);
|
||||
}
|
||||
View target = getActivity().getCurrentFocus();
|
||||
ViewUtil.hideKeyboard(target);
|
||||
}
|
||||
|
||||
// FIXME: Wrong result type
|
||||
|
|
@ -169,9 +170,7 @@ public class MultipleUploadListFragment extends Fragment {
|
|||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
View view = inflater.inflate(R.layout.fragment_multiple_uploads_list, container, false);
|
||||
photosGrid = (GridView) view.findViewById(R.id.multipleShareBackground);
|
||||
baseTitle = (EditText) view.findViewById(R.id.multipleBaseTitle);
|
||||
|
||||
ButterKnife.bind(this,view);
|
||||
photosAdapter = new PhotoDisplayAdapter();
|
||||
photosGrid.setAdapter(photosAdapter);
|
||||
photosGrid.setOnItemClickListener((AdapterView.OnItemClickListener) getActivity());
|
||||
|
|
@ -182,18 +181,13 @@ public class MultipleUploadListFragment extends Fragment {
|
|||
|
||||
baseTitle.setOnFocusChangeListener((v, hasFocus) -> {
|
||||
if (!hasFocus) {
|
||||
hideKeyboard(v);
|
||||
ViewUtil.hideKeyboard(v);
|
||||
}
|
||||
});
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
public void hideKeyboard(View view) {
|
||||
InputMethodManager inputMethodManager =(InputMethodManager)getActivity().getSystemService(Activity.INPUT_METHOD_SERVICE);
|
||||
inputMethodManager.hideSoftInputFromWindow(view.getWindowToken(), 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
baseTitle.removeTextChangedListener(textWatcher);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
|
@ -1,50 +1,53 @@
|
|||
package fr.free.nrw.commons.upload;
|
||||
|
||||
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.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Point;
|
||||
import android.graphics.Rect;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Environment;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.annotation.RequiresApi;
|
||||
import android.support.design.widget.FloatingActionButton;
|
||||
import android.support.design.widget.Snackbar;
|
||||
import android.support.graphics.drawable.VectorDrawableCompat;
|
||||
import android.support.v4.app.ActivityCompat;
|
||||
import android.support.v4.app.FragmentManager;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import android.support.v7.app.AlertDialog;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
import android.view.animation.DecelerateInterpolator;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder;
|
||||
import com.facebook.drawee.view.SimpleDraweeView;
|
||||
import com.github.chrisbanes.photoview.PhotoView;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
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 javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
|
||||
import butterknife.BindView;
|
||||
import butterknife.ButterKnife;
|
||||
import butterknife.OnClick;
|
||||
import fr.free.nrw.commons.R;
|
||||
import fr.free.nrw.commons.auth.AuthenticatedActivity;
|
||||
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.OnCategoriesSaveHandler;
|
||||
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.ModificationsContentProvider;
|
||||
import fr.free.nrw.commons.modifications.ModifierSequence;
|
||||
import fr.free.nrw.commons.modifications.ModifierSequenceDao;
|
||||
import fr.free.nrw.commons.modifications.TemplateRemoveModifier;
|
||||
|
||||
import fr.free.nrw.commons.utils.ImageUtils;
|
||||
import fr.free.nrw.commons.mwapi.CategoryApi;
|
||||
import fr.free.nrw.commons.mwapi.MediaWikiApi;
|
||||
import fr.free.nrw.commons.utils.ViewUtil;
|
||||
import timber.log.Timber;
|
||||
|
||||
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.FileUtils.getSHA1;
|
||||
|
||||
/**
|
||||
* 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
|
||||
extends AuthenticatedActivity
|
||||
implements SingleUploadFragment.OnUploadActionInitiated,
|
||||
OnCategoriesSaveHandler,SimilarImageDialogFragment.onResponse {
|
||||
|
||||
private static final int REQUEST_PERM_ON_CREATE_STORAGE = 1;
|
||||
OnCategoriesSaveHandler {
|
||||
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 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
|
||||
MediaWikiApi mwApi;
|
||||
@Inject
|
||||
|
|
@ -92,36 +94,55 @@ public class ShareActivity
|
|||
@Inject
|
||||
ModifierSequenceDao modifierSequenceDao;
|
||||
@Inject
|
||||
CategoryApi apiCall;
|
||||
@Inject
|
||||
@Named("default_preferences")
|
||||
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 mimeType;
|
||||
|
||||
private CategorizationFragment categorizationFragment;
|
||||
private Uri mediaUri;
|
||||
private Contribution contribution;
|
||||
private SimpleDraweeView backgroundImageView;
|
||||
|
||||
private boolean cacheFound;
|
||||
|
||||
private GPSExtractor imageObj;
|
||||
private GPSExtractor tempImageObj;
|
||||
private GPSExtractor gpsObj;
|
||||
private String decimalCoords;
|
||||
|
||||
private FileProcessor fileObj;
|
||||
private boolean useNewPermissions = false;
|
||||
private boolean storagePermitted = false;
|
||||
private boolean locationPermitted = false;
|
||||
|
||||
private String title;
|
||||
private String description;
|
||||
private String wikiDataEntityId;
|
||||
private Snackbar snackbar;
|
||||
private boolean duplicateCheckPassed = false;
|
||||
|
||||
private boolean haveCheckedForOtherImages = 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.
|
||||
* Requests Storage permission, if needed.
|
||||
*/
|
||||
@Override
|
||||
public void uploadActionInitiated(String title, String description) {
|
||||
|
|
@ -130,8 +151,6 @@ public class ShareActivity
|
|||
this.description = description;
|
||||
|
||||
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()) {
|
||||
requestPermissions(new String[]{Manifest.permission.READ_EXTERNAL_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)
|
||||
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)
|
||||
&& (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)
|
||||
!= 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() {
|
||||
getFileMetadata(locationPermitted);
|
||||
fileObj.processFileCoordinates(locationPermitted);
|
||||
|
||||
Toast startingToast = Toast.makeText(this, R.string.uploading_started, Toast.LENGTH_LONG);
|
||||
startingToast.show();
|
||||
|
||||
if (!cacheFound) {
|
||||
if (!fileObj.isCacheFound()) {
|
||||
//Has to be called after apiCall.request()
|
||||
cacheController.cacheCategory();
|
||||
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;
|
||||
showPostUpload();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts CategorizationFragment after uploadBegins.
|
||||
*/
|
||||
private void showPostUpload() {
|
||||
if (categorizationFragment == null) {
|
||||
categorizationFragment = new CategorizationFragment();
|
||||
|
|
@ -180,6 +209,11 @@ public class ShareActivity
|
|||
.commit();
|
||||
}
|
||||
|
||||
/**
|
||||
* Send categories to modifications queue after they are selected
|
||||
*
|
||||
* @param categories categories selected
|
||||
*/
|
||||
@Override
|
||||
public void onCategoriesSave(List<String> categories) {
|
||||
if (categories.size() > 0) {
|
||||
|
|
@ -217,9 +251,6 @@ public class ShareActivity
|
|||
finish();
|
||||
}
|
||||
|
||||
protected boolean isNearbyUpload() {
|
||||
return isNearbyUpload;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
|
|
@ -228,7 +259,6 @@ public class ShareActivity
|
|||
setContentView(R.layout.activity_share);
|
||||
ButterKnife.bind(this);
|
||||
initBack();
|
||||
backgroundImageView = (SimpleDraweeView) findViewById(R.id.backgroundImage);
|
||||
backgroundImageView.setHierarchy(GenericDraweeHierarchyBuilder
|
||||
.newInstance(getResources())
|
||||
.setPlaceholderImage(VectorDrawableCompat.create(getResources(),
|
||||
|
|
@ -237,7 +267,54 @@ public class ShareActivity
|
|||
R.drawable.ic_error_outline_black_24dp, getTheme()))
|
||||
.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();
|
||||
|
||||
if (Intent.ACTION_SEND.equals(intent.getAction())) {
|
||||
|
|
@ -257,174 +334,100 @@ public class ShareActivity
|
|||
if (mediaUri != null) {
|
||||
backgroundImageView.setImageURI(mediaUri);
|
||||
}
|
||||
|
||||
if (savedInstanceState != null) {
|
||||
contribution = savedInstanceState.getParcelable("contribution");
|
||||
}
|
||||
|
||||
requestAuthToken();
|
||||
/**
|
||||
* Function to display the zoom and map FAB
|
||||
*/
|
||||
private void showFABMenu() {
|
||||
isFABOpen = true;
|
||||
|
||||
Timber.d("Uri: %s", mediaUri.toString());
|
||||
Timber.d("Ext storage dir: %s", Environment.getExternalStorageDirectory());
|
||||
if (gpsObj != null && gpsObj.imageCoordsExists)
|
||||
mapButton.setVisibility(View.VISIBLE);
|
||||
zoomInButton.setVisibility(View.VISIBLE);
|
||||
|
||||
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;
|
||||
}
|
||||
mainFab.animate().rotationBy(180);
|
||||
mapButton.animate().translationY(-getResources().getDimension(R.dimen.second_fab));
|
||||
zoomInButton.animate().translationY(-getResources().getDimension(R.dimen.first_fab));
|
||||
}
|
||||
|
||||
// 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 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 onRequestPermissionsResult(int requestCode,
|
||||
@NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
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
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
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: {
|
||||
if (grantResults.length >= 1
|
||||
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
if (grantResults.length >= 1 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
locationPermitted = true;
|
||||
performPreUploadProcessingOfFile();
|
||||
}
|
||||
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();
|
||||
checkIfFileExists();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Storage (from submit button) - this needs to be separate from (1) because only the
|
||||
// submit button should bring user to next screen
|
||||
case REQUEST_PERM_ON_SUBMIT_STORAGE: {
|
||||
if (grantResults.length >= 1
|
||||
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
if (grantResults.length >= 1 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
//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
|
||||
performPreUploadProcessingOfFile();
|
||||
checkIfFileExists();
|
||||
|
||||
//Uploading only begins if storage permission granted from arrow icon
|
||||
uploadBegins();
|
||||
snackbar.dismiss();
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void performPreUploadProcessingOfFile() {
|
||||
if (!useNewPermissions || storagePermitted) {
|
||||
if (!duplicateCheckPassed) {
|
||||
//Test SHA1 of image to see if it matches SHA1 of a file on Commons
|
||||
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???...
|
||||
/**
|
||||
* Displays Snackbar to ask for location permissions
|
||||
*/
|
||||
|
||||
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) {
|
||||
private Snackbar requestPermissionUsingSnackBar(String rationale, final String[] perms, final int code) {
|
||||
Snackbar snackbar = Snackbar.make(findViewById(android.R.id.content), rationale,
|
||||
Snackbar.LENGTH_INDEFINITE).setAction(R.string.ok,
|
||||
view -> ActivityCompat.requestPermissions(ShareActivity.this, perms, code));
|
||||
|
|
@ -432,202 +435,44 @@ public class ShareActivity
|
|||
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;
|
||||
/**
|
||||
* Check if file user wants to upload already exists on Commons
|
||||
*/
|
||||
private void checkIfFileExists() {
|
||||
if (!useNewPermissions || storagePermitted) {
|
||||
if (!duplicateCheckPassed) {
|
||||
//Test SHA1 of image to see if it matches SHA1 of a file on Commons
|
||||
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;
|
||||
InputStream inputStream = getContentResolver().openInputStream(mediaUri);
|
||||
String fileSHA1 = getSHA1(inputStream);
|
||||
Timber.d("Input stream created from %s", mediaUri.toString());
|
||||
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);
|
||||
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.w(e, "Error in file " + copyPath);
|
||||
return null;
|
||||
Timber.e(e, "IO Exception: ");
|
||||
}
|
||||
}
|
||||
return filePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets coordinates for category suggestions, either from EXIF data or user location
|
||||
*
|
||||
* @param gpsEnabled if true use GPS
|
||||
*/
|
||||
private void getFileMetadata(boolean gpsEnabled) {
|
||||
Timber.d("Calling GPSExtractor");
|
||||
try {
|
||||
if (imageObj == null) {
|
||||
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);
|
||||
Timber.w("not ready for preprocessing: useNewPermissions=%s storage=%s location=%s",
|
||||
useNewPermissions, storagePermitted, locationPermitted);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
descriptor = getContentResolver().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(),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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
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
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
try {
|
||||
imageObj.unregisterLocationManager();
|
||||
gpsObj.unregisterLocationManager();
|
||||
Timber.d("Unregistered locationManager");
|
||||
} catch (NullPointerException e) {
|
||||
Timber.d("locationManager does not exist, not unregistered");
|
||||
|
|
@ -654,40 +499,157 @@ public class ShareActivity
|
|||
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 {
|
||||
digest = MessageDigest.getInstance("SHA1");
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
Timber.e(e, "Exception while getting Digest");
|
||||
return "";
|
||||
input = this.getContentResolver().openInputStream(imageuri);
|
||||
} catch (FileNotFoundException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
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);
|
||||
Zoom zoomObj = new Zoom(thumbView, flContainer, this.getContentResolver());
|
||||
Bitmap scaledImage = zoomObj.createScaledImage(input, imageuri);
|
||||
|
||||
return output;
|
||||
} catch (IOException e) {
|
||||
Timber.e(e, "IO Exception");
|
||||
return "";
|
||||
} finally {
|
||||
// Load the high-resolution "zoomed-in" image.
|
||||
expandedImageView.setImageBitmap(scaledImage);
|
||||
float startScale = zoomObj.adjustStartEndBounds(startBounds, finalBounds, globalOffset);
|
||||
|
||||
// Hide the thumbnail and show the zoomed-in view. When the animation
|
||||
// begins, it will position the zoomed-in view in the place of the
|
||||
// thumbnail.
|
||||
thumbView.setAlpha(0f);
|
||||
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 {
|
||||
is.close();
|
||||
} catch (IOException e) {
|
||||
Timber.e(e, "Exception on closing MD5 input stream");
|
||||
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);
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,9 @@ import android.view.ViewGroup;
|
|||
import android.view.Window;
|
||||
import android.widget.Button;
|
||||
|
||||
import butterknife.BindView;
|
||||
import butterknife.ButterKnife;
|
||||
import butterknife.OnClick;
|
||||
import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder;
|
||||
import com.facebook.drawee.view.SimpleDraweeView;
|
||||
import com.facebook.imagepipeline.listener.RequestListener;
|
||||
|
|
@ -29,29 +32,33 @@ import fr.free.nrw.commons.R;
|
|||
*/
|
||||
|
||||
public class SimilarImageDialogFragment extends DialogFragment {
|
||||
|
||||
@BindView(R.id.orginalImage)
|
||||
SimpleDraweeView originalImage;
|
||||
@BindView(R.id.possibleImage)
|
||||
SimpleDraweeView possibleImage;
|
||||
@BindView(R.id.postive_button)
|
||||
Button positiveButton;
|
||||
@BindView(R.id.negative_button)
|
||||
Button negativeButton;
|
||||
onResponse mOnResponse;//Implemented interface from shareActivity
|
||||
Boolean gotResponse = false;
|
||||
|
||||
public SimilarImageDialogFragment() {
|
||||
}
|
||||
public interface onResponse{
|
||||
public void onPostiveResponse();
|
||||
public void onPositiveResponse();
|
||||
|
||||
public void onNegativeResponse();
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
View view = inflater.inflate(R.layout.fragment_similar_image_dialog, container, false);
|
||||
ButterKnife.bind(this,view);
|
||||
Set<RequestListener> requestListeners = new HashSet<>();
|
||||
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
|
||||
.newInstance(getResources())
|
||||
.setPlaceholderImage(VectorDrawableCompat.create(getResources(),
|
||||
|
|
@ -70,22 +77,6 @@ public class SimilarImageDialogFragment extends DialogFragment {
|
|||
originalImage.setImageURI(Uri.fromFile(new File(getArguments().getString("originalImagePath"))));
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
@ -105,8 +96,23 @@ public class SimilarImageDialogFragment extends DialogFragment {
|
|||
@Override
|
||||
public void onDismiss(DialogInterface dialog) {
|
||||
// I user dismisses dialog by pressing outside the dialog.
|
||||
if(!gotResponse)
|
||||
if (!gotResponse) {
|
||||
mOnResponse.onNegativeResponse();
|
||||
}
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,21 +1,18 @@
|
|||
package fr.free.nrw.commons.upload;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.graphics.Color;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.v4.view.ViewCompat;
|
||||
import android.support.v7.app.AlertDialog;
|
||||
import android.text.Editable;
|
||||
import android.text.Html;
|
||||
import android.text.TextWatcher;
|
||||
import android.util.Log;
|
||||
import android.text.method.LinkMovementMethod;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
|
|
@ -23,7 +20,6 @@ import android.view.MenuItem;
|
|||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.ArrayAdapter;
|
||||
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.di.CommonsDaggerSupportFragment;
|
||||
import fr.free.nrw.commons.settings.Prefs;
|
||||
import fr.free.nrw.commons.utils.ViewUtil;
|
||||
import timber.log.Timber;
|
||||
|
||||
import static android.view.MotionEvent.ACTION_DOWN;
|
||||
import static android.view.MotionEvent.ACTION_UP;
|
||||
|
||||
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.licenseSpinner) Spinner licenseSpinner;
|
||||
|
||||
|
||||
@Inject @Named("default_preferences") SharedPreferences prefs;
|
||||
@Inject @Named("direct_nearby_upload_prefs") SharedPreferences directPrefs;
|
||||
|
||||
|
|
@ -166,13 +163,13 @@ public class SingleUploadFragment extends CommonsDaggerSupportFragment {
|
|||
|
||||
titleEdit.setOnFocusChangeListener((v, hasFocus) -> {
|
||||
if (!hasFocus) {
|
||||
hideKeyboard(v);
|
||||
ViewUtil.hideKeyboard(v);
|
||||
}
|
||||
});
|
||||
|
||||
descEdit.setOnFocusChangeListener((v, hasFocus) -> {
|
||||
if(!hasFocus){
|
||||
hideKeyboard(v);
|
||||
ViewUtil.hideKeyboard(v);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -181,12 +178,6 @@ public class SingleUploadFragment extends CommonsDaggerSupportFragment {
|
|||
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
|
||||
public void onDestroyView() {
|
||||
titleEdit.removeTextChangedListener(textWatcher);
|
||||
|
|
@ -222,21 +213,9 @@ public class SingleUploadFragment extends CommonsDaggerSupportFragment {
|
|||
setLicenseSummary(license);
|
||||
prefs.edit()
|
||||
.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)
|
||||
void setTitleDescButton() {
|
||||
|
|
@ -294,7 +273,9 @@ public class SingleUploadFragment extends CommonsDaggerSupportFragment {
|
|||
|
||||
@SuppressLint("StringFormatInvalid")
|
||||
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
|
||||
|
|
@ -309,11 +290,8 @@ public class SingleUploadFragment extends CommonsDaggerSupportFragment {
|
|||
super.onStop();
|
||||
|
||||
// FIXME: Stops the keyboard from being shown 'stale' while moving out of this fragment into the next
|
||||
View target = getView().findFocus();
|
||||
if (target != null) {
|
||||
InputMethodManager imm = (InputMethodManager) target.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||
imm.hideSoftInputFromWindow(target.getWindowToken(), 0);
|
||||
}
|
||||
View target = getActivity().getCurrentFocus();
|
||||
ViewUtil.hideKeyboard(target);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
|
|
@ -354,6 +332,7 @@ public class SingleUploadFragment extends CommonsDaggerSupportFragment {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
private void showInfoAlert (int titleStringID, int messageStringID){
|
||||
new AlertDialog.Builder(getContext())
|
||||
.setTitle(titleStringID)
|
||||
|
|
|
|||
|
|
@ -91,7 +91,7 @@ public class UploadController {
|
|||
* @param decimalCoords the coordinates in decimal. (e.g. "37.51136|-77.602615")
|
||||
* @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;
|
||||
|
||||
//TODO: Modify this to include coords
|
||||
|
|
@ -101,6 +101,7 @@ public class UploadController {
|
|||
|
||||
contribution.setTag("mimeType", mimeType);
|
||||
contribution.setSource(source);
|
||||
contribution.setWikiDataEntityId(wikiDataEntityId);
|
||||
|
||||
//Calls the next overloaded method
|
||||
startUpload(contribution, onComplete);
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import android.app.PendingIntent;
|
|||
import android.content.ContentResolver;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.NotificationCompat;
|
||||
|
|
@ -23,7 +22,6 @@ import java.util.regex.Matcher;
|
|||
import java.util.regex.Pattern;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
|
||||
import fr.free.nrw.commons.HandlerService;
|
||||
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.mwapi.MediaWikiApi;
|
||||
import fr.free.nrw.commons.mwapi.UploadResult;
|
||||
import fr.free.nrw.commons.wikidata.WikidataEditService;
|
||||
import timber.log.Timber;
|
||||
|
||||
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";
|
||||
|
||||
@Inject MediaWikiApi mwApi;
|
||||
@Inject WikidataEditService wikidataEditService;
|
||||
@Inject SessionManager sessionManager;
|
||||
@Inject @Named("default_preferences") SharedPreferences prefs;
|
||||
@Inject ContributionDao contributionDao;
|
||||
|
||||
private NotificationManager notificationManager;
|
||||
|
|
@ -137,6 +136,7 @@ public class UploadService extends HandlerService<Contribution> {
|
|||
|
||||
@Override
|
||||
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) {
|
||||
case ACTION_UPLOAD_FILE:
|
||||
|
||||
|
|
@ -231,10 +231,10 @@ public class UploadService extends HandlerService<Contribution> {
|
|||
Timber.d("Successfully revalidated token!");
|
||||
} else {
|
||||
Timber.d("Unable to revalidate :(");
|
||||
// TODO: Put up a new notification, ask them to re-login
|
||||
stopForeground(true);
|
||||
Toast failureToast = Toast.makeText(this, R.string.authentication_failed, Toast.LENGTH_LONG);
|
||||
failureToast.show();
|
||||
sessionManager.forceLogin(this);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
@ -253,6 +253,7 @@ public class UploadService extends HandlerService<Contribution> {
|
|||
if (!resultStatus.equals("Success")) {
|
||||
showFailedNotification(contribution);
|
||||
} else {
|
||||
wikidataEditService.createClaimWithLogging(contribution.getWikiDataEntityId(), filename);
|
||||
contribution.setFilename(uploadResult.getCanonicalFilename());
|
||||
contribution.setImageUrl(uploadResult.getImageUrl());
|
||||
contribution.setState(Contribution.STATE_COMPLETED);
|
||||
|
|
|
|||
115
app/src/main/java/fr/free/nrw/commons/upload/Zoom.java
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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"));
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +1,27 @@
|
|||
package fr.free.nrw.commons.utils;
|
||||
|
||||
import android.app.WallpaperManager;
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapRegionDecoder;
|
||||
import android.graphics.Color;
|
||||
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;
|
||||
|
||||
/**
|
||||
|
|
@ -132,4 +149,52 @@ public class ImageUtils {
|
|||
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ public class NetworkUtils {
|
|||
|
||||
public static boolean isInternetConnectionEstablished(Context context) {
|
||||
ConnectivityManager cm =
|
||||
(ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||
(ConnectivityManager)context.getApplicationContext().getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||
|
||||
NetworkInfo activeNetwork = cm.getActiveNetworkInfo();
|
||||
return activeNetwork != null &&
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package fr.free.nrw.commons.utils;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.Locale;
|
||||
|
||||
import info.debatty.java.stringsimilarity.Levenshtein;
|
||||
|
||||
|
|
@ -28,8 +29,8 @@ public class StringSortingUtils {
|
|||
}
|
||||
|
||||
private static double calculateSimilarity(String firstString, String secondString) {
|
||||
String longer = firstString.toLowerCase();
|
||||
String shorter = secondString.toLowerCase();
|
||||
String longer = firstString.toLowerCase(Locale.getDefault());
|
||||
String shorter = secondString.toLowerCase(Locale.getDefault());
|
||||
|
||||
if (firstString.length() < secondString.length()) {
|
||||
longer = secondString;
|
||||
|
|
|
|||
|
|
@ -5,10 +5,15 @@ import android.content.Context;
|
|||
import android.support.design.widget.Snackbar;
|
||||
import android.view.Display;
|
||||
import android.view.View;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.widget.Toast;
|
||||
|
||||
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) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
BIN
app/src/main/res/drawable-hdpi/ic_zoom_in_white_24dp.png
Normal file
|
After Width: | Height: | Size: 422 B |
BIN
app/src/main/res/drawable-hdpi/ic_zoom_out_white_24dp.png
Normal file
|
After Width: | Height: | Size: 412 B |
BIN
app/src/main/res/drawable-mdpi/ic_zoom_in_white_24dp.png
Normal file
|
After Width: | Height: | Size: 257 B |
BIN
app/src/main/res/drawable-mdpi/ic_zoom_out_white_24dp.png
Normal file
|
After Width: | Height: | Size: 249 B |
BIN
app/src/main/res/drawable-xhdpi/ic_zoom_in_white_24dp.png
Normal file
|
After Width: | Height: | Size: 486 B |
BIN
app/src/main/res/drawable-xhdpi/ic_zoom_out_white_24dp.png
Normal file
|
After Width: | Height: | Size: 470 B |
BIN
app/src/main/res/drawable-xxhdpi/ic_zoom_in_white_24dp.png
Normal file
|
After Width: | Height: | Size: 737 B |
BIN
app/src/main/res/drawable-xxhdpi/ic_zoom_out_white_24dp.png
Normal file
|
After Width: | Height: | Size: 731 B |
BIN
app/src/main/res/drawable-xxxhdpi/ic_zoom_in_white_24dp.png
Normal file
|
After Width: | Height: | Size: 954 B |
BIN
app/src/main/res/drawable-xxxhdpi/ic_zoom_out_white_24dp.png
Normal file
|
After Width: | Height: | Size: 925 B |
|
|
@ -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>
|
||||
|
|
@ -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: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"/>
|
||||
</vector>
|
||||
|
|
|
|||
9
app/src/main/res/drawable/ic_star_black_24dp.xml
Normal 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>
|
||||