diff --git a/CHANGELOG.md b/CHANGELOG.md index 1688b3b027..60562d7519 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # Wikimedia Commons for Android +## v2.8.1 +- Fixed bug with category edits not being sent to server + +## v2.8.0 +- Fixed failed uploads by modifying auth token +- Fixed crashes during upload by storing file temporarily +- Added automatic Wikidata p18 edits upon Nearby upload +- Added Explore feature to browse other Commons images, including featured images +- Added Achievements feature to see current level and upload stats +- Added quiz for users with high deletion rates +- Added first run tutorial for Nearby +- Various small improvements to ShareActivity UI + ## v2.7.2 - Modified subtext for "automatically get current location" setting to emphasize that it will reveal user's location diff --git a/CREDITS b/CREDITS index 6847ac9b66..a620e5a88e 100644 --- a/CREDITS +++ b/CREDITS @@ -40,6 +40,12 @@ their contribution to the product. * Suchit Kar * Tanvi Dadu * Ujjwal Agrawal +* Mansi Agarwal +* Siddharth Vaish +* Ashish Kumar +* Ilgaz Er +* Alicia Bendz +* Kaartic Sivaraam 3rd party open source libraries used: * Butterknife diff --git a/README.md b/README.md index 57ba0db031..70a5128aad 100644 --- a/README.md +++ b/README.md @@ -39,11 +39,9 @@ We try to have an extensive documentation at [our wiki here at Github][5]: * [Fresco][27] * [Stetho][28] * [Dagger][29] -* [AndroidSVG][30] -* [Java-HTTP-Fluent][31] -* [CircleProgressBar][32] -* [Glide][33] -* [Leak Canary][34] +* [Java-HTTP-Fluent][30] +* [CircleProgressBar][31] +* [Leak Canary][32] ## License ## @@ -80,8 +78,6 @@ This software is open source, licensed under the [Apache License 2.0][4]. [27]: https://github.com/facebook/fresco [28]: https://github.com/facebook/stetho [29]: https://github.com/google/dagger -[30]: https://github.com/BigBadaboom/androidsvg -[31]: https://github.com/yuvipanda/java-http-fluent/blob/master/src/main/java/in/yuvi/http/fluent/Http.java -[32]: https://github.com/dinuscxj/CircleProgressBar -[33]: https://github.com/bumptech/glide -[34]: https://github.com/square/leakcanary +[30]: https://github.com/yuvipanda/java-http-fluent/blob/master/src/main/java/in/yuvi/http/fluent/Http.java +[31]: https://github.com/dinuscxj/CircleProgressBar +[32]: https://github.com/square/leakcanary diff --git a/app/build.gradle b/app/build.gradle index 05050e4fb1..c6e6eaaf42 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -7,7 +7,6 @@ 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' @@ -21,12 +20,12 @@ dependencies { implementation 'com.jakewharton.timber:timber:4.5.1' implementation 'info.debatty:java-string-similarity:0.24' implementation 'com.borjabravo:readmoretextview:2.1.0' - - implementation 'com.android.support.constraint:constraint-layout:1.1.0' + implementation 'com.android.support.constraint:constraint-layout:1.1.2' implementation('com.mapbox.mapboxsdk:mapbox-android-sdk:5.5.0@aar') { transitive = true } implementation 'com.github.deano2390:MaterialShowcaseView:1.2.0' + //noinspection GradleCompatible implementation "com.android.support:support-v4:$SUPPORT_LIB_VERSION" implementation "com.android.support:appcompat-v7:$SUPPORT_LIB_VERSION" @@ -38,9 +37,28 @@ dependencies { 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 '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 'com.facebook.fresco:fresco:1.3.0' + implementation 'com.facebook.stetho:stetho:1.5.0' implementation 'com.android.support:multidex:1.0.3' + testImplementation 'junit:junit:4.12' + testImplementation 'org.robolectric:robolectric:3.7.1' + testImplementation 'org.robolectric:multidex:3.4.2' + testImplementation 'com.squareup.okhttp3:mockwebserver:3.8.1' + androidTestImplementation 'com.squareup.okhttp3:mockwebserver:3.8.1' + androidTestImplementation "com.android.support:support-annotations:${project.SUPPORT_LIB_VERSION}" + androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1' + debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.5.1' + releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.1' + testImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.1' 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' @@ -53,7 +71,6 @@ dependencies { 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" testImplementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" testImplementation 'junit:junit:4.12' @@ -61,21 +78,17 @@ dependencies { testImplementation 'com.nhaarman:mockito-kotlin:1.5.0' testImplementation 'com.squareup.okhttp3:mockwebserver:3.8.1' implementation 'com.dinuscxj:circleprogressbar:1.1.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: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" + //For handling runtime permissions + implementation 'com.karumi:dexter:5.0.0' } @@ -87,16 +100,14 @@ android { defaultConfig { applicationId 'fr.free.nrw.commons' - versionCode 85 - versionName '2.7.2' + versionCode 88 + versionName '2.8.1' setProperty("archivesBaseName", "app-commons-v$versionName-" + getBranchName()) minSdkVersion project.minSdkVersion targetSdkVersion project.targetSdkVersion testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" vectorDrawables.useSupportLibrary = true - - multiDexEnabled true } testOptions { @@ -117,7 +128,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', 'proguard-glide.txt' + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt' } debug { testCoverageEnabled true @@ -149,6 +160,7 @@ android { buildConfigField "String", "CONTRIBUTION_AUTHORITY", "\"fr.free.nrw.commons.contributions.contentprovider\"" buildConfigField "String", "MODIFICATION_AUTHORITY", "\"fr.free.nrw.commons.modifications.contentprovider\"" buildConfigField "String", "CATEGORY_AUTHORITY", "\"fr.free.nrw.commons.categories.contentprovider\"" + buildConfigField "String", "RECENT_SEARCH_AUTHORITY", "\"fr.free.nrw.commons.explore.recentsearches.contentprovider\"" dimension 'tier' } @@ -175,6 +187,7 @@ android { buildConfigField "String", "CONTRIBUTION_AUTHORITY", "\"fr.free.nrw.commons.beta.contributions.contentprovider\"" buildConfigField "String", "MODIFICATION_AUTHORITY", "\"fr.free.nrw.commons.beta.modifications.contentprovider\"" buildConfigField "String", "CATEGORY_AUTHORITY", "\"fr.free.nrw.commons.beta.categories.contentprovider\"" + buildConfigField "String", "RECENT_SEARCH_AUTHORITY", "\"fr.free.nrw.commons.beta.explore.recentsearches.contentprovider\"" dimension 'tier' } diff --git a/app/proguard-glide.txt b/app/proguard-glide.txt deleted file mode 100644 index ef3437660a..0000000000 --- a/app/proguard-glide.txt +++ /dev/null @@ -1,9 +0,0 @@ --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 \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e80e2a84f3..506a8c5e0e 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,4 +1,3 @@ - @@ -18,7 +17,6 @@ - @@ -44,8 +42,7 @@ - @@ -54,12 +51,6 @@ - - - @@ -123,6 +114,7 @@ android:label="@string/Achievements" /> + + @@ -141,6 +134,7 @@ + @@ -152,6 +146,7 @@ + @@ -190,7 +185,7 @@ @@ -207,4 +202,4 @@ - \ No newline at end of file + diff --git a/app/src/main/java/fr/free/nrw/commons/CommonsApplication.java b/app/src/main/java/fr/free/nrw/commons/CommonsApplication.java index cb47f75e9a..2ee5f305fb 100644 --- a/app/src/main/java/fr/free/nrw/commons/CommonsApplication.java +++ b/app/src/main/java/fr/free/nrw/commons/CommonsApplication.java @@ -1,9 +1,13 @@ package fr.free.nrw.commons; +import android.app.Application; +import android.app.NotificationChannel; +import android.app.NotificationManager; import android.content.Context; import android.content.SharedPreferences; import android.database.sqlite.SQLiteDatabase; -import android.support.multidex.MultiDexApplication; +import android.os.Build; +import android.support.annotation.RequiresApi; import com.facebook.drawee.backends.pipeline.Fresco; import com.facebook.imagepipeline.core.ImagePipelineConfig; @@ -41,7 +45,7 @@ resDialogCommentPrompt = R.string.crash_dialog_comment_prompt, resDialogOkToast = R.string.crash_dialog_ok_toast ) -public class CommonsApplication extends MultiDexApplication { +public class CommonsApplication extends Application { @Inject SessionManager sessionManager; @Inject DBOpenHelper dbOpenHelper; @@ -50,6 +54,11 @@ public class CommonsApplication extends MultiDexApplication { @Inject @Named("application_preferences") SharedPreferences applicationPrefs; @Inject @Named("prefs") SharedPreferences otherPrefs; + /** + * Constants begin + */ + public static final int OPEN_APPLICATION_DETAIL_SETTINGS = 1001; + public static final String DEFAULT_EDIT_SUMMARY = "Uploaded using [[COM:MOA|Commons Mobile App]]"; public static final String FEEDBACK_EMAIL = "commons-app-android@googlegroups.com"; @@ -60,6 +69,12 @@ public class CommonsApplication extends MultiDexApplication { public static final String LOGS_PRIVATE_EMAIL_SUBJECT = "Commons Android App (%s) Logs"; + public static final String NOTIFICATION_CHANNEL_ID_ALL = "CommonsNotificationAll"; + + /** + * Constants End + */ + private RefWatcher refWatcher; @@ -92,10 +107,23 @@ public void onCreate() { Stetho.initializeWithDefaults(this); } + + if (Build.VERSION.SDK_INT > Build.VERSION_CODES.O) { + createNotificationChannel(); + } + // Fire progress callbacks for every 3% of uploaded content System.setProperty("in.yuvi.http.fluent.PROGRESS_TRIGGER_THRESHOLD", "3.0"); } + @RequiresApi(26) + private void createNotificationChannel() { + NotificationChannel channel = new NotificationChannel( + NOTIFICATION_CHANNEL_ID_ALL, + getString(R.string.notifications_channel_name_all), NotificationManager.IMPORTANCE_NONE); + NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + manager.createNotificationChannel(channel); + } /** * Helps in setting up LeakCanary library diff --git a/app/src/main/java/fr/free/nrw/commons/MediaDataExtractor.java b/app/src/main/java/fr/free/nrw/commons/MediaDataExtractor.java index affb575282..16941b35af 100644 --- a/app/src/main/java/fr/free/nrw/commons/MediaDataExtractor.java +++ b/app/src/main/java/fr/free/nrw/commons/MediaDataExtractor.java @@ -2,6 +2,7 @@ import android.support.annotation.Nullable; +import android.text.TextUtils; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; @@ -161,7 +162,11 @@ private Node findTemplate(Element parentNode, String title_) throws IOException Node node = nodes.item(i); if (node.getNodeName().equals("template")) { String foundTitle = getTemplateTitle(node); - if (title.equals(new PageTitle(foundTitle).getDisplayText())) { + String displayText = new PageTitle(foundTitle).getDisplayText(); + //replaced equals with contains because multiple sources had multiple formats + //say from two sources I had {{Location|12.958117388888889|77.6440805}} & {{Location dec|47.99081|7.845416|heading:255.9}}, + //So exact string match would show null results for uploads via web + if (!(TextUtils.isEmpty(displayText)) && displayText.contains(title)) { return node; } } diff --git a/app/src/main/java/fr/free/nrw/commons/Utils.java b/app/src/main/java/fr/free/nrw/commons/Utils.java index 4e4b46b01f..1fdff8eab6 100644 --- a/app/src/main/java/fr/free/nrw/commons/Utils.java +++ b/app/src/main/java/fr/free/nrw/commons/Utils.java @@ -19,7 +19,9 @@ import java.io.InputStreamReader; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; +import java.util.LinkedHashMap; import java.util.Locale; +import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -110,6 +112,31 @@ public static int licenseNameFor(String license) { throw new RuntimeException("Unrecognized license value: " + license); } + /** + * Generates license url with given ID + * @param license License ID + * @return Url of license + */ + + + @NonNull + public static String licenseUrlFor(String license) { + switch (license) { + case Prefs.Licenses.CC_BY_3: + return "https://creativecommons.org/licenses/by/3.0/"; + case Prefs.Licenses.CC_BY_4: + return "https://creativecommons.org/licenses/by/4.0/"; + case Prefs.Licenses.CC_BY_SA_3: + return "https://creativecommons.org/licenses/by-sa/3.0/"; + case Prefs.Licenses.CC_BY_SA_4: + return "https://creativecommons.org/licenses/by-sa/4.0/"; + case Prefs.Licenses.CC0: + return "https://creativecommons.org/publicdomain/zero/1.0/"; + default: + throw new RuntimeException("Unrecognized license value: " + license); + } + } + /** * Fixing incorrect extension * @param title File name @@ -215,4 +242,14 @@ public static Bitmap getScreenShot(View view) { return bitmap; } + public static Map arraysToMap(K[] kArray, V[] vArray){ + if(kArray.length!=vArray.length) + throw new RuntimeException("arraysToMap array sizes don't match"); + Map map=new LinkedHashMap<>(); + for (int i=0;i 0) { + return accounts[0]; + } + } catch (SecurityException e) { + Timber.e(e); + } + return null; + } + + @Nullable + public static String getUserName(Context context) { + Account account = account(context); + return account == null ? null : account.name; + } + + @Nullable + public static String getPassword(Context context) { + Account account = account(context); + return account == null ? null : accountManager(context).getPassword(account); + } + + private static AccountManager accountManager(Context context) { + return AccountManager.get(context); + } + } diff --git a/app/src/main/java/fr/free/nrw/commons/auth/AuthenticatedActivity.java b/app/src/main/java/fr/free/nrw/commons/auth/AuthenticatedActivity.java index 611cb7975d..dd3c1f607e 100644 --- a/app/src/main/java/fr/free/nrw/commons/auth/AuthenticatedActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/auth/AuthenticatedActivity.java @@ -16,7 +16,8 @@ public abstract class AuthenticatedActivity extends NavigationBaseActivity { - @Inject SessionManager sessionManager; + @Inject + protected SessionManager sessionManager; @Inject MediaWikiApi mediaWikiApi; private String authCookie; diff --git a/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.java b/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.java index 35e5f4c943..1f2ebf0479 100644 --- a/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.java @@ -16,6 +16,7 @@ import android.support.design.widget.TextInputLayout; import android.support.v4.app.NavUtils; import android.support.v4.content.ContextCompat; +import android.support.v7.app.AlertDialog; import android.support.v7.app.AppCompatDelegate; import android.text.Editable; import android.text.TextWatcher; @@ -26,6 +27,7 @@ import android.widget.Button; import android.widget.EditText; import android.widget.TextView; +import android.widget.Toast; import java.io.IOException; import java.util.Locale; @@ -41,6 +43,7 @@ import fr.free.nrw.commons.R; import fr.free.nrw.commons.Utils; import fr.free.nrw.commons.WelcomeActivity; +import fr.free.nrw.commons.category.CategoryImagesActivity; import fr.free.nrw.commons.contributions.ContributionsActivity; import fr.free.nrw.commons.di.ApplicationlessInjection; import fr.free.nrw.commons.mwapi.MediaWikiApi; @@ -55,12 +58,12 @@ import static android.view.KeyEvent.KEYCODE_ENTER; import static android.view.View.VISIBLE; import static android.view.inputmethod.EditorInfo.IME_ACTION_DONE; -import static fr.free.nrw.commons.auth.AccountUtil.ACCOUNT_TYPE; import static fr.free.nrw.commons.auth.AccountUtil.AUTH_TOKEN_TYPE; public class LoginActivity extends AccountAuthenticatorActivity { public static final String PARAM_USERNAME = "fr.free.nrw.commons.login.username"; + private static final String FEATURED_IMAGES_CATEGORY = "Category:Featured_pictures_on_Wikimedia_Commons"; @Inject MediaWikiApi mwApi; @Inject SessionManager sessionManager; @@ -77,6 +80,7 @@ public class LoginActivity extends AccountAuthenticatorActivity { @BindView(R.id.login_credentials) TextView loginCredentials; @BindView(R.id.two_factor_container) TextInputLayout twoFactorContainer; @BindView(R.id.forgotPassword) HtmlTextView forgotPasswordText; + @BindView(R.id.skipLogin) HtmlTextView skipLoginText; ProgressDialog progressDialog; private AppCompatDelegate delegate; @@ -126,6 +130,15 @@ public void onCreate(Bundle savedInstanceState) { signupButton.setOnClickListener(view -> signUp()); forgotPasswordText.setOnClickListener(view -> forgotPassword()); + skipLoginText.setOnClickListener(view -> new AlertDialog.Builder(this).setTitle(R.string.skip_login_title) + .setMessage(R.string.skip_login_message) + .setCancelable(false) + .setPositiveButton(R.string.yes, (dialog, which) -> { + dialog.cancel(); + skipLogin(); + }) + .setNegativeButton(R.string.no, (dialog, which) -> dialog.cancel()) + .show()); if(BuildConfig.FLAVOR.equals("beta")){ loginCredentials.setText(getString(R.string.login_credential)); @@ -134,6 +147,17 @@ public void onCreate(Bundle savedInstanceState) { } } + /** + * This function is called when user skips the login. + * It redirects the user to Explore Activity. + */ + private void skipLogin() { + prefs.edit().putBoolean("login_skipped", true).apply(); + CategoryImagesActivity.startYourself(this, getString(R.string.title_activity_explore), FEATURED_IMAGES_CATEGORY); + finish(); + + } + private void forgotPassword() { Utils.handleWebUrl(this, Uri.parse(BuildConfig.FORGOT_PASSWORD_URL)); } @@ -160,9 +184,15 @@ protected void onResume() { if (sessionManager.getCurrentAccount() != null && sessionManager.isUserLoggedIn() && sessionManager.getCachedAuthCookie() != null) { + prefs.edit().putBoolean("login_skipped", false).apply(); sessionManager.revalidateAuthToken(); startMainActivity(); } + + if (prefs.getBoolean("login_skipped", false)){ + skipLogin(); + } + } @Override @@ -242,7 +272,7 @@ private void handlePassResult(String username, String password) { if (response != null) { Bundle authResult = new Bundle(); authResult.putString(AccountManager.KEY_ACCOUNT_NAME, username); - authResult.putString(AccountManager.KEY_ACCOUNT_TYPE, ACCOUNT_TYPE); + authResult.putString(AccountManager.KEY_ACCOUNT_TYPE, BuildConfig.ACCOUNT_TYPE); response.onResult(authResult); } } diff --git a/app/src/main/java/fr/free/nrw/commons/auth/SessionManager.java b/app/src/main/java/fr/free/nrw/commons/auth/SessionManager.java index 4a22b88c90..cd23d12829 100644 --- a/app/src/main/java/fr/free/nrw/commons/auth/SessionManager.java +++ b/app/src/main/java/fr/free/nrw/commons/auth/SessionManager.java @@ -76,10 +76,6 @@ public void createAccount(@Nullable AccountAuthenticatorResponse response, ContentResolver.setSyncAutomatically(account, BuildConfig.MODIFICATION_AUTHORITY, true); // Enable sync by default! } - private AccountManager accountManager() { - return AccountManager.get(context); - } - /** * @return Account|null */ @@ -95,6 +91,22 @@ public Account getCurrentAccount() { return currentAccount; } + @Nullable + public String getUserName() { + Account account = getCurrentAccount(); + return account == null ? null : account.name; + } + + @Nullable + public String getPassword() { + Account account = getCurrentAccount(); + return account == null ? null : accountManager().getPassword(account); + } + + private AccountManager accountManager() { + return AccountManager.get(context); + } + public Boolean revalidateAuthToken() { AccountManager accountManager = AccountManager.get(context); Account curAccount = getCurrentAccount(); @@ -103,12 +115,13 @@ public Boolean revalidateAuthToken() { return false; // This should never happen } - accountManager.invalidateAuthToken(BuildConfig.ACCOUNT_TYPE, mediaWikiApi.getAuthCookie()); + accountManager.invalidateAuthToken(BuildConfig.ACCOUNT_TYPE, null); String authCookie = getAuthCookie(); if (authCookie == null) { return false; } + mediaWikiApi.setAuthCookie(authCookie); return true; } diff --git a/app/src/main/java/fr/free/nrw/commons/auth/WikiAccountAuthenticator.java b/app/src/main/java/fr/free/nrw/commons/auth/WikiAccountAuthenticator.java index 7a0980d80f..2f71a69b42 100644 --- a/app/src/main/java/fr/free/nrw/commons/auth/WikiAccountAuthenticator.java +++ b/app/src/main/java/fr/free/nrw/commons/auth/WikiAccountAuthenticator.java @@ -13,10 +13,7 @@ import android.support.annotation.Nullable; import fr.free.nrw.commons.BuildConfig; -import fr.free.nrw.commons.contributions.ContributionsContentProvider; -import fr.free.nrw.commons.modifications.ModificationsContentProvider; -import static fr.free.nrw.commons.auth.AccountUtil.ACCOUNT_TYPE; import static fr.free.nrw.commons.auth.AccountUtil.AUTH_TOKEN_TYPE; public class WikiAccountAuthenticator extends AbstractAccountAuthenticator { @@ -99,7 +96,7 @@ public Bundle hasFeatures(@NonNull AccountAuthenticatorResponse response, } private boolean supportedAccountType(@Nullable String type) { - return ACCOUNT_TYPE.equals(type); + return BuildConfig.ACCOUNT_TYPE.equals(type); } private Bundle addAccount(AccountAuthenticatorResponse response) { diff --git a/app/src/main/java/fr/free/nrw/commons/category/CategoriesAdapterFactory.java b/app/src/main/java/fr/free/nrw/commons/category/CategoriesAdapterFactory.java index 417121c443..77b6615da0 100644 --- a/app/src/main/java/fr/free/nrw/commons/category/CategoriesAdapterFactory.java +++ b/app/src/main/java/fr/free/nrw/commons/category/CategoriesAdapterFactory.java @@ -1,24 +1,23 @@ package fr.free.nrw.commons.category; import com.pedrogomez.renderers.ListAdapteeCollection; -import com.pedrogomez.renderers.RVRendererAdapter; import com.pedrogomez.renderers.RendererBuilder; import java.util.Collections; import java.util.List; -class CategoriesAdapterFactory { - private final CategoriesRenderer.CategoryClickedListener listener; +public class CategoriesAdapterFactory { + private final CategoryClickedListener listener; - CategoriesAdapterFactory(CategoriesRenderer.CategoryClickedListener listener) { + public CategoriesAdapterFactory(CategoryClickedListener listener) { this.listener = listener; } - public RVRendererAdapter create(List placeList) { + public CategoryRendererAdapter create(List placeList) { RendererBuilder builder = new RendererBuilder() .bind(CategoryItem.class, new CategoriesRenderer(listener)); ListAdapteeCollection collection = new ListAdapteeCollection<>( placeList != null ? placeList : Collections.emptyList()); - return new RVRendererAdapter<>(builder, collection); + return new CategoryRendererAdapter(builder, collection); } } diff --git a/app/src/main/java/fr/free/nrw/commons/category/CategoriesModel.java b/app/src/main/java/fr/free/nrw/commons/category/CategoriesModel.java new file mode 100644 index 0000000000..d23c93c2a2 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/category/CategoriesModel.java @@ -0,0 +1,225 @@ +package fr.free.nrw.commons.category; + +import android.content.SharedPreferences; +import android.text.TextUtils; + +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Comparator; +import java.util.Date; +import java.util.HashMap; +import java.util.List; + +import javax.inject.Inject; +import javax.inject.Named; + +import fr.free.nrw.commons.mwapi.MediaWikiApi; +import fr.free.nrw.commons.upload.GpsCategoryModel; +import fr.free.nrw.commons.utils.StringSortingUtils; +import io.reactivex.Observable; +import timber.log.Timber; + +public class CategoriesModel implements CategoryClickedListener { + private static final int SEARCH_CATS_LIMIT = 25; + + private final MediaWikiApi mwApi; + private final CategoryDao categoryDao; + private final SharedPreferences prefs; + private final SharedPreferences directPrefs; + + private HashMap> categoriesCache; + private List selectedCategories; + + @Inject GpsCategoryModel gpsCategoryModel; + @Inject + public CategoriesModel(MediaWikiApi mwApi, CategoryDao categoryDao, + @Named("default_preferences") SharedPreferences prefs, + @Named("direct_nearby_upload_prefs") SharedPreferences directPrefs) { + this.mwApi = mwApi; + this.categoryDao = categoryDao; + this.prefs = prefs; + this.directPrefs = directPrefs; + this.categoriesCache = new HashMap<>(); + this.selectedCategories = new ArrayList<>(); + } + + //region Misc. utility methods + public Comparator sortBySimilarity(final String filter) { + Comparator stringSimilarityComparator = StringSortingUtils.sortBySimilarity(filter); + return (firstItem, secondItem) -> stringSimilarityComparator + .compare(firstItem.getName(), secondItem.getName()); + } + + public boolean containsYear(String item) { + //Check for current and previous year to exclude these categories from removal + Calendar now = Calendar.getInstance(); + int year = now.get(Calendar.YEAR); + String yearInString = String.valueOf(year); + + int prevYear = year - 1; + String prevYearInString = String.valueOf(prevYear); + Timber.d("Previous year: %s", prevYearInString); + + //Check if item contains a 4-digit word anywhere within the string (.* is wildcard) + //And that item does not equal the current year or previous year + //And if it is an irrelevant category such as Media_needing_categories_as_of_16_June_2017(Issue #750) + //Check if the year in the form of XX(X)0s is relevant, i.e. in the 2000s or 2010s as stated in Issue #1029 + return ((item.matches(".*(19|20)\\d{2}.*") && !item.contains(yearInString) && !item.contains(prevYearInString)) + || item.matches("(.*)needing(.*)") || item.matches("(.*)taken on(.*)") + || (item.matches(".*0s.*") && !item.matches(".*(200|201)0s.*"))); + } + + public void updateCategoryCount(CategoryItem item) { + Category category = categoryDao.find(item.getName()); + + // Newly used category... + if (category == null) { + category = new Category(null, item.getName(), new Date(), 0); + } + + category.incTimesUsed(); + categoryDao.save(category); + } + //endregion + + //region Category Caching + public void cacheAll(HashMap> categories) { + categoriesCache.putAll(categories); + } + + public HashMap> getCategoriesCache() { + return categoriesCache; + } + + boolean cacheContainsKey(String term) { + return categoriesCache.containsKey(term); + } + //endregion + + //region Category searching + public Observable searchAll(String term) { + //If user hasn't typed anything in yet, get GPS and recent items + if (TextUtils.isEmpty(term)) { + return gpsCategories() + .concatWith(titleCategories()) + .concatWith(recentCategories()); + } + + //if user types in something that is in cache, return cached category + if (cacheContainsKey(term)) { + return Observable.fromIterable(getCachedCategories(term)) + .map(name -> new CategoryItem(name, false)); + } + + //otherwise, search API for matching categories + return mwApi + .allCategories(term, SEARCH_CATS_LIMIT) + .map(name -> new CategoryItem(name, false)); + } + + public Observable searchCategories(String term) { + //If user hasn't typed anything in yet, get GPS and recent items + if (TextUtils.isEmpty(term)) { + return gpsCategories() + .concatWith(titleCategories()) + .concatWith(recentCategories()); + } + + return mwApi + .searchCategories(term, SEARCH_CATS_LIMIT) + .map(s -> new CategoryItem(s, false)); + } + + private ArrayList getCachedCategories(String term) { + return categoriesCache.get(term); + } + + public Observable defaultCategories() { + Observable directCat = directCategories(); + if (hasDirectCategories()) { + Timber.d("Image has direct Cat"); + return directCat + .concatWith(gpsCategories()) + .concatWith(titleCategories()) + .concatWith(recentCategories()); + } else { + Timber.d("Image has no direct Cat"); + return gpsCategories() + .concatWith(titleCategories()) + .concatWith(recentCategories()); + } + } + + private boolean hasDirectCategories() { + return !directPrefs.getString("Category", "").equals(""); + } + + private Observable directCategories() { + String directCategory = directPrefs.getString("Category", ""); + List categoryList = new ArrayList<>(); + Timber.d("Direct category found: " + directCategory); + + if (!directCategory.equals("")) { + categoryList.add(directCategory); + Timber.d("DirectCat does not equal emptyString. Direct Cat list has " + categoryList); + } + return Observable.fromIterable(categoryList).map(name -> new CategoryItem(name, false)); + } + + Observable gpsCategories() { + return Observable.fromIterable(gpsCategoryModel.getCategoryList()) + .map(name -> new CategoryItem(name, false)); + } + + private Observable titleCategories() { + //Retrieve the title that was saved when user tapped submit icon + String title = prefs.getString("Title", ""); + + return mwApi + .searchTitles(title, SEARCH_CATS_LIMIT) + .map(name -> new CategoryItem(name, false)); + } + + private Observable recentCategories() { + return Observable.fromIterable(categoryDao.recentCategories(SEARCH_CATS_LIMIT)) + .map(s -> new CategoryItem(s, false)); + } + //endregion + + //region Category Selection + @Override + public void categoryClicked(CategoryItem item) { + if (item.isSelected()) { + selectCategory(item); + updateCategoryCount(item); + } else { + unselectCategory(item); + } + } + + public void selectCategory(CategoryItem item) { + selectedCategories.add(item); + } + + public void unselectCategory(CategoryItem item) { + selectedCategories.remove(item); + } + + public int selectedCategoriesCount() { + return selectedCategories.size(); + } + + public List getSelectedCategories() { + return selectedCategories; + } + + public List getCategoryStringList() { + List output = new ArrayList<>(); + for (CategoryItem item : selectedCategories) { + output.add(item.getName()); + } + return output; + } + //endregion + +} diff --git a/app/src/main/java/fr/free/nrw/commons/category/CategoriesRenderer.java b/app/src/main/java/fr/free/nrw/commons/category/CategoriesRenderer.java index 81cccdb72d..f9a349ccb0 100644 --- a/app/src/main/java/fr/free/nrw/commons/category/CategoriesRenderer.java +++ b/app/src/main/java/fr/free/nrw/commons/category/CategoriesRenderer.java @@ -1,5 +1,6 @@ package fr.free.nrw.commons.category; +import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -11,7 +12,7 @@ import butterknife.ButterKnife; import fr.free.nrw.commons.R; -class CategoriesRenderer extends Renderer { +public class CategoriesRenderer extends Renderer { @BindView(R.id.tvName) CheckedTextView checkedView; private final CategoryClickedListener listener; @@ -44,11 +45,8 @@ protected void hookListeners(View view) { @Override public void render() { CategoryItem item = getContent(); + Log.e("Commons", "Rendering: "+item); checkedView.setChecked(item.isSelected()); checkedView.setText(item.getName()); } - - interface CategoryClickedListener { - void categoryClicked(CategoryItem item); - } } diff --git a/app/src/main/java/fr/free/nrw/commons/category/CategorizationFragment.java b/app/src/main/java/fr/free/nrw/commons/category/CategorizationFragment.java index 93ddb60d5d..0c1eec97bf 100644 --- a/app/src/main/java/fr/free/nrw/commons/category/CategorizationFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/category/CategorizationFragment.java @@ -1,6 +1,8 @@ package fr.free.nrw.commons.category; +import android.annotation.SuppressLint; +import android.app.Activity; import android.content.SharedPreferences; import android.os.Bundle; import android.support.v7.app.AlertDialog; @@ -73,24 +75,13 @@ public class CategorizationFragment extends CommonsDaggerSupportFragment { @Inject @Named("prefs") SharedPreferences prefsPrefs; @Inject @Named("direct_nearby_upload_prefs") SharedPreferences directPrefs; @Inject CategoryDao categoryDao; - @Inject GpsCategoryModel gpsCategoryModel; + @Inject CategoriesModel categoriesModel; - private RVRendererAdapter categoriesAdapter; + private CategoryRendererAdapter categoriesAdapter; private OnCategoriesSaveHandler onCategoriesSaveHandler; - private HashMap> categoriesCache; - private List selectedCategories = new ArrayList<>(); private TitleTextWatcher textWatcher = new TitleTextWatcher(); private boolean hasDirectCategories = false; - private final CategoriesAdapterFactory adapterFactory = new CategoriesAdapterFactory(item -> { - if (item.isSelected()) { - selectedCategories.add(item); - updateCategoryCount(item); - } else { - selectedCategories.remove(item); - } - }); - @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -100,18 +91,24 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, categoriesList.setLayoutManager(new LinearLayoutManager(getContext())); ArrayList items = new ArrayList<>(); - categoriesCache = new HashMap<>(); if (savedInstanceState != null) { items.addAll(savedInstanceState.getParcelableArrayList("currentCategories")); //noinspection unchecked - categoriesCache.putAll((HashMap>) savedInstanceState + categoriesModel.cacheAll((HashMap>) savedInstanceState .getSerializable("categoriesCache")); } + CategoriesAdapterFactory adapterFactory = new CategoriesAdapterFactory(item -> { + if (item.isSelected()) { + categoriesModel.selectCategory(item); + categoriesModel.updateCategoryCount(item); + } else { + categoriesModel.unselectCategory(item); + } + }); categoriesAdapter = adapterFactory.create(items); categoriesList.setAdapter(categoriesAdapter); - categoriesFilter.addTextChangedListener(textWatcher); categoriesFilter.setOnFocusChangeListener((v, hasFocus) -> { @@ -162,22 +159,17 @@ public void onResume() { @Override public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); - int itemCount = categoriesAdapter.getItemCount(); - ArrayList items = new ArrayList<>(itemCount); - for (int i = 0; i < itemCount; i++) { - items.add(categoriesAdapter.getItem(i)); - } - outState.putParcelableArrayList("currentCategories", items); - outState.putSerializable("categoriesCache", categoriesCache); + outState.putParcelableArrayList("currentCategories", categoriesAdapter.allItems()); + outState.putSerializable("categoriesCache", categoriesModel.getCategoriesCache()); } @Override public boolean onOptionsItemSelected(MenuItem menuItem) { switch (menuItem.getItemId()) { case R.id.menu_save_categories: - if (selectedCategories.size() > 0) { + if (categoriesModel.selectedCategoriesCount() > 0) { //Some categories selected, proceed to submission - onCategoriesSaveHandler.onCategoriesSave(getStringList(selectedCategories)); + onCategoriesSaveHandler.onCategoriesSave(categoriesModel.getCategoryStringList()); } else { //No categories selected, prompt the user to select some showConfirmationDialog(); @@ -196,8 +188,9 @@ public void onActivityCreated(Bundle savedInstanceState) { getActivity().setTitle(R.string.categories_activity_title); } + @SuppressLint({"StringFormatInvalid", "CheckResult"}) private void updateCategoryList(String filter) { - Observable.fromIterable(selectedCategories) + Observable.fromIterable(categoriesModel.getSelectedCategories()) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .doOnSubscribe(disposable -> { @@ -208,14 +201,14 @@ private void updateCategoryList(String filter) { }) .observeOn(Schedulers.io()) .concatWith( - searchAll(filter) - .mergeWith(searchCategories(filter)) + categoriesModel.searchAll(filter) + .mergeWith(categoriesModel.searchCategories(filter)) .concatWith(TextUtils.isEmpty(filter) - ? defaultCategories() : Observable.empty()) + ? categoriesModel.defaultCategories() : Observable.empty()) ) - .filter(categoryItem -> !containsYear(categoryItem.getName())) + .filter(categoryItem -> !categoriesModel.containsYear(categoryItem.getName())) .distinct() - .sorted(sortBySimilarity(filter)) + .sorted(categoriesModel.sortBySimilarity(filter)) .observeOn(AndroidSchedulers.mainThread()) .subscribe( s -> categoriesAdapter.add(s), @@ -224,7 +217,7 @@ private void updateCategoryList(String filter) { categoriesAdapter.notifyDataSetChanged(); categoriesSearchInProgress.setVisibility(View.GONE); - if (categoriesAdapter.getItemCount() == selectedCategories.size()) { + if (categoriesAdapter.getItemCount() == categoriesModel.selectedCategoriesCount()) { // There are no suggestions if (TextUtils.isEmpty(filter)) { // Allow to send image with no categories @@ -258,13 +251,13 @@ private Observable defaultCategories() { if (hasDirectCategories) { Timber.d("Image has direct Cat"); return directCat - .concatWith(gpsCategories()) + .concatWith(categoriesModel.gpsCategories()) .concatWith(titleCategories()) .concatWith(recentCategories()); } else { Timber.d("Image has no direct Cat"); - return gpsCategories() + return categoriesModel.gpsCategories() .concatWith(titleCategories()) .concatWith(recentCategories()); } @@ -286,11 +279,6 @@ private Observable directCategories() { return Observable.fromIterable(categoryList).map(name -> new CategoryItem(name, false)); } - private Observable gpsCategories() { - return Observable.fromIterable(gpsCategoryModel.getCategoryList()) - .map(name -> new CategoryItem(name, false)); - } - private Observable titleCategories() { //Retrieve the title that was saved when user tapped submit icon String title = prefs.getString("Title", ""); @@ -305,70 +293,6 @@ private Observable recentCategories() { .map(s -> new CategoryItem(s, false)); } - private Observable searchAll(String term) { - //If user hasn't typed anything in yet, get GPS and recent items - if (TextUtils.isEmpty(term)) { - return Observable.empty(); - } - - //if user types in something that is in cache, return cached category - if (categoriesCache.containsKey(term)) { - return Observable.fromIterable(categoriesCache.get(term)) - .map(name -> new CategoryItem(name, false)); - } - - //otherwise, search API for matching categories - return mwApi - .allCategories(term, SEARCH_CATS_LIMIT) - .map(name -> new CategoryItem(name, false)); - } - - private Observable searchCategories(String term) { - //If user hasn't typed anything in yet, get GPS and recent items - if (TextUtils.isEmpty(term)) { - return Observable.empty(); - } - - return mwApi - .searchCategories(term, SEARCH_CATS_LIMIT) - .map(s -> new CategoryItem(s, false)); - } - - private boolean containsYear(String item) { - //Check for current and previous year to exclude these categories from removal - Calendar now = Calendar.getInstance(); - int year = now.get(Calendar.YEAR); - String yearInString = String.valueOf(year); - - int prevYear = year - 1; - String prevYearInString = String.valueOf(prevYear); - Timber.d("Previous year: %s", prevYearInString); - - //Check if item contains a 4-digit word anywhere within the string (.* is wildcard) - //And that item does not equal the current year or previous year - //And if it is an irrelevant category such as Media_needing_categories_as_of_16_June_2017(Issue #750) - //Check if the year in the form of XX(X)0s is relevant, i.e. in the 2000s or 2010s as stated in Issue #1029 - return ((item.matches(".*(19|20)\\d{2}.*") && !item.contains(yearInString) && !item.contains(prevYearInString)) - || item.matches("(.*)needing(.*)") || item.matches("(.*)taken on(.*)") - || (item.matches(".*0s.*") && !item.matches(".*(200|201)0s.*"))); - } - - private void updateCategoryCount(CategoryItem item) { - Category category = categoryDao.find(item.getName()); - - // Newly used category... - if (category == null) { - category = new Category(null, item.getName(), new Date(), 0); - } - - category.incTimesUsed(); - categoryDao.save(category); - } - - public int getCurrentSelectedCount() { - return selectedCategories.size(); - } - /** * Show dialog asking for confirmation to leave without saving categories. */ @@ -377,10 +301,10 @@ public void showBackButtonDialog() { .setMessage("Are you sure you want to go back? The image will not " + "have any categories saved.") .setTitle("Warning") - .setPositiveButton("No", (dialog, id) -> { + .setPositiveButton(android.R.string.no, (dialog, id) -> { //No need to do anything, user remains on categorization screen }) - .setNegativeButton("Yes", (dialog, id) -> getActivity().finish()) + .setNegativeButton(android.R.string.yes, (dialog, id) -> getActivity().finish()) .create() .show(); } @@ -391,12 +315,12 @@ private void showConfirmationDialog() { + "Are you sure you want to submit without selecting " + "categories?") .setTitle("No Categories Selected") - .setPositiveButton("No, go back", (dialog, id) -> { + .setPositiveButton(android.R.string.no, (dialog, id) -> { //Exit menuItem so user can select their categories }) - .setNegativeButton("Yes, submit", (dialog, id) -> { + .setNegativeButton(android.R.string.yes, (dialog, id) -> { //Proceed to submission - onCategoriesSaveHandler.onCategoriesSave(getStringList(selectedCategories)); + onCategoriesSaveHandler.onCategoriesSave(categoriesModel.getCategoryStringList()); }) .create() .show(); diff --git a/app/src/main/java/fr/free/nrw/commons/category/CategoryClickedListener.java b/app/src/main/java/fr/free/nrw/commons/category/CategoryClickedListener.java new file mode 100644 index 0000000000..df99b40603 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/category/CategoryClickedListener.java @@ -0,0 +1,5 @@ +package fr.free.nrw.commons.category; + +public interface CategoryClickedListener { + void categoryClicked(CategoryItem item); +} diff --git a/app/src/main/java/fr/free/nrw/commons/category/CategoryDetailsActivity.java b/app/src/main/java/fr/free/nrw/commons/category/CategoryDetailsActivity.java index 3ab3c2c072..fce2fac5ef 100644 --- a/app/src/main/java/fr/free/nrw/commons/category/CategoryDetailsActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/category/CategoryDetailsActivity.java @@ -250,4 +250,24 @@ public void onBackPressed() { } super.onBackPressed(); } + + /** + * This method is called on success of API call for Images inside a category. + * The viewpager will notified that number of items have changed. + */ + public void viewPagerNotifyDataSetChanged() { + if (mediaDetails!=null){ + mediaDetails.notifyDataSetChanged(); + } + } + + /** + * This method is called when viewPager has reached its end. + * Fetches more images using search query and adds it to the grid view and viewpager adapter + */ + public void requestMoreImages() { + if (categoryImagesListFragment!=null){ + categoryImagesListFragment.fetchMoreImagesViewPager(); + } + } } diff --git a/app/src/main/java/fr/free/nrw/commons/category/CategoryImagesActivity.java b/app/src/main/java/fr/free/nrw/commons/category/CategoryImagesActivity.java index 3821344724..eafc46e682 100644 --- a/app/src/main/java/fr/free/nrw/commons/category/CategoryImagesActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/category/CategoryImagesActivity.java @@ -170,6 +170,16 @@ public Media getMediaAtPosition(int i) { } } + /** + * This method is called on success of API call for featured Images. + * The viewpager will notified that number of items have changed. + */ + public void viewPagerNotifyDataSetChanged() { + if (mediaDetails!=null){ + mediaDetails.notifyDataSetChanged(); + } + } + /** * This method is called on from getCount of MediaDetailPagerFragment * The viewpager will contain same number of media items as that of media elements in adapter. @@ -236,4 +246,14 @@ public boolean onOptionsItemSelected(MenuItem item) { return super.onOptionsItemSelected(item); } } + + /** + * This method is called when viewPager has reached its end. + * Fetches more images using search query and adds it to the gridView and viewpager adapter + */ + public void requestMoreImages() { + if (categoryImagesListFragment!=null){ + categoryImagesListFragment.fetchMoreImagesViewPager(); + } + } } diff --git a/app/src/main/java/fr/free/nrw/commons/category/CategoryImagesListFragment.java b/app/src/main/java/fr/free/nrw/commons/category/CategoryImagesListFragment.java index 385662a054..a78157ee25 100644 --- a/app/src/main/java/fr/free/nrw/commons/category/CategoryImagesListFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/category/CategoryImagesListFragment.java @@ -190,6 +190,20 @@ public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCoun }); } + /** + * This method is called when viewPager has reached its end. + * Fetches more images for the category and adds it to the grid view and viewpager adapter + */ + public void fetchMoreImagesViewPager(){ + if (hasMoreImages && !isLoading) { + isLoading = true; + fetchMoreImages(); + } + if (!hasMoreImages){ + progressBar.setVisibility(GONE); + } + } + /** * Fetches more images for the category and adds it to the grid view adapter */ @@ -228,8 +242,17 @@ private void handleSuccess(List collection) { return; } gridAdapter.addItems(collection); + try { + ((CategoryImagesActivity) getContext()).viewPagerNotifyDataSetChanged(); + }catch (Exception e){ + e.printStackTrace(); + } + try { + ((CategoryDetailsActivity) getContext()).viewPagerNotifyDataSetChanged(); + }catch (Exception e){ + e.printStackTrace(); + } } - progressBar.setVisibility(GONE); isLoading = false; statusTextView.setVisibility(GONE); diff --git a/app/src/main/java/fr/free/nrw/commons/category/CategoryItem.java b/app/src/main/java/fr/free/nrw/commons/category/CategoryItem.java index f6bacfb51a..f3ade09d8b 100644 --- a/app/src/main/java/fr/free/nrw/commons/category/CategoryItem.java +++ b/app/src/main/java/fr/free/nrw/commons/category/CategoryItem.java @@ -3,7 +3,7 @@ import android.os.Parcel; import android.os.Parcelable; -class CategoryItem implements Parcelable { +public class CategoryItem implements Parcelable { private final String name; private boolean selected; @@ -71,4 +71,9 @@ public boolean equals(Object o) { public int hashCode() { return name.hashCode(); } + + @Override + public String toString() { + return "CategoryItem: '" + name + '\''; + } } diff --git a/app/src/main/java/fr/free/nrw/commons/category/CategoryRendererAdapter.java b/app/src/main/java/fr/free/nrw/commons/category/CategoryRendererAdapter.java new file mode 100644 index 0000000000..887ad595fc --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/category/CategoryRendererAdapter.java @@ -0,0 +1,22 @@ +package fr.free.nrw.commons.category; + +import com.pedrogomez.renderers.AdapteeCollection; +import com.pedrogomez.renderers.RVRendererAdapter; +import com.pedrogomez.renderers.RendererBuilder; + +import java.util.ArrayList; + +public class CategoryRendererAdapter extends RVRendererAdapter { + CategoryRendererAdapter(RendererBuilder rendererBuilder, AdapteeCollection collection) { + super(rendererBuilder, collection); + } + + protected ArrayList allItems() { + int itemCount = getItemCount(); + ArrayList items = new ArrayList<>(itemCount); + for (int i = 0; i < itemCount; i++) { + items.add(getItem(i)); + } + return items; + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/Contribution.java b/app/src/main/java/fr/free/nrw/commons/contributions/Contribution.java index 9f5084e6a5..98e9665071 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/Contribution.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/Contribution.java @@ -2,8 +2,11 @@ import android.net.Uri; import android.os.Parcel; +import android.support.annotation.IntDef; import android.support.annotation.NonNull; +import android.support.annotation.StringDef; +import java.lang.annotation.Retention; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Locale; @@ -13,6 +16,8 @@ import fr.free.nrw.commons.Media; import fr.free.nrw.commons.settings.Prefs; +import static java.lang.annotation.RetentionPolicy.SOURCE; + public class Contribution extends Media { public static Creator CREATOR = new Creator() { @@ -33,6 +38,10 @@ public Contribution[] newArray(int i) { public static final int STATE_QUEUED = 2; public static final int STATE_IN_PROGRESS = 3; + @Retention(SOURCE) + @StringDef({SOURCE_CAMERA, SOURCE_GALLERY, SOURCE_EXTERNAL}) + public @interface FileSource {} + public static final String SOURCE_CAMERA = "camera"; public static final String SOURCE_GALLERY = "gallery"; public static final String SOURCE_EXTERNAL = "external"; @@ -141,10 +150,6 @@ public void setDateUploaded(Date date) { this.dateUploaded = date; } - public String getTrackingTemplates() { - return "{{subst:unc}}"; // Remove when we have categorization - } - public String getPageContents() { StringBuilder buffer = new StringBuilder(); SimpleDateFormat isoFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH); @@ -169,8 +174,15 @@ public String getPageContents() { buffer.append("== {{int:license-header}} ==\n") .append(licenseTemplateFor(getLicense())).append("\n\n") - .append("{{Uploaded from Mobile|platform=Android|version=").append(BuildConfig.VERSION_NAME).append("}}\n") - .append(getTrackingTemplates()); + .append("{{Uploaded from Mobile|platform=Android|version=").append(BuildConfig.VERSION_NAME).append("}}\n"); + if(categories!=null&&categories.size()!=0) { + for (int i = 0; i < categories.size(); i++) { + String category = categories.get(i); + buffer.append("\n[[Category:").append(category).append("]]"); + } + } + else + buffer.append("{{subst:unc}}"); return buffer.toString(); } @@ -232,7 +244,7 @@ public String getWikiDataEntityId() { /** * When the corresponding wikidata entity is known as in case of nearby uploads, it can be set * using the setter method - * @param wikiDataEntityId + * @param wikiDataEntityId wikiDataEntityId */ public void setWikiDataEntityId(String wikiDataEntityId) { this.wikiDataEntityId = wikiDataEntityId; diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionController.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionController.java index f57c8e239a..c282c7258f 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionController.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionController.java @@ -17,7 +17,7 @@ import java.util.Date; import java.util.List; -import fr.free.nrw.commons.upload.ShareActivity; +import fr.free.nrw.commons.upload.UploadActivity; import timber.log.Timber; import static android.content.Intent.ACTION_GET_CONTENT; @@ -95,8 +95,8 @@ public void startGalleryPick() { public void handleImagePicked(int requestCode, @Nullable Uri uri, boolean isDirectUpload, String wikiDataEntityId) { FragmentActivity activity = fragment.getActivity(); - Timber.d("handleImagePicked() called with onActivityResult()"); - Intent shareIntent = new Intent(activity, ShareActivity.class); + Timber.d("handleImagePicked() called with onActivityResult(). Boolean isDirectUpload: " + isDirectUpload + "String wikiDataEntityId: " + wikiDataEntityId); + Intent shareIntent = new Intent(activity, UploadActivity.class); shareIntent.setAction(ACTION_SEND); switch (requestCode) { case SELECT_FROM_GALLERY: @@ -113,21 +113,26 @@ public void handleImagePicked(int requestCode, @Nullable Uri uri, boolean isDire shareIntent.setType("image/jpeg"); shareIntent.putExtra(EXTRA_STREAM, lastGeneratedCaptureUri); shareIntent.putExtra(EXTRA_SOURCE, SOURCE_CAMERA); - break; default: break; } + Timber.i("Image selected"); + shareIntent.putExtra("isDirectUpload", isDirectUpload); + Timber.d("Put extras into image intent, isDirectUpload is " + isDirectUpload); + try { - shareIntent.putExtra("isDirectUpload", isDirectUpload); if (wikiDataEntityId != null && !wikiDataEntityId.equals("")) { shareIntent.putExtra(WIKIDATA_ENTITY_ID_PREF, wikiDataEntityId); } - activity.startActivity(shareIntent); } catch (SecurityException e) { Timber.e(e, "Security Exception"); } + + if (activity != null) { + activity.startActivity(shareIntent); + } } void saveState(Bundle outState) { diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListFragment.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListFragment.java index 6abb3ce43e..e366a14681 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListFragment.java @@ -3,13 +3,11 @@ import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageManager; -import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.v4.content.ContextCompat; import android.support.v7.app.AlertDialog; -import android.util.Log; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; @@ -33,7 +31,6 @@ import fr.free.nrw.commons.R; import fr.free.nrw.commons.di.CommonsDaggerSupportFragment; import fr.free.nrw.commons.nearby.NearbyActivity; -import fr.free.nrw.commons.utils.ContributionUtils; import timber.log.Timber; import static android.Manifest.permission.READ_EXTERNAL_STORAGE; @@ -154,11 +151,11 @@ public boolean onOptionsItemSelected(MenuItem item) { new AlertDialog.Builder(getActivity()) .setMessage(getString(R.string.read_storage_permission_rationale)) - .setPositiveButton("OK", (dialog, which) -> { + .setPositiveButton(android.R.string.ok, (dialog, which) -> { requestPermissions(new String[]{READ_EXTERNAL_STORAGE}, 1); dialog.dismiss(); }) - .setNegativeButton("Cancel", null) + .setNegativeButton(android.R.string.cancel, null) .create() .show(); @@ -196,11 +193,11 @@ public boolean onOptionsItemSelected(MenuItem item) { // sees the explanation, try again to request the permission. new AlertDialog.Builder(getActivity()) .setMessage(getString(R.string.write_storage_permission_rationale)) - .setPositiveButton("OK", (dialog, which) -> { + .setPositiveButton(android.R.string.ok, (dialog, which) -> { requestPermissions(new String[]{WRITE_EXTERNAL_STORAGE}, 3); dialog.dismiss(); }) - .setNegativeButton("Cancel", null) + .setNegativeButton(android.R.string.cancel, null) .create() .show(); } else { diff --git a/app/src/main/java/fr/free/nrw/commons/delete/DeleteTask.java b/app/src/main/java/fr/free/nrw/commons/delete/DeleteTask.java index 37b9a7a828..f35a6c38e2 100644 --- a/app/src/main/java/fr/free/nrw/commons/delete/DeleteTask.java +++ b/app/src/main/java/fr/free/nrw/commons/delete/DeleteTask.java @@ -14,6 +14,7 @@ import javax.inject.Inject; +import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.Media; import fr.free.nrw.commons.R; import fr.free.nrw.commons.auth.SessionManager; @@ -44,14 +45,17 @@ public DeleteTask (Context context, Media media, String reason){ } @Override - protected void onPreExecute(){ + protected void onPreExecute() { ApplicationlessInjection .getInstance(context.getApplicationContext()) .getCommonsApplicationComponent() .inject(this); - notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); - notificationBuilder = new NotificationCompat.Builder(context); + notificationManager = + (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + notificationBuilder = new NotificationCompat.Builder( + context, + CommonsApplication.NOTIFICATION_CHANNEL_ID_ALL); Toast toast = new Toast(context); toast.setGravity(Gravity.CENTER,0,0); toast = Toast.makeText(context,"Trying to nominate "+media.getDisplayTitle()+ " for deletion",Toast.LENGTH_SHORT); diff --git a/app/src/main/java/fr/free/nrw/commons/di/ActivityBuilderModule.java b/app/src/main/java/fr/free/nrw/commons/di/ActivityBuilderModule.java index 70ffec55f1..6c9b38fe88 100644 --- a/app/src/main/java/fr/free/nrw/commons/di/ActivityBuilderModule.java +++ b/app/src/main/java/fr/free/nrw/commons/di/ActivityBuilderModule.java @@ -15,8 +15,7 @@ import fr.free.nrw.commons.nearby.NearbyActivity; import fr.free.nrw.commons.notification.NotificationActivity; import fr.free.nrw.commons.settings.SettingsActivity; -import fr.free.nrw.commons.upload.MultipleShareActivity; -import fr.free.nrw.commons.upload.ShareActivity; +import fr.free.nrw.commons.upload.UploadActivity; @Module @SuppressWarnings({"WeakerAccess", "unused"}) @@ -28,11 +27,11 @@ public abstract class ActivityBuilderModule { @ContributesAndroidInjector abstract WelcomeActivity bindWelcomeActivity(); - @ContributesAndroidInjector - abstract ShareActivity bindShareActivity(); + //@ContributesAndroidInjector + //abstract ShareActivity bindShareActivity(); - @ContributesAndroidInjector - abstract MultipleShareActivity bindMultipleShareActivity(); + //@ContributesAndroidInjector + //abstract MultipleShareActivity bindMultipleShareActivity(); @ContributesAndroidInjector abstract ContributionsActivity bindContributionsActivity(); @@ -64,4 +63,6 @@ public abstract class ActivityBuilderModule { @ContributesAndroidInjector abstract AchievementsActivity bindAchievementsActivity(); + @ContributesAndroidInjector + abstract UploadActivity bindUploadActivity(); } diff --git a/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.java b/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.java index cf96193601..15cd91b62b 100644 --- a/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.java +++ b/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.java @@ -1,30 +1,41 @@ package fr.free.nrw.commons.di; +import android.app.Activity; import android.content.ContentProviderClient; import android.content.Context; import android.content.SharedPreferences; import android.preference.PreferenceManager; import android.support.v4.util.LruCache; +import android.view.inputmethod.InputMethodManager; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.google.gson.Gson; 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.R; 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.settings.Prefs; 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.explore.recentsearches.RecentSearchesContentProvider.RECENT_SEARCH_AUTHORITY; @Module @SuppressWarnings({"WeakerAccess", "unused"}) @@ -40,6 +51,35 @@ public Context providesApplicationContext() { return this.applicationContext; } + @Provides + public InputMethodManager provideInputMethodManager() { + return (InputMethodManager) applicationContext.getSystemService(Activity.INPUT_METHOD_SERVICE); + } + + @Provides + @Named("licenses") + public List provideLicenses(Context context) { + List licenseItems = new ArrayList<>(); + licenseItems.add(context.getString(R.string.license_name_cc0)); + licenseItems.add(context.getString(R.string.license_name_cc_by)); + licenseItems.add(context.getString(R.string.license_name_cc_by_sa)); + licenseItems.add(context.getString(R.string.license_name_cc_by_four)); + licenseItems.add(context.getString(R.string.license_name_cc_by_sa_four)); + return licenseItems; + } + + @Provides + @Named("licenses_by_name") + public Map provideLicensesByName(Context context) { + Map byName = new HashMap<>(); + byName.put(context.getString(R.string.license_name_cc0), Prefs.Licenses.CC0); + byName.put(context.getString(R.string.license_name_cc_by), Prefs.Licenses.CC_BY_3); + byName.put(context.getString(R.string.license_name_cc_by_sa), Prefs.Licenses.CC_BY_SA_3); + byName.put(context.getString(R.string.license_name_cc_by_four), Prefs.Licenses.CC_BY_4); + byName.put(context.getString(R.string.license_name_cc_by_sa_four), Prefs.Licenses.CC_BY_SA_4); + return byName; + } + @Provides public AccountUtil providesAccountUtil(Context context) { return new AccountUtil(context); @@ -60,7 +100,7 @@ public ContentProviderClient provideCategoryContentProviderClient(Context contex @Provides @Named("recentsearch") public ContentProviderClient provideRecentSearchContentProviderClient(Context context) { - return context.getContentResolver().acquireContentProviderClient(RECENT_SEARCH_AUTHORITY); + return context.getContentResolver().acquireContentProviderClient(BuildConfig.RECENT_SEARCH_AUTHORITY); } @Provides @@ -123,12 +163,37 @@ public SessionManager providesSessionManager(Context context, return new SessionManager(context, mediaWikiApi, sharedPreferences); } +// @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, defaultPreferences, categoryPrefs, gson); +// } + @Provides @Singleton public LocationServiceManager provideLocationServiceManager(Context context) { return new LocationServiceManager(context); } + /* + * 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 Gson(); +// } +// +// @Provides +// @Singleton +// public CacheController provideCacheController() { +// return new CacheController(); +// } + @Provides @Singleton public DBOpenHelper provideDBOpenHelper(Context context) { diff --git a/app/src/main/java/fr/free/nrw/commons/di/FragmentBuilderModule.java b/app/src/main/java/fr/free/nrw/commons/di/FragmentBuilderModule.java index b14d8feeff..18900a06aa 100644 --- a/app/src/main/java/fr/free/nrw/commons/di/FragmentBuilderModule.java +++ b/app/src/main/java/fr/free/nrw/commons/di/FragmentBuilderModule.java @@ -15,8 +15,6 @@ import fr.free.nrw.commons.nearby.NearbyMapFragment; import fr.free.nrw.commons.nearby.NoPermissionsFragment; import fr.free.nrw.commons.settings.SettingsFragment; -import fr.free.nrw.commons.upload.MultipleUploadListFragment; -import fr.free.nrw.commons.upload.SingleUploadFragment; @Module @SuppressWarnings({"WeakerAccess", "unused"}) @@ -46,12 +44,6 @@ public abstract class FragmentBuilderModule { @ContributesAndroidInjector abstract SettingsFragment bindSettingsFragment(); - @ContributesAndroidInjector - abstract MultipleUploadListFragment bindMultipleUploadListFragment(); - - @ContributesAndroidInjector - abstract SingleUploadFragment bindSingleUploadFragment(); - @ContributesAndroidInjector abstract CategoryImagesListFragment bindFeaturedImagesListFragment(); diff --git a/app/src/main/java/fr/free/nrw/commons/explore/SearchActivity.java b/app/src/main/java/fr/free/nrw/commons/explore/SearchActivity.java index 475b3900a7..38d78fb34e 100644 --- a/app/src/main/java/fr/free/nrw/commons/explore/SearchActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/explore/SearchActivity.java @@ -53,6 +53,7 @@ public class SearchActivity extends NavigationBaseActivity implements MediaDetai private FragmentManager supportFragmentManager; private MediaDetailPagerFragment mediaDetails; ViewPagerAdapter viewPagerAdapter; + private String query; @Override protected void onCreate(Bundle savedInstanceState) { @@ -104,6 +105,7 @@ public void setTabs() { .debounce(500, TimeUnit.MILLISECONDS) .observeOn(AndroidSchedulers.mainThread()) .subscribe( query -> { + this.query = query.toString(); //update image list if (!TextUtils.isEmpty(query)) { viewPager.setVisibility(View.VISIBLE); @@ -145,7 +147,16 @@ public int getTotalMediaCount() { */ @Override public void notifyDatasetChanged() { + } + /** + * This method is called on success of API call for image Search. + * The viewpager will notified that number of items have changed. + */ + public void viewPagerNotifyDataSetChanged() { + if (mediaDetails!=null){ + mediaDetails.notifyDataSetChanged(); + } } /** @@ -245,4 +256,14 @@ public void updateText(String query) { // https://stackoverflow.com/questions/6117967/how-to-remove-focus-without-setting-focus-to-another-control/15481511 viewPager.requestFocus(); } + + /** + * This method is called when viewPager has reached its end. + * Fetches more images using search query and adds it to the recycler view and viewpager adapter + */ + public void requestMoreImages() { + if (searchImageFragment!=null){ + searchImageFragment.addImagesToList(query); + } + } } diff --git a/app/src/main/java/fr/free/nrw/commons/explore/images/SearchImageFragment.java b/app/src/main/java/fr/free/nrw/commons/explore/images/SearchImageFragment.java index a503207e4d..1770516201 100644 --- a/app/src/main/java/fr/free/nrw/commons/explore/images/SearchImageFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/explore/images/SearchImageFragment.java @@ -1,6 +1,7 @@ package fr.free.nrw.commons.explore.images; +import android.annotation.SuppressLint; import android.content.SharedPreferences; import android.content.res.Configuration; import android.os.Bundle; @@ -13,6 +14,8 @@ import android.view.ViewGroup; import android.widget.ProgressBar; import android.widget.TextView; +import android.widget.Toast; + import com.pedrogomez.renderers.RVRendererAdapter; import java.util.ArrayList; import java.util.Date; @@ -119,6 +122,7 @@ public void onScrollStateChanged(RecyclerView recyclerView, int newState) { * Checks for internet connection and then initializes the recycler view with 25 images of the searched query * Clearing imageAdapter every time new keyword is searched so that user can see only new results */ + @SuppressLint("CheckResult") public void updateImageList(String query) { this.query = query; imagesNotFoundView.setVisibility(GONE); @@ -140,6 +144,7 @@ public void updateImageList(String query) { /** * Adds more results to existing search results */ + @SuppressLint("CheckResult") public void addImagesToList(String query) { this.query = query; progressBar.setVisibility(View.VISIBLE); @@ -156,10 +161,13 @@ public void addImagesToList(String query) { * @param mediaList List of media to be added */ private void handlePaginationSuccess(List mediaList) { - queryList.addAll(mediaList); progressBar.setVisibility(View.GONE); - imagesAdapter.addAll(mediaList); - imagesAdapter.notifyDataSetChanged(); + if (mediaList.size() != 0 || !queryList.get(queryList.size() - 1).getFilename().equals(mediaList.get(mediaList.size() - 1).getFilename())) { + queryList.addAll(mediaList); + imagesAdapter.addAll(mediaList); + imagesAdapter.notifyDataSetChanged(); + ((SearchActivity) getContext()).viewPagerNotifyDataSetChanged(); + } } @@ -179,6 +187,7 @@ private void handleSuccess(List mediaList) { progressBar.setVisibility(View.GONE); imagesAdapter.addAll(mediaList); imagesAdapter.notifyDataSetChanged(); + ((SearchActivity)getContext()).viewPagerNotifyDataSetChanged(); // check if user is waiting for 5 seconds if yes then save search query to history. Handler handler = new Handler(); @@ -193,7 +202,6 @@ private void handleSuccess(List mediaList) { private void handleError(Throwable throwable) { Timber.e(throwable, "Error occurred while loading queried images"); try { - initErrorView(); ViewUtil.showSnackbar(imagesRecyclerView, R.string.error_loading_images); }catch (Exception e){ e.printStackTrace(); @@ -239,7 +247,7 @@ public Media getImageAtPosition(int i) { return null; } else { - return new Media(imagesAdapter.getItem(i).getFilename()); + return imagesAdapter.getItem(i); } } } diff --git a/app/src/main/java/fr/free/nrw/commons/explore/images/SearchImagesRenderer.java b/app/src/main/java/fr/free/nrw/commons/explore/images/SearchImagesRenderer.java index 42c044d705..022e8307e2 100644 --- a/app/src/main/java/fr/free/nrw/commons/explore/images/SearchImagesRenderer.java +++ b/app/src/main/java/fr/free/nrw/commons/explore/images/SearchImagesRenderer.java @@ -65,11 +65,11 @@ interface ImageClickedListener { */ private void setAuthorView(Media item, TextView author) { if (item.getCreator() != null && !item.getCreator().equals("")) { - author.setVisibility(View.GONE); + author.setVisibility(View.VISIBLE); String uploadedByTemplate = getContext().getString(R.string.image_uploaded_by); author.setText(String.format(uploadedByTemplate, item.getCreator())); } else { - author.setVisibility(View.VISIBLE); + author.setVisibility(View.GONE); } } } diff --git a/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesContentProvider.java b/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesContentProvider.java index bf3cf959ae..ed7821885e 100644 --- a/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesContentProvider.java +++ b/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesContentProvider.java @@ -11,6 +11,7 @@ import javax.inject.Inject; +import fr.free.nrw.commons.BuildConfig; import fr.free.nrw.commons.contributions.ContributionDao; import fr.free.nrw.commons.data.DBOpenHelper; import fr.free.nrw.commons.di.CommonsDaggerContentProvider; @@ -28,17 +29,16 @@ **/ public class RecentSearchesContentProvider extends CommonsDaggerContentProvider { - public static final String RECENT_SEARCH_AUTHORITY = "fr.free.nrw.commons.explore.recentsearches.contentprovider"; // For URI matcher private static final int RECENT_SEARCHES = 1; private static final int RECENT_SEARCHES_ID = 2; private static final String BASE_PATH = "recent_searches"; - public static final Uri BASE_URI = Uri.parse("content://" + RECENT_SEARCH_AUTHORITY + "/" + BASE_PATH); + public static final Uri BASE_URI = Uri.parse("content://" + BuildConfig.RECENT_SEARCH_AUTHORITY + "/" + BASE_PATH); private static final UriMatcher uriMatcher = new UriMatcher(NO_MATCH); static { - uriMatcher.addURI(RECENT_SEARCH_AUTHORITY, BASE_PATH, RECENT_SEARCHES); - uriMatcher.addURI(RECENT_SEARCH_AUTHORITY, BASE_PATH + "/#", RECENT_SEARCHES_ID); + uriMatcher.addURI(BuildConfig.RECENT_SEARCH_AUTHORITY, BASE_PATH, RECENT_SEARCHES); + uriMatcher.addURI(BuildConfig.RECENT_SEARCH_AUTHORITY, BASE_PATH + "/#", RECENT_SEARCHES_ID); } public static Uri uriForId(int id) { diff --git a/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesFragment.java b/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesFragment.java index 5c109fbb4e..7c0a6fcca0 100644 --- a/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesFragment.java @@ -41,7 +41,7 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, recent_searches_delete_button.setOnClickListener(v -> { new AlertDialog.Builder(getContext()) .setMessage(getString(R.string.delete_recent_searches_dialog)) - .setPositiveButton("YES", (dialog, which) -> { + .setPositiveButton(android.R.string.yes, (dialog, which) -> { recentSearchesDao.deleteAll(recentSearches); Toast.makeText(getContext(),getString(R.string.search_history_deleted),Toast.LENGTH_SHORT).show(); recentSearches = recentSearchesDao.recentSearches(10); @@ -50,7 +50,7 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, adapter.notifyDataSetChanged(); dialog.dismiss(); }) - .setNegativeButton("NO", null) + .setNegativeButton(android.R.string.no, null) .create() .show(); }); diff --git a/app/src/main/java/fr/free/nrw/commons/glide/SvgDecoder.java b/app/src/main/java/fr/free/nrw/commons/glide/SvgDecoder.java deleted file mode 100644 index 9087f95014..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/glide/SvgDecoder.java +++ /dev/null @@ -1,36 +0,0 @@ -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 { - - @Override - public boolean handles(@NonNull InputStream source, @NonNull Options options) { - // TODO: Can we tell? - return true; - } - - public Resource 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); - } - } -} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/glide/SvgDrawableTranscoder.java b/app/src/main/java/fr/free/nrw/commons/glide/SvgDrawableTranscoder.java deleted file mode 100644 index 89910c8fbc..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/glide/SvgDrawableTranscoder.java +++ /dev/null @@ -1,28 +0,0 @@ -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 { - @Nullable - @Override - public Resource transcode(@NonNull Resource toTranscode, - @NonNull Options options) { - SVG svg = toTranscode.get(); - Picture picture = svg.renderToPicture(); - PictureDrawable drawable = new PictureDrawable(picture); - return new SimpleResource<>(drawable); - } -} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/glide/SvgSoftwareLayerSetter.java b/app/src/main/java/fr/free/nrw/commons/glide/SvgSoftwareLayerSetter.java deleted file mode 100644 index 66a3bd6bfb..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/glide/SvgSoftwareLayerSetter.java +++ /dev/null @@ -1,51 +0,0 @@ -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 { - - /** - * 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 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 target, DataSource dataSource, boolean isFirstResource) { - ImageView view = ((ImageViewTarget) target).getView(); - view.setLayerType(ImageView.LAYER_TYPE_SOFTWARE, null); - return false; - } -} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/location/LocationServiceManager.java b/app/src/main/java/fr/free/nrw/commons/location/LocationServiceManager.java index cd1082ba5d..4a137beed3 100644 --- a/app/src/main/java/fr/free/nrw/commons/location/LocationServiceManager.java +++ b/app/src/main/java/fr/free/nrw/commons/location/LocationServiceManager.java @@ -106,6 +106,9 @@ public LatLng getLKL() { if (lastKL == null) { lastKL = locationManager.getLastKnownLocation(LocationManager.NETWORK_PROVIDER); } + if (lastKL == null) { + return null; + } return LatLng.from(lastKL); } else { return null; diff --git a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java index 30551e24f7..b46789ba07 100644 --- a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java @@ -1,6 +1,8 @@ package fr.free.nrw.commons.media; import android.app.AlertDialog; +import android.content.ClipData; +import android.content.ClipboardManager; import android.content.DialogInterface; import android.content.Intent; import android.database.DataSetObserver; @@ -9,6 +11,7 @@ import android.os.Bundle; import android.support.annotation.Nullable; import android.text.Editable; +import android.text.Html; import android.text.TextUtils; import android.text.TextWatcher; import android.util.TypedValue; @@ -50,6 +53,7 @@ import fr.free.nrw.commons.ui.widget.CompatTextView; import timber.log.Timber; +import static android.content.Context.CLIPBOARD_SERVICE; import static android.view.View.GONE; import static android.view.View.VISIBLE; import static android.widget.Toast.LENGTH_SHORT; @@ -162,6 +166,7 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle sa final View view = inflater.inflate(R.layout.fragment_media_detail, container, false); ButterKnife.bind(this,view); + seeMore.setText(Html.fromHtml(getString(R.string.nominated_see_more))); if (isCategoryImage){ authorLayout.setVisibility(VISIBLE); @@ -351,6 +356,17 @@ public void onMediaDetailCoordinatesClicked(){ } } + @OnClick(R.id.copyWikicode) + public void onCopyWikicodeClicked(){ + String data = "[[" + media.getFilename() + "|thumb|" + media.getDescription() + "]]"; + ClipboardManager clipboard = (ClipboardManager) getContext().getApplicationContext().getSystemService(CLIPBOARD_SERVICE); + clipboard.setPrimaryClip(ClipData.newPlainText("wikiCode", data)); + + Timber.d("Generated wikidata copy code: %s", data); + + Toast.makeText(getContext(), getString(R.string.wikicode_copied), Toast.LENGTH_SHORT).show(); + } + @OnClick(R.id.nominateDeletion) public void onDeleteButtonClicked(){ //Reviewer correct me if i have misunderstood something over here diff --git a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailPagerFragment.java b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailPagerFragment.java index fc58ae9900..b51e859038 100644 --- a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailPagerFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailPagerFragment.java @@ -35,9 +35,12 @@ import fr.free.nrw.commons.Media; import fr.free.nrw.commons.R; import fr.free.nrw.commons.auth.SessionManager; +import fr.free.nrw.commons.category.CategoryDetailsActivity; +import fr.free.nrw.commons.category.CategoryImagesActivity; 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.explore.SearchActivity; import fr.free.nrw.commons.mwapi.MediaWikiApi; import fr.free.nrw.commons.utils.ImageUtils; import timber.log.Timber; @@ -62,6 +65,7 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple ViewPager pager; private Boolean editable; private boolean isFeaturedImage; + MediaDetailAdapter adapter; public MediaDetailPagerFragment() { this(false, false); @@ -81,7 +85,7 @@ public View onCreateView(LayoutInflater inflater, ButterKnife.bind(this,view); pager.addOnPageChangeListener(this); - final MediaDetailAdapter adapter = new MediaDetailAdapter(getChildFragmentManager()); + adapter = new MediaDetailAdapter(getChildFragmentManager()); if (savedInstanceState != null) { final int pageNumber = savedInstanceState.getInt("current-page"); @@ -90,6 +94,12 @@ public View onCreateView(LayoutInflater inflater, view.postDelayed(() -> { pager.setAdapter(adapter); pager.setCurrentItem(pageNumber, false); + + if(getActivity() == null) { + Timber.d("Returning as activity is destroyed!"); + return; + } + getActivity().supportInvalidateOptionsMenu(); adapter.notifyDataSetChanged(); }, 100); @@ -119,6 +129,10 @@ public void onCreate(Bundle savedInstanceState) { @Override public boolean onOptionsItemSelected(MenuItem item) { + if(getActivity() == null) { + Timber.d("Returning as activity is destroyed!"); + return true; + } MediaDetailProvider provider = (MediaDetailProvider) getActivity(); Media m = provider.getMediaAtPosition(pager.getCurrentItem()); switch (item.getItemId()) { @@ -185,10 +199,13 @@ private void setWallpaper(Media media) { * @param m Media file to download */ private void downloadMedia(Media m) { - String imageUrl = m.getImageUrl(), - fileName = m.getFilename(); + String imageUrl = m.getImageUrl(), fileName = m.getFilename(); - if (imageUrl == null || fileName == null) { + if (imageUrl == null + || fileName == null + || getContext() == null + || getActivity() == null) { + Timber.d("Skipping download media as either imageUrl %s or filename %s activity is null", imageUrl, fileName); return; } @@ -230,6 +247,10 @@ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { inflater.inflate(R.menu.fragment_image_detail, menu); if (pager != null) { MediaDetailProvider provider = (MediaDetailProvider) getActivity(); + if(provider == null) { + return; + } + Media m = provider.getMediaAtPosition(pager.getCurrentItem()); if (m != null) { // Enable default set of actions, then re-enable different set of actions only if it is a failed contrib @@ -269,11 +290,39 @@ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { public void showImage(int i) { Handler handler = new Handler(); - handler.postDelayed(() -> pager.setCurrentItem(i), 10); + handler.postDelayed(() -> pager.setCurrentItem(i), 5); + } + + /** + * The method notify the viewpager that number of items have changed. + */ + public void notifyDataSetChanged(){ + adapter.notifyDataSetChanged(); } @Override public void onPageScrolled(int i, float v, int i2) { + if(getActivity() == null) { + Timber.d("Returning as activity is destroyed!"); + return; + } + if (i+1 >= adapter.getCount()){ + try{ + ((CategoryImagesActivity) getContext()).requestMoreImages(); + }catch (Exception e){ + e.printStackTrace(); + } + try{ + ((CategoryDetailsActivity) getContext()).requestMoreImages(); + }catch (Exception e){ + e.printStackTrace(); + } + try{ + ((SearchActivity) getContext()).requestMoreImages(); + }catch (Exception e){ + e.printStackTrace(); + } + } getActivity().supportInvalidateOptionsMenu(); } @@ -308,6 +357,10 @@ public MediaDetailAdapter(FragmentManager fm) { public Fragment getItem(int i) { if (i == 0) { // See bug https://code.google.com/p/android/issues/detail?id=27526 + if(getActivity() == null) { + Timber.d("Skipping getItem. Returning as activity is destroyed!"); + return null; + } pager.postDelayed(() -> getActivity().supportInvalidateOptionsMenu(), 5); } return MediaDetailFragment.forMedia(i, editable, isFeaturedImage); @@ -315,6 +368,10 @@ public Fragment getItem(int i) { @Override public int getCount() { + if(getActivity() == null) { + Timber.d("Skipping getCount. Returning as activity is destroyed!"); + return 0; + } return ((MediaDetailProvider) getActivity()).getTotalMediaCount(); } } diff --git a/app/src/main/java/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApi.java b/app/src/main/java/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApi.java index 8860241702..12b8972ffc 100644 --- a/app/src/main/java/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApi.java +++ b/app/src/main/java/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApi.java @@ -25,8 +25,6 @@ import org.apache.http.params.CoreProtocolPNames; import org.apache.http.util.EntityUtils; import org.json.JSONObject; -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; @@ -37,7 +35,6 @@ import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; -import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.List; @@ -48,6 +45,7 @@ import fr.free.nrw.commons.BuildConfig; import fr.free.nrw.commons.Media; import fr.free.nrw.commons.PageTitle; +import fr.free.nrw.commons.auth.AccountUtil; import fr.free.nrw.commons.category.CategoryImageUtils; import fr.free.nrw.commons.category.QueryContinue; import fr.free.nrw.commons.notification.Notification; @@ -72,8 +70,8 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi { private static final String THUMB_SIZE = "640"; private AbstractHttpClient httpClient; - private MWApi api; - private MWApi wikidataApi; + private CustomMwApi api; + private CustomMwApi wikidataApi; private Context context; private SharedPreferences defaultPreferences; private SharedPreferences categoryPreferences; @@ -94,9 +92,11 @@ public ApacheHttpClientMediaWikiApi(Context context, ClientConnectionManager cm = new ThreadSafeClientConnManager(params, schemeRegistry); params.setParameter(CoreProtocolPNames.USER_AGENT, getUserAgent()); httpClient = new DefaultHttpClient(cm, params); - httpClient.addRequestInterceptor(NetworkInterceptors.getHttpRequestInterceptor()); - api = new MWApi(apiURL, httpClient); - wikidataApi = new MWApi(wikidatApiURL, httpClient); + if (BuildConfig.DEBUG) { + httpClient.addRequestInterceptor(NetworkInterceptors.getHttpRequestInterceptor()); + } + api = new CustomMwApi(apiURL, httpClient); + wikidataApi = new CustomMwApi(wikidatApiURL, httpClient); this.defaultPreferences = defaultPreferences; this.categoryPreferences = categoryPreferences; this.gson = gson; @@ -161,25 +161,25 @@ private String getLoginToken() throws IOException { } /** - * @param loginApiResult ApiResult Any clientlogin api result + * @param loginCustomApiResult CustomApiResult Any clientlogin api result * @return String On success: "PASS" * continue: "2FA" (More information required for 2FA) * failure: A failure message code (defined by mediawiki) * misc: genericerror-UI, genericerror-REDIRECT, genericerror-RESTART */ - private String getErrorCodeToReturn(ApiResult loginApiResult) { - String status = loginApiResult.getString("/api/clientlogin/@status"); + private String getErrorCodeToReturn(CustomApiResult loginCustomApiResult) { + String status = loginCustomApiResult.getString("/api/clientlogin/@status"); if (status.equals("PASS")) { api.isLoggedIn = true; setAuthCookieOnLogin(true); return status; } else if (status.equals("FAIL")) { setAuthCookieOnLogin(false); - return loginApiResult.getString("/api/clientlogin/@messagecode"); + return loginCustomApiResult.getString("/api/clientlogin/@messagecode"); } else if ( status.equals("UI") - && loginApiResult.getString("/api/clientlogin/requests/_v/@id").equals("TOTPAuthenticationRequest") - && loginApiResult.getString("/api/clientlogin/requests/_v/@provider").equals("Two-factor authentication (OATH).") + && loginCustomApiResult.getString("/api/clientlogin/requests/_v/@id").equals("TOTPAuthenticationRequest") + && loginCustomApiResult.getString("/api/clientlogin/requests/_v/@provider").equals("Two-factor authentication (OATH).") ) { setAuthCookieOnLogin(false); return "2FA"; @@ -209,16 +209,26 @@ public String getAuthCookie() { @Override public void setAuthCookie(String authCookie) { api.setAuthCookie(authCookie); + + Timber.d("Mediawiki auth cookie is %s", api.getAuthCookie()); } @Override public boolean validateLogin() throws IOException { - return api.validateLogin(); + boolean validateLoginResp = api.validateLogin(); + Timber.d("Validate login response is %s", validateLoginResp); + return validateLoginResp; } @Override public String getEditToken() throws IOException { - return api.getEditToken(); + String editToken = api.action("query") + .param("centralauthtoken", getCentralAuthToken()) + .param("meta", "tokens") + .post() + .getString("/api/query/tokens/@csrftoken"); + Timber.d("MediaWiki edit token is %s", editToken); + return editToken; } @Override @@ -227,6 +237,14 @@ public String getCentralAuthToken() throws IOException { .get() .getString("/api/centralauthtoken/@centralauthtoken"); Timber.d("MediaWiki Central auth token is %s", centralAuthToken); + + if(centralAuthToken == null || centralAuthToken.isEmpty()) { + api.removeAllCookies(); + String login = login(AccountUtil.getUserName(context), AccountUtil.getPassword(context)); + if(login.equals("PASS")) { + return getCentralAuthToken(); + } + } return centralAuthToken; } @@ -252,7 +270,8 @@ public boolean pageExists(String pageName) throws IOException { public String edit(String editToken, String processedPageContent, String filename, String summary) throws IOException { return api.action("edit") .param("title", filename) - .param("token", editToken) + .param("token", getEditToken()) + .param("centralauthtoken", getCentralAuthToken()) .param("text", processedPageContent) .param("summary", summary) .post() @@ -265,7 +284,8 @@ public String edit(String editToken, String processedPageContent, String filenam public String appendEdit(String editToken, String processedPageContent, String filename, String summary) throws IOException { return api.action("edit") .param("title", filename) - .param("token", editToken) + .param("token", getEditToken()) + .param("centralauthtoken", getCentralAuthToken()) .param("appendtext", processedPageContent) .param("summary", summary) .post() @@ -277,7 +297,8 @@ public String appendEdit(String editToken, String processedPageContent, String f public String prependEdit(String editToken, String processedPageContent, String filename, String summary) throws IOException { return api.action("edit") .param("title", filename) - .param("token", editToken) + .param("token", getEditToken()) + .param("centralauthtoken", getCentralAuthToken()) .param("prependtext", processedPageContent) .param("summary", summary) .post() @@ -299,7 +320,7 @@ public String findThumbnailByFilename(String filename) throws IOException { @Override @NonNull public MediaResult fetchMediaByFilename(String filename) throws IOException { - ApiResult apiResult = api.action("query") + CustomApiResult apiResult = api.action("query") .param("prop", "revisions") .param("titles", filename) .param("rvprop", "content") @@ -316,7 +337,7 @@ public MediaResult fetchMediaByFilename(String filename) throws IOException { @NonNull public Observable searchCategories(String filterValue, int searchCatsLimit) { return Single.fromCallable(() -> { - List categoryNodes = null; + List categoryNodes = null; try { categoryNodes = api.action("query") .param("format", "xml") @@ -336,7 +357,7 @@ public Observable searchCategories(String filterValue, int searchCatsLim } List categories = new ArrayList<>(); - for (ApiResult categoryNode : categoryNodes) { + for (CustomApiResult categoryNode : categoryNodes) { String cat = categoryNode.getDocument().getTextContent(); String catString = cat.replace("Category:", ""); categories.add(catString); @@ -350,7 +371,7 @@ public Observable searchCategories(String filterValue, int searchCatsLim @NonNull public Observable allCategories(String filterValue, int searchCatsLimit) { return Single.fromCallable(() -> { - ArrayList categoryNodes = null; + ArrayList categoryNodes = null; try { categoryNodes = api.action("query") .param("list", "allcategories") @@ -367,7 +388,7 @@ public Observable allCategories(String filterValue, int searchCatsLimit) } List categories = new ArrayList<>(); - for (ApiResult categoryNode : categoryNodes) { + for (CustomApiResult categoryNode : categoryNodes) { categories.add(categoryNode.getDocument().getTextContent()); } @@ -375,16 +396,6 @@ public Observable allCategories(String filterValue, int searchCatsLimit) }).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") @@ -411,7 +422,7 @@ public String getWikidataCsrfToken() throws IOException { @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") + CustomApiResult result = wikidataApi.action("wbcreateclaim") .param("entity", entityId) .param("centralauthtoken", getCentralAuthToken()) .param("token", getWikidataCsrfToken()) @@ -444,7 +455,7 @@ public String wikidatCreateClaim(String entityId, String property, String snakty @Nullable @Override public boolean addWikidataEditTag(String revisionId) throws IOException { - ApiResult result = wikidataApi.action("tag") + CustomApiResult result = wikidataApi.action("tag") .param("revid", revisionId) .param("centralauthtoken", getCentralAuthToken()) .param("token", getWikidataCsrfToken()) @@ -471,7 +482,7 @@ public boolean addWikidataEditTag(String revisionId) throws IOException { @NonNull public Observable searchTitles(String title, int searchCatsLimit) { return Single.fromCallable((Callable>) () -> { - ArrayList categoryNodes; + ArrayList categoryNodes; try { categoryNodes = api.action("query") @@ -493,7 +504,7 @@ public Observable searchTitles(String title, int searchCatsLimit) { } List titleCategories = new ArrayList<>(); - for (ApiResult categoryNode : categoryNodes) { + for (CustomApiResult categoryNode : categoryNodes) { String cat = categoryNode.getDocument().getTextContent(); String catString = cat.replace("Category:", ""); titleCategories.add(catString); @@ -506,7 +517,7 @@ public Observable searchTitles(String title, int searchCatsLimit) { @Override @NonNull public LogEventResult logEvents(String user, String lastModified, String queryContinue, int limit) throws IOException { - org.mediawiki.api.MWApi.RequestBuilder builder = api.action("query") + CustomMwApi.RequestBuilder builder = api.action("query") .param("list", "logevents") .param("letype", "upload") .param("leprop", "title|timestamp|ids") @@ -518,7 +529,7 @@ public LogEventResult logEvents(String user, String lastModified, String queryCo if (!TextUtils.isEmpty(queryContinue)) { builder.param("lestart", queryContinue); } - ApiResult result = builder.get(); + CustomApiResult result = builder.get(); return new LogEventResult( getLogEventsFromResult(result), @@ -526,11 +537,11 @@ public LogEventResult logEvents(String user, String lastModified, String queryCo } @NonNull - private ArrayList getLogEventsFromResult(ApiResult result) { - ArrayList uploads = result.getNodes("/api/query/logevents/item"); + private ArrayList getLogEventsFromResult(CustomApiResult result) { + ArrayList uploads = result.getNodes("/api/query/logevents/item"); Timber.d("%d results!", uploads.size()); ArrayList logEvents = new ArrayList<>(); - for (ApiResult image : uploads) { + for (CustomApiResult image : uploads) { logEvents.add(new LogEventResult.LogEvent( image.getString("@pageid"), image.getString("@title"), @@ -554,7 +565,7 @@ public String revisionsByFilename(String filename) throws IOException { @Override @NonNull public List getNotifications() { - ApiResult notificationNode = null; + CustomApiResult notificationNode = null; try { notificationNode = api.action("query") .param("notprop", "list") @@ -589,9 +600,9 @@ public List getNotifications() { @Override @NonNull public List getSubCategoryList(String categoryName) { - ApiResult apiResult = null; + CustomApiResult apiResult = null; try { - MWApi.RequestBuilder requestBuilder = api.action("query") + CustomMwApi.RequestBuilder requestBuilder = api.action("query") .param("generator", "categorymembers") .param("format", "xml") .param("gcmtype","subcat") @@ -609,7 +620,7 @@ public List getSubCategoryList(String categoryName) { return new ArrayList<>(); } - ApiResult categoryImagesNode = apiResult.getNode("/api/query/pages"); + CustomApiResult categoryImagesNode = apiResult.getNode("/api/query/pages"); if (categoryImagesNode == null || categoryImagesNode.getDocument() == null || categoryImagesNode.getDocument().getChildNodes() == null @@ -630,9 +641,9 @@ public List getSubCategoryList(String categoryName) { @Override @NonNull public List getParentCategoryList(String categoryName) { - ApiResult apiResult = null; + CustomApiResult apiResult = null; try { - MWApi.RequestBuilder requestBuilder = api.action("query") + CustomMwApi.RequestBuilder requestBuilder = api.action("query") .param("generator", "categories") .param("format", "xml") .param("titles", categoryName) @@ -649,7 +660,7 @@ public List getParentCategoryList(String categoryName) { return new ArrayList<>(); } - ApiResult categoryImagesNode = apiResult.getNode("/api/query/pages"); + CustomApiResult categoryImagesNode = apiResult.getNode("/api/query/pages"); if (categoryImagesNode == null || categoryImagesNode.getDocument() == null || categoryImagesNode.getDocument().getChildNodes() == null @@ -672,9 +683,9 @@ public List getParentCategoryList(String categoryName) { @Override @NonNull public List getCategoryImages(String categoryName) { - ApiResult apiResult = null; + CustomApiResult apiResult = null; try { - MWApi.RequestBuilder requestBuilder = api.action("query") + CustomMwApi.RequestBuilder requestBuilder = api.action("query") .param("generator", "categorymembers") .param("format", "xml") .param("gcmtype", "file") @@ -699,7 +710,7 @@ public List getCategoryImages(String categoryName) { return new ArrayList<>(); } - ApiResult categoryImagesNode = apiResult.getNode("/api/query/pages"); + CustomApiResult categoryImagesNode = apiResult.getNode("/api/query/pages"); if (categoryImagesNode == null || categoryImagesNode.getDocument() == null || categoryImagesNode.getDocument().getChildNodes() == null @@ -727,18 +738,22 @@ public List getCategoryImages(String categoryName) { @Override @NonNull public List searchImages(String query, int offset) { - List imageNodes = null; + List imageNodes = null; + List authorNodes = null; + CustomApiResult customApiResult; try { - imageNodes = api.action("query") + customApiResult= api.action("query") .param("format", "xml") - .param("list", "search") - .param("srwhat", "text") - .param("srnamespace", "6") - .param("srlimit", "25") - .param("sroffset",offset) - .param("srsearch", query) - .get() - .getNodes("/api/query/search/p/@title"); + .param("generator", "search") + .param("gsrwhat", "text") + .param("gsrnamespace", "6") + .param("gsrlimit", "25") + .param("gsroffset",offset) + .param("gsrsearch", query) + .param("prop", "imageinfo") + .get(); + imageNodes= customApiResult.getNodes("/api/query/pages/page/@title"); + authorNodes= customApiResult.getNodes("/api/query/pages/page/imageinfo/ii/@user"); } catch (IOException e) { Timber.e("Failed to obtain searchImages", e); } @@ -748,11 +763,13 @@ public List searchImages(String query, int offset) { } List images = new ArrayList<>(); - for (ApiResult imageNode : imageNodes) { - String imgName = imageNode.getDocument().getTextContent(); - images.add(new Media(imgName)); - } + for (int i=0; i< imageNodes.size();i++){ + String imgName = imageNodes.get(i).getDocument().getTextContent(); + Media media = new Media(imgName); + media.setCreator(authorNodes.get(i).getDocument().getTextContent()); + images.add(media); + } return images; } @@ -765,7 +782,7 @@ public List searchImages(String query, int offset) { @Override @NonNull public List searchCategory(String query, int offset) { - List categoryNodes = null; + List categoryNodes = null; try { categoryNodes = api.action("query") .param("format", "xml") @@ -786,7 +803,7 @@ public List searchCategory(String query, int offset) { } List categories = new ArrayList<>(); - for (ApiResult categoryNode : categoryNodes) { + for (CustomApiResult categoryNode : categoryNodes) { String catName = categoryNode.getDocument().getTextContent(); categories.add(catName); } @@ -858,11 +875,11 @@ public UploadResult uploadFile(String filename, long dataLength, String pageContents, String editSummary, - final ProgressListener progressListener, Uri fileUri, - Uri contentProviderUri) throws IOException { + Uri contentProviderUri, + final ProgressListener progressListener) throws IOException { - ApiResult result = api.upload(filename, file, dataLength, pageContents, editSummary, progressListener::onProgress); + CustomApiResult result = api.upload(filename, file, dataLength, pageContents, editSummary, getCentralAuthToken(), getEditToken(), progressListener::onProgress); Log.e("WTF", "Result: " + result.toString()); @@ -910,7 +927,7 @@ public Single getUploadCount(String userName) { public boolean isUserBlockedFromCommons() { boolean userBlocked = false; try { - ApiResult result = api.action("query") + CustomApiResult result = api.action("query") .param("action", "query") .param("format", "xml") .param("meta", "userinfo") diff --git a/app/src/main/java/fr/free/nrw/commons/mwapi/CustomApiResult.java b/app/src/main/java/fr/free/nrw/commons/mwapi/CustomApiResult.java new file mode 100644 index 0000000000..a35270150f --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/mwapi/CustomApiResult.java @@ -0,0 +1,123 @@ +package fr.free.nrw.commons.mwapi; + +import org.apache.http.client.HttpClient; +import org.w3c.dom.Document; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.SAXException; + +import java.io.IOError; +import java.io.IOException; +import java.io.StringWriter; +import java.util.ArrayList; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerException; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathExpressionException; +import javax.xml.xpath.XPathFactory; + +import in.yuvi.http.fluent.Http; +import timber.log.Timber; + +public class CustomApiResult { + private Node doc; + private XPath evaluator; + + CustomApiResult(Node doc) { + this.doc = doc; + this.evaluator = XPathFactory.newInstance().newXPath(); + } + + static CustomApiResult fromRequestBuilder(Http.HttpRequestBuilder builder, HttpClient client) throws IOException { + + Timber.d("API request is %s", builder.toString()); + Timber.d("API params are %s", client.getParams()); + + try { + DocumentBuilder docBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); + Document doc = docBuilder.parse(builder.use(client).charset("utf-8").data("format", "xml").asResponse().getEntity().getContent()); + printStringFromDocument(doc); + return new CustomApiResult(doc); + } catch (ParserConfigurationException e) { + // I don't know wtf I can do about this on... + throw new RuntimeException(e); + } catch (IllegalStateException e) { + // So, this should never actually happen - since we assume MediaWiki always generates valid json + // So the only thing causing this would be a network truncation + // Sooo... I can throw IOError + // Thanks Java, for making me spend significant time on shit that happens once in a bluemoon + // I surely am writing Nuclear Submarine controller code + throw new IOError(e); + } catch (SAXException e) { + // See Rant above + throw new IOError(e); + } + } + + public static void printStringFromDocument(Document doc) + { + try + { + DOMSource domSource = new DOMSource(doc); + StringWriter writer = new StringWriter(); + StreamResult result = new StreamResult(writer); + TransformerFactory tf = TransformerFactory.newInstance(); + Transformer transformer = tf.newTransformer(); + transformer.transform(domSource, result); + Timber.d("API response is\n %s", writer.toString()); + } + catch(TransformerException ex) + { + Timber.d("Error occurred in transforming", ex); + } + } + + public Node getDocument() { + return doc; + } + + public ArrayList getNodes(String xpath) { + try { + ArrayList results = new ArrayList(); + NodeList nodes = (NodeList) evaluator.evaluate(xpath, doc, XPathConstants.NODESET); + for(int i = 0; i < nodes.getLength(); i++) { + results.add(new CustomApiResult(nodes.item(i))); + } + return results; + } catch (XPathExpressionException e) { + return null; + } + + } + public CustomApiResult getNode(String xpath) { + try { + return new CustomApiResult((Node) evaluator.evaluate(xpath, doc, XPathConstants.NODE)); + } catch (XPathExpressionException e) { + return null; + } + } + + public Double getNumber(String xpath) { + try { + return (Double) evaluator.evaluate(xpath, doc, XPathConstants.NUMBER); + } catch (XPathExpressionException e) { + return null; + } + } + + public String getString(String xpath) { + try { + return (String) evaluator.evaluate(xpath, doc, XPathConstants.STRING); + } catch (XPathExpressionException e) { + return null; + } + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/mwapi/CustomMwApi.java b/app/src/main/java/fr/free/nrw/commons/mwapi/CustomMwApi.java new file mode 100644 index 0000000000..1b9e93fa9c --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/mwapi/CustomMwApi.java @@ -0,0 +1,183 @@ +package fr.free.nrw.commons.mwapi; + +import org.apache.http.cookie.Cookie; +import org.apache.http.impl.client.AbstractHttpClient; +import org.apache.http.impl.cookie.BasicClientCookie; +import org.mediawiki.api.ApiResult; + +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.HashMap; +import java.util.List; + +import in.yuvi.http.fluent.Http; +import in.yuvi.http.fluent.ProgressListener; +import timber.log.Timber; + +public class CustomMwApi { + public class RequestBuilder { + private HashMap params; + private CustomMwApi api; + + RequestBuilder(CustomMwApi api) { + params = new HashMap(); + this.api = api; + } + + public RequestBuilder param(String key, Object value) { + params.put(key, value); + return this; + } + + public CustomApiResult get() throws IOException { + return api.makeRequest("GET", params); + } + + public CustomApiResult post() throws IOException { + return api.makeRequest("POST", params); + } + } + + private AbstractHttpClient client; + private String apiURL; + public boolean isLoggedIn; + private String authCookie = null; + private String userName = null; + private String userID = null; + + public CustomMwApi(String apiURL, AbstractHttpClient client) { + this.apiURL = apiURL; + this.client = client; + } + + public RequestBuilder action(String action) { + RequestBuilder builder = new RequestBuilder(this); + builder.param("action", action); + return builder; + } + + public String getAuthCookie() { + if(authCookie == null){ + authCookie = ""; + List cookies = client.getCookieStore().getCookies(); + for(Cookie cookie: cookies) { + authCookie += cookie.getName() + "=" + cookie.getValue() + ";"; + } + } + return authCookie; + } + + public void setAuthCookie(String authCookie) { + this.authCookie = authCookie; + this.isLoggedIn = true; + String[] cookies = authCookie.split(";"); + String domain; + try { + domain = new URL(apiURL).getHost(); + } catch (MalformedURLException e) { + // Mighty well better not happen! + e.printStackTrace(); + throw new RuntimeException(e); + } + // This works because I know which cookies are going to be set by MediaWiki, and they don't contain a = or ; in them :D + for(String cookie: cookies) { + String[] parts = cookie.split("="); + BasicClientCookie c = new BasicClientCookie(parts[0], parts[1]); + c.setDomain(domain); + client.getCookieStore().addCookie(c); + } + } + + public void removeAllCookies() { + client.getCookieStore().clear(); + } + + public boolean validateLogin() throws IOException { + CustomApiResult userMeta = this.action("query").param("meta", "userinfo").get(); + this.userID = userMeta.getString("/api/query/userinfo/@id"); + this.userName = userMeta.getString("/api/query/userinfo/@name"); + Timber.d("User id is %s and user name is %s", userID, userName); + return !userID.equals("0"); + } + + public String getUserID() throws IOException { + if(this.userID == null || this.userID.equals("0")) { + this.validateLogin(); + } + return userID; + } + + public String getUserName() throws IOException { + if(this.userID == null || this.userID.equals("0")) { + this.validateLogin(); + } + return userName; + } + + public String login(String username, String password) throws IOException { + CustomApiResult tokenData = this.action("login").param("lgname", username).param("lgpassword", password).post(); + String result = tokenData.getString("/api/login/@result"); + if (result.equals("NeedToken")) { + String token = tokenData.getString("/api/login/@token"); + CustomApiResult confirmData = this.action("login").param("lgname", username).param("lgpassword", password).param("lgtoken", token).post(); + String finalResult = confirmData.getString("/api/login/@result"); + if(finalResult.equals("Success")) { + isLoggedIn = true; + } + return finalResult; + } else { + return result; + } + } + + public CustomApiResult upload(String filename, InputStream file, long length, String text, String comment, String centralAuthToken, String token) throws IOException { + return this.upload(filename, file, length, text, comment,centralAuthToken, token, null); + } + + public CustomApiResult upload(String filename, InputStream file, String text, String comment, String centralAuthToken, String token) throws IOException { + return this.upload(filename, file, -1, text, comment,centralAuthToken, token, null); + } + + public CustomApiResult upload(String filename, InputStream file, long length, String text, String comment, String centralAuthToken, String token, ProgressListener uploadProgressListener) throws IOException { + Timber.d("Token being used is %s", token); + + Http.HttpRequestBuilder builder = Http.multipart(apiURL) + .data("action", "upload") + .data("token", token) + .data("centralauthtoken", centralAuthToken) + .data("text", text) + .data("ignorewarnings", "1") + .data("comment", comment) + .data("filename", filename) + .sendProgressListener(uploadProgressListener); + if(length != -1) { + builder.file("file", filename, file, length); + } else { + builder.file("file", filename, file); + } + + Timber.d("Final cookies are %s", client.getCookieStore().getCookies().toString()); + + return CustomApiResult.fromRequestBuilder(builder, client); + } + + public void logout() throws IOException { + // I should be doing more validation here, but meh + isLoggedIn = false; + this.action("logout").post(); + } + + private CustomApiResult makeRequest(String method, HashMap params) throws IOException { + Http.HttpRequestBuilder builder; + if (method.equals("POST")) { + builder = Http.post(apiURL); + } else { + builder = Http.get(apiURL); + } + builder.data(params); + return CustomApiResult.fromRequestBuilder(builder, client); + } +} +; \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/mwapi/MediaWikiApi.java b/app/src/main/java/fr/free/nrw/commons/mwapi/MediaWikiApi.java index a40f04bffa..d96e9b177a 100644 --- a/app/src/main/java/fr/free/nrw/commons/mwapi/MediaWikiApi.java +++ b/app/src/main/java/fr/free/nrw/commons/mwapi/MediaWikiApi.java @@ -55,7 +55,7 @@ public interface MediaWikiApi { List searchCategory(String title, int offset); @NonNull - UploadResult uploadFile(String filename, InputStream file, long dataLength, String pageContents, String editSummary, ProgressListener progressListener, Uri fileUri, Uri contentProviderUri) throws IOException; + UploadResult uploadFile(String filename, InputStream file, long dataLength, String pageContents, String editSummary, Uri fileUri, Uri contentProviderUri, ProgressListener progressListener) throws IOException; @Nullable String edit(String editToken, String processedPageContent, String filename, String summary) throws IOException; diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/DirectUpload.java b/app/src/main/java/fr/free/nrw/commons/nearby/DirectUpload.java index 9a12c6d390..ca25fccf3d 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/DirectUpload.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/DirectUpload.java @@ -31,12 +31,12 @@ void initiateGalleryUpload() { if (fragment.shouldShowRequestPermissionRationale(READ_EXTERNAL_STORAGE)) { new AlertDialog.Builder(fragment.getActivity()) .setMessage(fragment.getActivity().getString(R.string.read_storage_permission_rationale)) - .setPositiveButton("OK", (dialog, which) -> { + .setPositiveButton(android.R.string.ok, (dialog, which) -> { Timber.d("Requesting permissions for read external storage"); fragment.requestPermissions(new String[]{READ_EXTERNAL_STORAGE}, 4); dialog.dismiss(); }) - .setNegativeButton("Cancel", null) + .setNegativeButton(android.R.string.cancel, null) .create() .show(); } else { @@ -58,11 +58,11 @@ void initiateCameraUpload() { if (fragment.shouldShowRequestPermissionRationale(WRITE_EXTERNAL_STORAGE)) { new AlertDialog.Builder(fragment.getActivity()) .setMessage(fragment.getActivity().getString(R.string.write_storage_permission_rationale)) - .setPositiveButton("OK", (dialog, which) -> { + .setPositiveButton(android.R.string.ok, (dialog, which) -> { fragment.requestPermissions(new String[]{WRITE_EXTERNAL_STORAGE}, 5); dialog.dismiss(); }) - .setNegativeButton("Cancel", null) + .setNegativeButton(android.R.string.cancel, null) .create() .show(); } else { diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyActivity.java b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyActivity.java index cb28df9474..2143496f01 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyActivity.java @@ -15,7 +15,6 @@ import android.support.design.widget.BottomSheetBehavior; import android.support.v4.app.FragmentTransaction; import android.support.v7.app.AlertDialog; - import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; @@ -51,8 +50,10 @@ 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.LOCATION_SIGNIFICANTLY_CHANGED; +import static fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType.LOCATION_SLIGHTLY_CHANGED; import static fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType.MAP_UPDATED; +import static fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType.PERMISSION_JUST_GRANTED; public class NearbyActivity extends NavigationBaseActivity implements LocationUpdateListener, @@ -291,11 +292,11 @@ private void checkLocationPermission() { // sees the explanation, try again to request the permission. new AlertDialog.Builder(this) .setMessage(getString(R.string.location_permission_rationale_nearby)) - .setPositiveButton("OK", (dialog, which) -> { + .setPositiveButton(android.R.string.ok, (dialog, which) -> { requestLocationPermissions(); dialog.dismiss(); }) - .setNegativeButton("Cancel", (dialog, id) -> { + .setNegativeButton(android.R.string.cancel, (dialog, id) -> { showLocationPermissionDeniedErrorDialog(); dialog.cancel(); }) @@ -466,11 +467,11 @@ private void registerLocationUpdates() { if (locationManager.isPermissionExplanationRequired(this)) { new AlertDialog.Builder(this) .setMessage(getString(R.string.location_permission_rationale_nearby)) - .setPositiveButton("OK", (dialog, which) -> { + .setPositiveButton(android.R.string.ok, (dialog, which) -> { requestLocationPermissions(); dialog.dismiss(); }) - .setNegativeButton("Cancel", (dialog, id) -> { + .setNegativeButton(android.R.string.cancel, (dialog, id) -> { showLocationPermissionDeniedErrorDialog(); dialog.cancel(); }) diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyController.java b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyController.java index bd042b4d79..4f777aeb46 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyController.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyController.java @@ -55,7 +55,7 @@ public NearbyPlacesInfo loadAttractionsFromLocation(LatLng curLatLng) throws IOE } List places = nearbyPlaces.getFromWikidataQuery(curLatLng, Locale.getDefault().getLanguage()); - if (places.size() > 0) { + if (null != places && places.size() > 0) { LatLng[] boundaryCoordinates = {places.get(0).location, // south places.get(0).location, // north places.get(0).location, // west diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyListFragment.java b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyListFragment.java index 81768d3890..e9acd99793 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyListFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyListFragment.java @@ -148,12 +148,13 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) { if (resultCode == RESULT_OK) { Timber.d("OnActivityResult() parameters: Req code: %d Result code: %d Data: %s", requestCode, resultCode, data); + String wikidataEntityId = directPrefs.getString("WikiDataEntityId", null); if (requestCode == ContributionController.SELECT_FROM_CAMERA) { // If coming from camera, pass null as uri. Because camera photos get saved to a // fixed directory - controller.handleImagePicked(requestCode, null, true, null); + controller.handleImagePicked(requestCode, null, true, wikidataEntityId); } else { - controller.handleImagePicked(requestCode, data.getData(), true, null); + controller.handleImagePicked(requestCode, data.getData(), true, wikidataEntityId); } } else { Timber.e("OnActivityResult() parameters: Req code: %d Result code: %d Data: %s", diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyMapFragment.java b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyMapFragment.java index bb2d9c671a..ff6d8f5238 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyMapFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyMapFragment.java @@ -15,6 +15,7 @@ import android.support.design.widget.BottomSheetBehavior; import android.support.design.widget.CoordinatorLayout; import android.support.design.widget.FloatingActionButton; +import android.support.v7.app.AlertDialog; import android.view.Gravity; import android.view.KeyEvent; import android.view.LayoutInflater; @@ -53,8 +54,10 @@ import javax.inject.Named; import dagger.android.support.DaggerFragment; +import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.R; import fr.free.nrw.commons.Utils; +import fr.free.nrw.commons.auth.LoginActivity; import fr.free.nrw.commons.contributions.ContributionController; import fr.free.nrw.commons.utils.ContributionUtils; import fr.free.nrw.commons.utils.UriDeserializer; @@ -68,6 +71,8 @@ public class NearbyMapFragment extends DaggerFragment { + @Inject + @Named("application_preferences") SharedPreferences applicationPrefs; public MapView mapView; private List baseMarkerOptions; private fr.free.nrw.commons.location.LatLng curLatLng; @@ -373,7 +378,23 @@ private void initViews() { } private void setListeners() { - fabPlus.setOnClickListener(view -> animateFAB(isFabOpen)); + fabPlus.setOnClickListener(view -> { + if (applicationPrefs.getBoolean("login_skipped", false)) { + // prompt the user to login + new AlertDialog.Builder(getContext()) + .setMessage(R.string.login_alert_message) + .setPositiveButton(R.string.login, (dialog, which) -> { + // logout of the app + BaseLogoutListener logoutListener = new BaseLogoutListener(); + CommonsApplication app = (CommonsApplication) getActivity().getApplication(); + app.clearApplicationData(getContext(), logoutListener); + + }) + .show(); + }else { + animateFAB(isFabOpen); + } + }); bottomSheetDetails.setOnClickListener(view -> { if (bottomSheetDetailsBehavior.getState() == BottomSheetBehavior.STATE_COLLAPSED) { @@ -491,6 +512,21 @@ public void onMapReady(MapboxMap mapboxMap) { mapView.setStyleUrl("asset://mapstyle.json"); } + /** + * onLogoutComplete is called after shared preferences and data stored in local database are cleared. + */ + private class BaseLogoutListener implements CommonsApplication.LogoutListener { + @Override + public void onLogoutComplete() { + Timber.d("Logout complete callback received."); + Intent nearbyIntent = new Intent( getActivity(), LoginActivity.class); + nearbyIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); + nearbyIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + startActivity(nearbyIntent); + getActivity().finish(); + } + } + /** * Adds a marker for the user's current position. Adds a * circle which uses the accuracy * 2, to draw a circle @@ -770,12 +806,13 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) { if (resultCode == RESULT_OK) { Timber.d("OnActivityResult() parameters: Req code: %d Result code: %d Data: %s", requestCode, resultCode, data); + String wikidataEntityId = directPrefs.getString("WikiDataEntityId", null); if (requestCode == ContributionController.SELECT_FROM_CAMERA) { // If coming from camera, pass null as uri. Because camera photos get saved to a // fixed directory - controller.handleImagePicked(requestCode, null, true, null); + controller.handleImagePicked(requestCode, null, true, wikidataEntityId); } else { - controller.handleImagePicked(requestCode, data.getData(), true, null); + controller.handleImagePicked(requestCode, data.getData(), true, wikidataEntityId); } } else { Timber.e("OnActivityResult() parameters: Req code: %d Result code: %d Data: %s", diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyPlaces.java b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyPlaces.java index c8d20f7539..f9d35d63ee 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyPlaces.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyPlaces.java @@ -5,6 +5,7 @@ import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; +import java.io.InterruptedIOException; import java.net.URL; import java.net.URLConnection; import java.util.ArrayList; @@ -45,7 +46,12 @@ List getFromWikidataQuery(LatLng curLatLng, String lang) throws IOExcepti // increase the radius gradually to find a satisfactory number of nearby places while (radius <= MAX_RADIUS) { - places = getFromWikidataQuery(curLatLng, lang, radius); + try { + places = getFromWikidataQuery(curLatLng, lang, radius); + } catch (InterruptedIOException e) { + Timber.d("exception in fetching nearby places", e.getLocalizedMessage()); + return places; + } Timber.d("%d results at radius: %f", places.size(), radius); if (places.size() >= MIN_RESULTS) { break; diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/PlaceRenderer.java b/app/src/main/java/fr/free/nrw/commons/nearby/PlaceRenderer.java index 0d33b4a5e4..ec4dbf8bac 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/PlaceRenderer.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/PlaceRenderer.java @@ -6,6 +6,7 @@ import android.content.SharedPreferences; import android.support.v4.app.Fragment; import android.support.transition.TransitionManager; +import android.support.v7.app.AlertDialog; import android.support.v7.widget.PopupMenu; import android.util.Log; import android.view.LayoutInflater; @@ -27,12 +28,18 @@ import butterknife.ButterKnife; import fr.free.nrw.commons.R; import fr.free.nrw.commons.Utils; +import fr.free.nrw.commons.auth.LoginActivity; import fr.free.nrw.commons.contributions.ContributionController; import fr.free.nrw.commons.di.ApplicationlessInjection; import timber.log.Timber; +import static fr.free.nrw.commons.theme.NavigationBaseActivity.startActivityWithFlags; +import static fr.free.nrw.commons.wikidata.WikidataConstants.WIKIDATA_ENTITY_ID_PREF; + public class PlaceRenderer extends Renderer { + @Inject + @Named("application_preferences") SharedPreferences applicationPrefs; @BindView(R.id.tvName) TextView tvName; @BindView(R.id.tvDesc) TextView tvDesc; @BindView(R.id.distance) TextView distance; @@ -88,9 +95,9 @@ protected void hookListeners(View view) { Log.d("Renderer", "clicked"); TransitionManager.beginDelayedTransition(buttonLayout); - if(buttonLayout.isShown()){ + if (buttonLayout.isShown()) { closeLayout(buttonLayout); - }else { + } else { openLayout(buttonLayout); } @@ -106,18 +113,46 @@ protected void hookListeners(View view) { }); cameraButton.setOnClickListener(view2 -> { - Timber.d("Camera button tapped. Image title: " + place.getName() + "Image desc: " + place.getLongDescription()); - DirectUpload directUpload = new DirectUpload(fragment, controller); - storeSharedPrefs(); - directUpload.initiateCameraUpload(); + if (applicationPrefs.getBoolean("login_skipped", false)) { + // prompt the user to login + new AlertDialog.Builder(getContext()) + .setMessage(R.string.login_alert_message) + .setPositiveButton(R.string.login, (dialog, which) -> { + startActivityWithFlags( getContext(), LoginActivity.class, Intent.FLAG_ACTIVITY_CLEAR_TOP, + Intent.FLAG_ACTIVITY_SINGLE_TOP); + prefs.edit().putBoolean("login_skipped", false).apply(); + fragment.getActivity().finish(); + }) + .show(); + } else { + Timber.d("Camera button tapped. Image title: " + place.getName() + "Image desc: " + place.getLongDescription()); + DirectUpload directUpload = new DirectUpload(fragment, controller); + storeSharedPrefs(); + directUpload.initiateCameraUpload(); + } }); + galleryButton.setOnClickListener(view3 -> { - Timber.d("Gallery button tapped. Image title: " + place.getName() + "Image desc: " + place.getLongDescription()); - DirectUpload directUpload = new DirectUpload(fragment, controller); - storeSharedPrefs(); - directUpload.initiateGalleryUpload(); + if (applicationPrefs.getBoolean("login_skipped", false)) { + // prompt the user to login + new AlertDialog.Builder(getContext()) + .setMessage(R.string.login_alert_message) + .setPositiveButton(R.string.login, (dialog, which) -> { + startActivityWithFlags( getContext(), LoginActivity.class, Intent.FLAG_ACTIVITY_CLEAR_TOP, + Intent.FLAG_ACTIVITY_SINGLE_TOP); + prefs.edit().putBoolean("login_skipped", false).apply(); + fragment.getActivity().finish(); + }) + .show(); + }else { + Timber.d("Gallery button tapped. Image title: " + place.getName() + "Image desc: " + place.getLongDescription()); + DirectUpload directUpload = new DirectUpload(fragment, controller); + storeSharedPrefs(); + directUpload.initiateGalleryUpload(); + } }); + } private void storeSharedPrefs() { @@ -126,6 +161,7 @@ private void storeSharedPrefs() { editor.putString("Title", place.getName()); editor.putString("Desc", place.getLongDescription()); editor.putString("Category", place.getCategory()); + editor.putString(WIKIDATA_ENTITY_ID_PREF, place.getWikiDataEntityId()); editor.apply(); } @@ -207,4 +243,4 @@ private boolean showMenu() { return place.hasCommonsLink() || place.hasWikidataLink(); } -} \ No newline at end of file +} diff --git a/app/src/main/java/fr/free/nrw/commons/notification/NotificationRenderer.java b/app/src/main/java/fr/free/nrw/commons/notification/NotificationRenderer.java index 6dcfca35dd..1164d9d57f 100644 --- a/app/src/main/java/fr/free/nrw/commons/notification/NotificationRenderer.java +++ b/app/src/main/java/fr/free/nrw/commons/notification/NotificationRenderer.java @@ -1,6 +1,5 @@ package fr.free.nrw.commons.notification; -import android.graphics.drawable.PictureDrawable; import android.text.Html; import android.view.LayoutInflater; import android.view.View; @@ -9,23 +8,17 @@ 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 { - private RequestBuilder requestBuilder; - @BindView(R.id.title) ReadMoreTextView title; @BindView(R.id.time) TextView time; @BindView(R.id.icon) ImageView icon; @@ -48,11 +41,6 @@ protected void hookListeners(View rootView) { 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; } @@ -61,7 +49,6 @@ public void render() { Notification notification = getContent(); setTitle(notification.notificationText); time.setText(notification.date); - requestBuilder.load(notification.iconUrl).into(icon); } /** diff --git a/app/src/main/java/fr/free/nrw/commons/notification/SvgModule.java b/app/src/main/java/fr/free/nrw/commons/notification/SvgModule.java deleted file mode 100644 index 5a1e8ae636..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/notification/SvgModule.java +++ /dev/null @@ -1,35 +0,0 @@ -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; - } -} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/quiz/QuizChecker.java b/app/src/main/java/fr/free/nrw/commons/quiz/QuizChecker.java index b085636605..abc6bf48f5 100644 --- a/app/src/main/java/fr/free/nrw/commons/quiz/QuizChecker.java +++ b/app/src/main/java/fr/free/nrw/commons/quiz/QuizChecker.java @@ -135,7 +135,7 @@ public void callQuiz() { alert.setTitle(context.getResources().getString(R.string.quiz)); alert.setMessage(context.getResources().getString(R.string.quiz_alert_message, REVERT_PERCENTAGE_FOR_MESSAGE)); - alert.setPositiveButton("Proceed", new DialogInterface.OnClickListener() { + alert.setPositiveButton(R.string.about_translate_proceed, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { int newRevetSharedPrefs = revertCount+ revertPref.getInt(REVERT_SHARED_PREFERENCE,0); diff --git a/app/src/main/java/fr/free/nrw/commons/quiz/QuizResultActivity.java b/app/src/main/java/fr/free/nrw/commons/quiz/QuizResultActivity.java index e7c40f6157..93a113338b 100644 --- a/app/src/main/java/fr/free/nrw/commons/quiz/QuizResultActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/quiz/QuizResultActivity.java @@ -183,12 +183,12 @@ public void showAlert(Bitmap screenshot) { TextView shareMessage = (TextView) view.findViewById(R.id.alert_text); shareMessage.setText(R.string.quiz_result_share_message); alertadd.setView(view); - alertadd.setPositiveButton("Proceed", new DialogInterface.OnClickListener() { + alertadd.setPositiveButton(R.string.about_translate_proceed, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { shareScreen(screenshot); } }); - alertadd.setNegativeButton("Cancel", new DialogInterface.OnClickListener() { + alertadd.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { dialog.cancel(); diff --git a/app/src/main/java/fr/free/nrw/commons/theme/BaseActivity.java b/app/src/main/java/fr/free/nrw/commons/theme/BaseActivity.java index 8ef8e84a44..9668f11eea 100644 --- a/app/src/main/java/fr/free/nrw/commons/theme/BaseActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/theme/BaseActivity.java @@ -8,7 +8,7 @@ import fr.free.nrw.commons.di.CommonsDaggerAppCompatActivity; public abstract class BaseActivity extends CommonsDaggerAppCompatActivity { - boolean currentTheme; + protected boolean currentTheme; @Override protected void onCreate(Bundle savedInstanceState) { diff --git a/app/src/main/java/fr/free/nrw/commons/theme/NavigationBaseActivity.java b/app/src/main/java/fr/free/nrw/commons/theme/NavigationBaseActivity.java index 8107a961a9..49fee01c12 100644 --- a/app/src/main/java/fr/free/nrw/commons/theme/NavigationBaseActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/theme/NavigationBaseActivity.java @@ -5,6 +5,7 @@ import android.content.ActivityNotFoundException; import android.content.Context; import android.content.Intent; +import android.content.SharedPreferences; import android.net.Uri; import android.support.annotation.NonNull; import android.support.design.widget.NavigationView; @@ -12,6 +13,7 @@ import android.support.v7.app.ActionBarDrawerToggle; import android.support.v7.app.AlertDialog; import android.support.v7.widget.Toolbar; +import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; @@ -19,6 +21,9 @@ import android.widget.TextView; import android.widget.Toast; +import javax.inject.Inject; +import javax.inject.Named; + import butterknife.BindView; import fr.free.nrw.commons.AboutActivity; import fr.free.nrw.commons.BuildConfig; @@ -46,6 +51,8 @@ public abstract class NavigationBaseActivity extends BaseActivity NavigationView navigationView; @BindView(R.id.drawer_layout) DrawerLayout drawerLayout; + @Inject @Named("application_preferences") SharedPreferences prefs; + private ActionBarDrawerToggle toggle; @@ -61,6 +68,24 @@ public void initDrawer() { toggle.syncState(); setDrawerPaneWidth(); setUserName(); + Menu nav_Menu = navigationView.getMenu(); + View headerLayout = navigationView.getHeaderView(0); + ImageView userIcon = headerLayout.findViewById(R.id.user_icon); + if (prefs.getBoolean("login_skipped", false)) { + userIcon.setVisibility(View.GONE); + nav_Menu.findItem(R.id.action_login).setVisible(true); + nav_Menu.findItem(R.id.action_home).setVisible(false); + nav_Menu.findItem(R.id.action_notifications).setVisible(false); + nav_Menu.findItem(R.id.action_settings).setVisible(false); + nav_Menu.findItem(R.id.action_logout).setVisible(false); + }else { + userIcon.setVisibility(View.VISIBLE); + nav_Menu.findItem(R.id.action_login).setVisible(false); + nav_Menu.findItem(R.id.action_home).setVisible(true); + nav_Menu.findItem(R.id.action_notifications).setVisible(true); + nav_Menu.findItem(R.id.action_settings).setVisible(true); + nav_Menu.findItem(R.id.action_logout).setVisible(true); + } } /** @@ -71,7 +96,7 @@ private void setUserName() { View navHeaderView = navigationView.getHeaderView(0); TextView username = navHeaderView.findViewById(R.id.username); AccountManager accountManager = AccountManager.get(this); - Account[] allAccounts = accountManager.getAccountsByType(AccountUtil.ACCOUNT_TYPE); + Account[] allAccounts = accountManager.getAccountsByType(BuildConfig.ACCOUNT_TYPE); if (allAccounts.length != 0) { username.setText(allAccounts[0].name); } @@ -120,6 +145,14 @@ private void setDrawerPaneWidth() { public boolean onNavigationItemSelected(@NonNull final MenuItem item) { final int itemId = item.getItemId(); switch (itemId) { + case R.id.action_login: + drawerLayout.closeDrawer(navigationView); + startActivityWithFlags( + this, LoginActivity.class, Intent.FLAG_ACTIVITY_CLEAR_TOP, + Intent.FLAG_ACTIVITY_SINGLE_TOP); + prefs.edit().putBoolean("login_skipped", false).apply(); + finish(); + return true; case R.id.action_home: drawerLayout.closeDrawer(navigationView); startActivityWithFlags( diff --git a/app/src/main/java/fr/free/nrw/commons/upload/Description.java b/app/src/main/java/fr/free/nrw/commons/upload/Description.java index 8e41c422be..68e374012c 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/Description.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/Description.java @@ -1,30 +1,30 @@ package fr.free.nrw.commons.upload; import android.text.TextUtils; -import android.util.TimeUtils; +import java.util.List; + +/** + * Holds a description of an item being uploaded by {@link UploadActivity} + */ class Description { - private String languageId; - private String languageDisplayText; + private String languageCode; private String descriptionText; - private boolean set; private int selectedLanguageIndex = -1; - public String getLanguageId() { - return languageId; - } - - public void setLanguageId(String languageId) { - this.languageId = languageId; - } - - public String getLanguageDisplayText() { - return languageDisplayText; + /** + * @return The language code ie. "en" or "fr" + */ + public String getLanguageCode() { + return languageCode; } - public void setLanguageDisplayText(String languageDisplayText) { - this.languageDisplayText = languageDisplayText; + /** + * @param languageCode The language code ie. "en" or "fr" + */ + public void setLanguageCode(String languageCode) { + this.languageCode = languageCode; } public String getDescriptionText() { @@ -33,25 +33,42 @@ public String getDescriptionText() { public void setDescriptionText(String descriptionText) { this.descriptionText = descriptionText; - - if (!TextUtils.isEmpty(descriptionText)) { - set = true; - } - } - - public boolean isSet() { - return set; - } - - public void setSet(boolean set) { - this.set = set; } + /** + * @return the index of the language selected in a spinner with {@link SpinnerLanguagesAdapter} + */ public int getSelectedLanguageIndex() { return selectedLanguageIndex; } + /** + * @param selectedLanguageIndex the index of the language selected in a spinner with {@link SpinnerLanguagesAdapter} + */ public void setSelectedLanguageIndex(int selectedLanguageIndex) { this.selectedLanguageIndex = selectedLanguageIndex; } + + + /** + * Formats the list of descriptions into the format Commons requires for uploads. + * + * @param descriptions the list of descriptions, description is ignored if text is null. + * @return a string with the pattern of {{en|1=descriptionText}} + */ + public static String formatList(List descriptions) { + StringBuilder descListString = new StringBuilder(); + for (Description description : descriptions) { + if (!description.isEmpty()) { + String individualDescription = String.format("{{%s|1=%s}}", description.getLanguageCode(), + description.getDescriptionText()); + descListString.append(individualDescription); + } + } + return descListString.toString(); + } + + public boolean isEmpty() { + return descriptionText == null || descriptionText.isEmpty(); + } } diff --git a/app/src/main/java/fr/free/nrw/commons/upload/DescriptionsAdapter.java b/app/src/main/java/fr/free/nrw/commons/upload/DescriptionsAdapter.java index 8642e12101..d7dace1f39 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/DescriptionsAdapter.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/DescriptionsAdapter.java @@ -4,12 +4,12 @@ import android.content.Context; import android.graphics.drawable.Drawable; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.support.v4.view.ViewCompat; import android.support.v7.widget.AppCompatSpinner; import android.support.v7.widget.RecyclerView; -import android.text.Editable; import android.text.TextUtils; -import android.text.TextWatcher; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; @@ -17,60 +17,86 @@ import android.widget.AdapterView; import android.widget.AdapterView.OnItemSelectedListener; import android.widget.EditText; +import android.widget.TextView; + import butterknife.BindView; import butterknife.ButterKnife; import butterknife.OnTouch; +import butterknife.Optional; import fr.free.nrw.commons.R; +import fr.free.nrw.commons.utils.AbstractTextWatcher; +import fr.free.nrw.commons.utils.BiMap; import fr.free.nrw.commons.utils.ViewUtil; +import io.reactivex.Observable; +import io.reactivex.subjects.BehaviorSubject; +import io.reactivex.subjects.Subject; +import timber.log.Timber; + import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; import java.util.List; -import java.util.Locale; class DescriptionsAdapter extends RecyclerView.Adapter { + Title title; List descriptions; - List languages; private Context context; private Callback callback; + private Subject titleChangedSubject; + + private BiMap selectedLanguages; public DescriptionsAdapter() { + title = new Title(); descriptions = new ArrayList<>(); descriptions.add(new Description()); - languages = new ArrayList<>(); + titleChangedSubject=BehaviorSubject.create(); + selectedLanguages=new BiMap<>(); } public void setCallback(Callback callback) { this.callback = callback; } - public void setDescriptions(List descriptions) { + public Observable getTitleChangeObserver(){ + return titleChangedSubject; + } + + public void setItems(Title title, List descriptions) { this.descriptions = descriptions; + this.title = title; + selectedLanguages=new BiMap<>(); notifyDataSetChanged(); } - public void setLanguages(List languages) { - this.languages = languages; + @Override + public int getItemViewType(int position) { + if (position == 0) return 1; + else return 2; } + @NonNull @Override - public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { - View view = LayoutInflater.from(parent.getContext()) - .inflate(R.layout.row_item_description, parent, false); + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View view; + if (viewType == 1) { + view = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.row_item_title, parent, false); + } else { + view = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.row_item_description, parent, false); + } context = parent.getContext(); - ViewHolder viewHolder = new ViewHolder(view); - return viewHolder; + return new ViewHolder(view); } @Override - public void onBindViewHolder(ViewHolder holder, int position) { + public void onBindViewHolder(@NonNull ViewHolder holder, int position) { holder.init(position); } @Override public int getItemCount() { - return descriptions.size(); + return descriptions.size() + 1; } public List getDescriptions() { @@ -79,119 +105,159 @@ public List getDescriptions() { public void addDescription(Description description) { this.descriptions.add(description); - notifyItemInserted(descriptions.size() - 1); + notifyItemInserted(descriptions.size() + 1); } + public Title getTitle() { + return title; + } + + public void setTitle(Title title) { + this.title = title; + notifyItemInserted(0); + } public class ViewHolder extends RecyclerView.ViewHolder { + @Nullable @BindView(R.id.spinner_description_languages) AppCompatSpinner spinnerDescriptionLanguages; - @BindView(R.id.et_description_text) - EditText etDescriptionText; - private View view; + @BindView(R.id.description_item_edit_text) + EditText descItemEditText; + + private View view; public ViewHolder(View itemView) { super(itemView); ButterKnife.bind(this, itemView); this.view = itemView; + Timber.i("descItemEditText:" + descItemEditText); } public void init(int position) { - Description description = descriptions.get(position); - if (!TextUtils.isEmpty(description.getDescriptionText())) { - etDescriptionText.setText(description.getDescriptionText()); - } else { - etDescriptionText.setText(""); - } - Drawable drawableRight = context.getResources() - .getDrawable(R.drawable.mapbox_info_icon_default); - if (position != 0) { - etDescriptionText.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null); - } else { - etDescriptionText.setCompoundDrawablesWithIntrinsicBounds(null, null, drawableRight, null); - } - - etDescriptionText.addTextChangedListener(new TextWatcher() { - @Override - public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) { - - } - - @Override - public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) { - - } - - @Override - public void afterTextChanged(Editable editable) { - description.setDescriptionText(editable.toString()); + if (position == 0) { + Timber.d("Title is "+title); + if (!title.isEmpty()) { + descItemEditText.setText(title.toString()); + } else { + descItemEditText.setText(""); } - }); + Drawable drawableRight = context.getResources() + .getDrawable(R.drawable.mapbox_info_icon_default); + descItemEditText.setCompoundDrawablesWithIntrinsicBounds(null, null, drawableRight, null); + + descItemEditText.addTextChangedListener(new AbstractTextWatcher(titleText ->{ + title.setTitleText(titleText); + titleChangedSubject.onNext(titleText); + })); + + descItemEditText.setOnFocusChangeListener((v, hasFocus) -> { + if (!hasFocus) { + ViewUtil.hideKeyboard(v); + } + }); - etDescriptionText.setOnFocusChangeListener((v, hasFocus) -> { - if (!hasFocus) { - ViewUtil.hideKeyboard(v); + } else { + Description description = descriptions.get(position-1); + Timber.d("Description is "+description); + if (!TextUtils.isEmpty(description.getDescriptionText())) { + descItemEditText.setText(description.getDescriptionText()); + } else { + descItemEditText.setText(""); } - }); - - SpinnerLanguagesAdapter languagesAdapter = new SpinnerLanguagesAdapter(context, - R.layout.row_item_languages_spinner); - Collections.sort(languages, (language, t1) -> language.getLocale().getDisplayLanguage() - .compareTo(t1.getLocale().getDisplayLanguage().toString())); - languagesAdapter.setLanguages(languages); - languagesAdapter.notifyDataSetChanged(); - spinnerDescriptionLanguages.setAdapter(languagesAdapter); - - if (description.getSelectedLanguageIndex() == -1) { - if (position == 0) { - int defaultLocaleIndex = getIndexOfUserDefaultLocale(); - spinnerDescriptionLanguages.setSelection(defaultLocaleIndex); + if (position == 1) { + Drawable drawableRight = context.getResources() + .getDrawable(R.drawable.mapbox_info_icon_default); + descItemEditText.setCompoundDrawablesWithIntrinsicBounds(null, null, drawableRight, null); } else { - spinnerDescriptionLanguages.setSelection(0); + descItemEditText.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null); } - } else { - spinnerDescriptionLanguages.setSelection(description.getSelectedLanguageIndex()); - } - languages.get(spinnerDescriptionLanguages.getSelectedItemPosition()).setSet(true); - - //TODO do it the butterknife way - spinnerDescriptionLanguages.setOnItemSelectedListener(new OnItemSelectedListener() { - @Override - public void onItemSelected(AdapterView adapterView, View view, int position, - long l) { - //TODO handle case when user tries to select an already selected language - updateDescriptionBasedOnSelectedLanguageIndex(description, position); + descItemEditText.addTextChangedListener(new AbstractTextWatcher(description::setDescriptionText)); + descItemEditText.setOnFocusChangeListener((v, hasFocus) -> { + if (!hasFocus) { + ViewUtil.hideKeyboard(v); + } + }); + + SpinnerLanguagesAdapter languagesAdapter = new SpinnerLanguagesAdapter(context, + R.layout.row_item_languages_spinner, selectedLanguages); + languagesAdapter.notifyDataSetChanged(); + spinnerDescriptionLanguages.setAdapter(languagesAdapter); + + if (description.getSelectedLanguageIndex() == -1) { + if (position == 1) { + int defaultLocaleIndex = languagesAdapter.getIndexOfUserDefaultLocale(context); + spinnerDescriptionLanguages.setSelection(defaultLocaleIndex); + } else { + spinnerDescriptionLanguages.setSelection(0); + } + } else { + spinnerDescriptionLanguages.setSelection(description.getSelectedLanguageIndex()); + selectedLanguages.put(spinnerDescriptionLanguages, description.getLanguageCode()); } - @Override - public void onNothingSelected(AdapterView adapterView) { + //TODO do it the butterknife way + spinnerDescriptionLanguages.setOnItemSelectedListener(new OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView adapterView, View view, int position, + long l) { + description.setSelectedLanguageIndex(position); + String languageCode=((SpinnerLanguagesAdapter)adapterView.getAdapter()).getLanguageCode(position); + description.setLanguageCode(languageCode); + selectedLanguages.remove(adapterView); + selectedLanguages.put(adapterView, languageCode); + ((SpinnerLanguagesAdapter)adapterView.getAdapter()).selectedLangCode=languageCode; +// if(prevSpinnerItem!=null) +// prevSpinnerItem.setText(prevSpinnerItem.getText().subSequence(0,2)); +// prevSpinnerItem=(TextView)adapterView.getItemAtPosition(position); +// prevSpinnerItem.setText(languageCode); + } - } - }); + @Override + public void onNothingSelected(AdapterView adapterView) { + } + }); + } } - @OnTouch(R.id.et_description_text) + @Optional + @OnTouch(R.id.description_item_edit_text) boolean descriptionInfo(View view, MotionEvent motionEvent) { - + //Title info is visible only for the title if (getAdapterPosition() == 0) { - //Description info is visible only for the first item - final int value; if (ViewCompat.getLayoutDirection(view) == ViewCompat.LAYOUT_DIRECTION_LTR) { - value = etDescriptionText.getRight() - etDescriptionText + final int value = view.getRight() - descItemEditText .getCompoundDrawables()[2] .getBounds().width(); + if (motionEvent.getAction() == ACTION_UP && motionEvent.getRawX() >= value) { + callback.showAlert(R.string.media_detail_title, R.string.title_info); + return true; + } + } else { + final int value = descItemEditText.getLeft() + descItemEditText + .getCompoundDrawables()[0] + .getBounds().width(); + if (motionEvent.getAction() == ACTION_UP && motionEvent.getRawX() <= value) { + callback.showAlert(R.string.media_detail_title, R.string.title_info); + return true; + } + } + //Description info is visible only for the first description + }else if (getAdapterPosition() == 1) { + final int value; + if (ViewCompat.getLayoutDirection(view) == ViewCompat.LAYOUT_DIRECTION_LTR) { + value = view.getRight() - descItemEditText.getCompoundDrawables()[2].getBounds().width(); if (motionEvent.getAction() == ACTION_UP && motionEvent.getRawX() >= value) { callback.showAlert(R.string.media_detail_description, R.string.description_info); return true; } } else { - value = etDescriptionText.getLeft() + etDescriptionText + value = descItemEditText.getLeft() + descItemEditText .getCompoundDrawables()[0] .getBounds().width(); if (motionEvent.getAction() == ACTION_UP && motionEvent.getRawX() <= value) { @@ -205,27 +271,7 @@ boolean descriptionInfo(View view, MotionEvent motionEvent) { } } - private int getIndexOfUserDefaultLocale() { - for (int i = 0; i < languages.size(); i++) { - if (languages.get(i).getLocale() - .equals(context.getResources().getConfiguration().locale)) { - return i; - } - } - return 0; - } - - private void updateDescriptionBasedOnSelectedLanguageIndex(Description description, - int position) { - Language language = languages.get(position); - Locale locale = language.getLocale(); - description.setSelectedLanguageIndex(position); - description.setLanguageDisplayText(locale.getDisplayName()); - description.setLanguageId(locale.getLanguage()); - } - public interface Callback { - void showAlert(int mediaDetailDescription, int descriptionInfo); } } diff --git a/app/src/main/java/fr/free/nrw/commons/upload/DetectUnwantedPicturesAsync.java b/app/src/main/java/fr/free/nrw/commons/upload/DetectUnwantedPicturesAsync.java deleted file mode 100644 index 5a413e49a4..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/upload/DetectUnwantedPicturesAsync.java +++ /dev/null @@ -1,82 +0,0 @@ -package fr.free.nrw.commons.upload; - -import android.app.Activity; -import android.content.Intent; -import android.graphics.BitmapRegionDecoder; -import android.os.AsyncTask; -import android.support.v7.app.AlertDialog; - -import java.io.IOException; -import java.lang.ref.WeakReference; - -import fr.free.nrw.commons.R; -import fr.free.nrw.commons.contributions.ContributionsActivity; -import fr.free.nrw.commons.utils.ImageUtils; -import timber.log.Timber; - -/** - * Created by bluesir9 on 16/9/17. - * - *

Responsible for checking if the picture that the user is trying to upload is useful or not. Will attempt to filter - * away completely black,fuzzy/blurry pictures(for now). - * - *

todo: Detect selfies? - */ - -public class DetectUnwantedPicturesAsync extends AsyncTask { - - private final String imageMediaFilePath; - public final WeakReference activityWeakReference; - - DetectUnwantedPicturesAsync(WeakReference activityWeakReference, String imageMediaFilePath) { - //this.callback = callback; - this.imageMediaFilePath = imageMediaFilePath; - this.activityWeakReference = activityWeakReference; - } - - @Override - protected ImageUtils.Result doInBackground(Void... voids) { - try { - Timber.d("FilePath: " + imageMediaFilePath); - if (imageMediaFilePath == null) { - return ImageUtils.Result.IMAGE_OK; - } - - BitmapRegionDecoder decoder = BitmapRegionDecoder.newInstance(imageMediaFilePath,false); - - return ImageUtils.checkIfImageIsTooDark(decoder); - } catch (IOException ioe) { - Timber.e(ioe, "IO Exception"); - return ImageUtils.Result.IMAGE_OK; - } - } - - @Override - protected void onPostExecute(ImageUtils.Result result) { - super.onPostExecute(result); - Activity activity = activityWeakReference.get(); - - if (result != ImageUtils.Result.IMAGE_OK) { - //show appropriate error message - String errorMessage = result == ImageUtils.Result.IMAGE_DARK ? activity.getString(R.string.upload_image_too_dark) : activity.getString(R.string.upload_image_blurry); - AlertDialog.Builder errorDialogBuilder = new AlertDialog.Builder(activity); - errorDialogBuilder.setMessage(errorMessage); - errorDialogBuilder.setTitle(activity.getString(R.string.warning)); - errorDialogBuilder.setPositiveButton(activity.getString(R.string.no), (dialogInterface, i) -> { - //user does not wish to upload the picture, take them back to ContributionsActivity - Intent intent = new Intent(activity, ContributionsActivity.class); - dialogInterface.dismiss(); - activity.startActivity(intent); - }); - errorDialogBuilder.setNegativeButton(activity.getString(R.string.yes), (dialogInterface, i) -> { - //user wishes to go ahead with the upload of this picture, just dismiss this dialog - dialogInterface.dismiss(); - }); - - AlertDialog errorDialog = errorDialogBuilder.create(); - if (!activity.isFinishing()) { - errorDialog.show(); - } - } - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/upload/DexterPermissionObtainer.java b/app/src/main/java/fr/free/nrw/commons/upload/DexterPermissionObtainer.java new file mode 100644 index 0000000000..4e8fed5b1e --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/upload/DexterPermissionObtainer.java @@ -0,0 +1,170 @@ +package fr.free.nrw.commons.upload; + +import android.app.Activity; + +import com.karumi.dexter.Dexter; +import com.karumi.dexter.DexterBuilder; +import com.karumi.dexter.listener.PermissionDeniedResponse; +import com.karumi.dexter.listener.PermissionGrantedResponse; +import com.karumi.dexter.listener.single.BasePermissionListener; + +import fr.free.nrw.commons.R; +import fr.free.nrw.commons.utils.DialogUtil; +import fr.free.nrw.commons.utils.ExternalStorageUtils; +import fr.free.nrw.commons.utils.PermissionUtils; +import io.reactivex.Completable; +import io.reactivex.subjects.CompletableSubject; +import timber.log.Timber; + +public class DexterPermissionObtainer { + private final String TAG = "#MultipleShareActivity#"; + private final String requestedPermission; + private android.app.AlertDialog storagePermissionInfoDialog; + private DexterBuilder dexterStoragePermissionBuilder; + + private PermissionDeniedResponse permissionDeniedResponse; + + private boolean storagePromptInProgress; + + private final String rationaleTitle; + private final String rationaleText; + + private Activity activity; + + public CompletableSubject storagePromptObservable; + + /** + * @param activity The activity that is requesting the permission + * @param requestedPermission The permission being requested in the form of Manifest.permission.* + * @param rationaleTitle The title of the rationale dialog + * @param rationaleText The text inside the rationale dialog + */ + public DexterPermissionObtainer(Activity activity, String requestedPermission, String rationaleTitle, String rationaleText) { + this.activity = activity; + this.rationaleTitle = rationaleTitle; + this.rationaleText = rationaleText; + this.requestedPermission = requestedPermission; + this.storagePromptObservable = CompletableSubject.create(); + initPermissionsRationaleDialog(); + } + + /** + * Checks if storage permissions are obtained, prompts the users to grant storage permissions if necessary. + * When storage permission is present, onPermissionObtained is called. + */ + public Completable confirmStoragePermissions() { + if (ExternalStorageUtils.isStoragePermissionGranted(activity)) { + Timber.i("Storage permissions already granted."); + storagePromptObservable.onComplete(); + } else if (!storagePromptInProgress) { + if (storagePromptObservable.hasComplete()) { + storagePromptObservable = CompletableSubject.create(); + } + //If permission is not there, ask for it + storagePromptInProgress = true; + askDexterToHandleExternalStoragePermission(); + } + return storagePromptObservable; + } + + /* * + * Checks if storage permissions are obtained, prompts the users to grant storage permissions if necessary. + * When storage permission is present, onPermissionObtained is called. + * + public void confirmStoragePermissions() { + if (ExternalStorageUtils.isStoragePermissionGranted(activity)) { + Timber.i("Storage permissions already granted."); + onPermissionObtained.run(); + } else if (!storagePromptInProgress) { + //If permission is not there, ask for it + storagePromptInProgress = true; + askDexterToHandleExternalStoragePermission(); + } + //return storagePermissionReady;return null; + }*/ + + + /** + * To be called when the user returns to the original activity after manually enabling storage permissions. + */ + public void onManualPermissionReturned() { + //OnActivity result, no matter what the result is, our function can handle that. + askDexterToHandleExternalStoragePermission(); + } + + /** + * This method initialised the Dexter's permission builder (if not already initialised). Also makes sure that the builder is initialised + * only once, otherwise we would'nt know on which instance of it, the user is working on. And after the builder is initialised, it checks + * for the required permission and then handles the permission status, thanks to Dexter's appropriate callbacks. + */ + private void askDexterToHandleExternalStoragePermission() { + Timber.d(TAG, "External storage permission is being requested"); + if (null == dexterStoragePermissionBuilder) { + dexterStoragePermissionBuilder = Dexter.withActivity(activity) + .withPermission(requestedPermission) + .withListener(new BasePermissionListener() { + @Override + public void onPermissionGranted(PermissionGrantedResponse response) { + Timber.d(TAG, "User has granted us the permission for writing the external storage"); + //If permission is granted, well and good + storagePromptInProgress = false; + storagePromptObservable.onComplete(); + //onPermissionObtained.run(); + } + + @Override + public void onPermissionDenied(PermissionDeniedResponse response) { + Timber.d(TAG, "User has granted us the permission for writing the external storage"); + //If permission is not granted in whatsoever scenario, we show him a dialog stating why we need the permission + permissionDeniedResponse = response; + if (null != storagePermissionInfoDialog && !storagePermissionInfoDialog + .isShowing()) { + storagePermissionInfoDialog.show(); + } + } + }); + } + dexterStoragePermissionBuilder.check(); + } + + /** + * We have agreed to show a dialog showing why we need a particular permission. + * This method is used to initialise the dialog which is going to show the permission's rationale. + * The dialog is initialised along with a callback for positive and negative user actions. + */ + private void initPermissionsRationaleDialog() { + if (storagePermissionInfoDialog == null) { + storagePermissionInfoDialog = DialogUtil + .getAlertDialogWithPositiveAndNegativeCallbacks( + activity, + rationaleTitle, rationaleText, + R.drawable.ic_launcher, new DialogUtil.Callback() { + @Override + public void onPositiveButtonClicked() { + //If the user is willing to give us the permission + //But had somehow previously choose never ask again, we take him to app settings to manually enable permission + if (null == permissionDeniedResponse) { + //Dexter returned null, lets see if this ever happens + Timber.w("Dexter returned null as permissionDeniedResponse"); + } else if (permissionDeniedResponse.isPermanentlyDenied()) { + PermissionUtils.askUserToManuallyEnablePermissionFromSettings(activity); + Timber.i("Permission permanently denied."); + } else { + //or if we still have chance to show runtime permission dialog, we show him that. + askDexterToHandleExternalStoragePermission(); + Timber.d("Asking via Dexter for permission."); + } + } + + @Override + public void onNegativeButtonClicked() { + //This was the behaviour as of now, I was planning to maybe snack him with some message + //and then call finish after some time, or may be it could be associated with some action + // on the snack. If the user does not want us to give the permission, even after showing + // rationale dialog, lets not trouble him any more. + activity.finish(); + } + }); + } + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/upload/ExistingFileAsync.java b/app/src/main/java/fr/free/nrw/commons/upload/ExistingFileAsync.java deleted file mode 100644 index f74c408678..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/upload/ExistingFileAsync.java +++ /dev/null @@ -1,95 +0,0 @@ -package fr.free.nrw.commons.upload; - -import android.app.Activity; -import android.content.Context; -import android.content.Intent; -import android.os.AsyncTask; -import android.support.v7.app.AlertDialog; - -import java.io.IOException; -import java.lang.ref.WeakReference; - -import fr.free.nrw.commons.R; -import fr.free.nrw.commons.contributions.ContributionsActivity; -import fr.free.nrw.commons.mwapi.MediaWikiApi; -import timber.log.Timber; - -/** - * Sends asynchronous queries to the Commons MediaWiki API to check that file doesn't already exist - * Displays a warning to the user if the file already exists on Commons - */ -public class ExistingFileAsync extends AsyncTask { - - interface Callback { - void onResult(Result result); - } - - public enum Result { - NO_DUPLICATE, - DUPLICATE_PROCEED, - DUPLICATE_CANCELLED - } - - private final WeakReference activity; - private final MediaWikiApi api; - private final String fileSha1; - private final WeakReference context; - private final Callback callback; - - public ExistingFileAsync(WeakReference activity, String fileSha1, WeakReference context, Callback callback, MediaWikiApi mwApi) { - this.activity = activity; - this.fileSha1 = fileSha1; - this.context = context; - this.callback = callback; - this.api = mwApi; - } - - @Override - protected void onPreExecute() { - super.onPreExecute(); - } - - @Override - protected Boolean doInBackground(Void... voids) { - - // https://commons.wikimedia.org/w/api.php?action=query&list=allimages&format=xml&aisha1=801957214aba50cb63bb6eb1b0effa50188900ba - boolean fileExists; - try { - String fileSha1 = this.fileSha1; - fileExists = api.existingFile(fileSha1); - } catch (IOException e) { - Timber.e(e, "IO Exception: "); - return false; - } - - Timber.d("File already exists in Commons: %s", fileExists); - return fileExists; - } - - @Override - protected void onPostExecute(Boolean fileExists) { - super.onPostExecute(fileExists); - - // If file exists, display warning to user. - // Use soft warning for now (user able to choose to proceed) until have determined that implementation works without bugs - if (fileExists) { - AlertDialog.Builder builder = new AlertDialog.Builder(context.get()); - builder.setMessage(R.string.file_exists) - .setTitle(R.string.warning); - builder.setPositiveButton(R.string.no, (dialog, id) -> { - //Go back to ContributionsActivity - Intent intent = new Intent(context.get(), ContributionsActivity.class); - context.get().startActivity(intent); - callback.onResult(Result.DUPLICATE_CANCELLED); - }); - builder.setNegativeButton(R.string.yes, (dialog, id) -> callback.onResult(Result.DUPLICATE_PROCEED)); - - AlertDialog dialog = builder.create(); - if (!activity.get().isFinishing()) { - dialog.show(); - } - } else { - callback.onResult(Result.NO_DUPLICATE); - } - } -} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/upload/FileProcessor.java b/app/src/main/java/fr/free/nrw/commons/upload/FileProcessor.java index b29d686f5b..79aebf01f6 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/FileProcessor.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/FileProcessor.java @@ -1,21 +1,21 @@ 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.media.ExifInterface; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.ParcelFileDescriptor; +import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v7.app.AppCompatActivity; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; -import java.lang.ref.WeakReference; import java.util.Date; import java.util.List; @@ -44,89 +44,44 @@ public class FileProcessor implements SimilarImageDialogFragment.onResponse { @Inject @Named("default_preferences") SharedPreferences prefs; - private Uri mediaUri; + private String filePath; private ContentResolver contentResolver; private GPSExtractor imageObj; private Context context; private String decimalCoords; - private boolean haveCheckedForOtherImages = false; - private String filePath; + private ExifInterface exifInterface; private boolean useExtStorage; - private boolean cacheFound; + private boolean haveCheckedForOtherImages = false; private GPSExtractor tempImageObj; - FileProcessor(Uri mediaUri, ContentResolver contentResolver, Context context) { - this.mediaUri = mediaUri; + FileProcessor(@NonNull String filePath, ContentResolver contentResolver, Context context) { + this.filePath = filePath; 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; - } + try { + exifInterface=new ExifInterface(filePath); + } catch (IOException e) { + Timber.e(e); } - return filePath; + useExtStorage = prefs.getBoolean("useExternalStorage", true); } /** * Processes file coordinates, either from EXIF data or user location - * - * @param gpsEnabled if true use GPS */ - GPSExtractor processFileCoordinates(boolean gpsEnabled) { + GPSExtractor processFileCoordinates() { 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()); - } - } else { - String filePath = getPathOfMediaOrCopy(); - if (filePath != null) { - imageObj = new GPSExtractor(filePath); - } - } - - decimalCoords = imageObj.getCoords(); - if (decimalCoords == null || !imageObj.imageCoordsExists) { - //Find other photos taken around the same time which has gps coordinates - if (!haveCheckedForOtherImages) - findOtherImages();// Do not do repeat the process - } else { - useImageCoords(); - } - - } catch (FileNotFoundException e) { - Timber.w("File not found: " + mediaUri, e); + imageObj = new GPSExtractor(exifInterface); + decimalCoords = imageObj.getCoords(); + if (decimalCoords == null || !imageObj.imageCoordsExists) { + //Find other photos taken around the same time which has gps coordinates + if (!haveCheckedForOtherImages) + findOtherImages();// Do not do repeat the process + } else { + useImageCoords(); } + return imageObj; } @@ -136,10 +91,9 @@ String getDecimalCoords() { /** * Find other images around the same location that were taken within the last 20 sec - * */ private void findOtherImages() { - Timber.d("filePath" + getPathOfMediaOrCopy()); + Timber.d("filePath" + filePath); long timeOfCreation = new File(filePath).lastModified();//Time when the original image was created File folder = new File(filePath.substring(0, filePath.lastIndexOf('/'))); @@ -154,7 +108,7 @@ private void findOtherImages() { tempImageObj = null;//Temporary GPSExtractor to extract coords from these photos ParcelFileDescriptor descriptor = null; try { - descriptor = contentResolver.openFileDescriptor(Uri.parse(file.getAbsolutePath()), "r"); + descriptor = contentResolver.openFileDescriptor(Uri.fromFile(file), "r"); } catch (FileNotFoundException e) { e.printStackTrace(); } @@ -210,7 +164,6 @@ public void useImageCoords() { // 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()) @@ -223,7 +176,6 @@ public void useImageCoords() { ); 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); } @@ -232,20 +184,6 @@ public void useImageCoords() { } } - 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) context), imageMediaFilePath); - detectUnwantedPicturesAsync.execute(); - } - @Override public void onPositiveResponse() { imageObj = tempImageObj; diff --git a/app/src/main/java/fr/free/nrw/commons/upload/FileUtils.java b/app/src/main/java/fr/free/nrw/commons/upload/FileUtils.java index 8c9e563140..dfcbf1c6f1 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/FileUtils.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/FileUtils.java @@ -1,6 +1,7 @@ package fr.free.nrw.commons.upload; import android.annotation.SuppressLint; +import android.content.ContentResolver; import android.content.ContentUris; import android.content.Context; import android.content.SharedPreferences; @@ -12,6 +13,7 @@ import android.preference.PreferenceManager; import android.provider.DocumentsContract; import android.provider.MediaStore; +import android.provider.OpenableColumns; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.util.Log; @@ -34,6 +36,8 @@ import timber.log.Timber; +import static com.mapbox.mapboxsdk.Mapbox.getApplicationContext; + public class FileUtils { /** @@ -77,21 +81,32 @@ static String getSHA1(InputStream is) { /** * 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; - } + @NonNull + static String createExternalCopyPathAndCopy(Uri uri, ContentResolver contentResolver) throws IOException { + FileDescriptor fileDescriptor = contentResolver.openFileDescriptor(uri, "r").getFileDescriptor(); + String copyPath = Environment.getExternalStorageDirectory().toString() + "/CommonsApp/" + new Date().getTime() + "." + getFileExt(uri, contentResolver); + File newFile = new File(Environment.getExternalStorageDirectory().toString() + "/CommonsApp"); + newFile.mkdir(); + FileUtils.copy(fileDescriptor, copyPath); + Timber.d("Filepath (copied): %s", copyPath); + return copyPath; + } + + /** + * 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 + */ + @NonNull + static String createCopyPathAndCopy(Uri uri, Context context) throws IOException { + FileDescriptor fileDescriptor = context.getContentResolver().openFileDescriptor(uri, "r").getFileDescriptor(); + String copyPath = context.getCacheDir().getAbsolutePath() + "/" + new Date().getTime() + "." + getFileExt(uri, context.getContentResolver()); + FileUtils.copy(fileDescriptor, copyPath); + Timber.d("Filepath (copied): %s", copyPath); + return copyPath; } /** @@ -122,13 +137,13 @@ public static String getPath(Context context, Uri uri) { if ("primary".equalsIgnoreCase(type)) { returnPath = Environment.getExternalStorageDirectory() + "/" + split[1]; } - } else if (isDownloadsDocument(uri)) { // DownloadsProvider + } else if (isDownloadsDocument(uri)) { // DownloadsProvider final String id = DocumentsContract.getDocumentId(uri); final Uri contentUri = ContentUris.withAppendedId( Uri.parse("content://downloads/document"), Long.valueOf(id)); - returnPath = getDataColumn(context, contentUri, null, null); + returnPath = getDataColumn(context, contentUri, null, null); } else if (isMediaDocument(uri)) { // MediaProvider final String docId = DocumentsContract.getDocumentId(uri); @@ -167,7 +182,7 @@ else if ("file".equalsIgnoreCase(uri.getScheme())) { returnPath = uri.getPath(); } - if(returnPath == null) { + if (returnPath == null) { //fetching path may fail depending on the source URI and all hope is lost //so we will create and use a copy of the file, which seems to work String copyPath = null; @@ -305,6 +320,7 @@ public static void copy(@NonNull FileDescriptor source, @NonNull String destinat /** * 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 */ @@ -331,6 +347,7 @@ public static String readFromResource(String fileName) throws IOException { /** * Deletes files. + * * @param file context */ public static boolean deleteFile(File file) { @@ -356,7 +373,7 @@ public static File createAndGetAppLogsFile(String logs) { commonsAppDirectory.mkdir(); } - File logsFile = new File(commonsAppDirectory,"logs.txt"); + File logsFile = new File(commonsAppDirectory, "logs.txt"); if (logsFile.exists()) { //old logs file is useless logsFile.delete(); @@ -378,4 +395,39 @@ public static File createAndGetAppLogsFile(String logs) { } } + public static String getFilename(Uri uri, ContentResolver contentResolver) { + if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.JELLY_BEAN) + return ""; + String result = null; + if (uri.getScheme().equals("content")) { + try (Cursor cursor = contentResolver.query(uri, null, null, null, null)) { + if (cursor != null && cursor.moveToFirst()) { + result = cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)); + } + } + } + if (result == null) { + result = uri.getPath(); + int cut = result.lastIndexOf('/'); + if (cut != -1) { + result = result.substring(cut + 1); + } + } + return result; + } + + public static String getFileExt(String fileName){ + //Default file extension + String extension=".jpg"; + + int i = fileName.lastIndexOf('.'); + if (i > 0) { + extension = fileName.substring(i+1); + } + return extension; + } + + public static String getFileExt(Uri uri, ContentResolver contentResolver) { + return getFileExt(getFilename(uri, contentResolver)); + } } \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/upload/GPSExtractor.java b/app/src/main/java/fr/free/nrw/commons/upload/GPSExtractor.java index e45b31f051..a6b150c42d 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/GPSExtractor.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/GPSExtractor.java @@ -16,11 +16,22 @@ */ public class GPSExtractor { - private ExifInterface exif; + public static final GPSExtractor DUMMY= new GPSExtractor(); private double decLatitude; private double decLongitude; public boolean imageCoordsExists; + private String latitude; + private String longitude; + private String latitudeRef; + private String longitudeRef; + private String decimalCoords; + /** + * Dummy constructor. + */ + private GPSExtractor(){ + + } /** * Construct from the file descriptor of the image (only for API 24 or newer). * @param fileDescriptor the file descriptor of the image @@ -28,7 +39,8 @@ public class GPSExtractor { @RequiresApi(24) public GPSExtractor(@NonNull FileDescriptor fileDescriptor) { try { - exif = new ExifInterface(fileDescriptor); + ExifInterface exif = new ExifInterface(fileDescriptor); + processCoords(exif); } catch (IOException | IllegalArgumentException e) { Timber.w(e); } @@ -41,29 +53,26 @@ public GPSExtractor(@NonNull FileDescriptor fileDescriptor) { */ public GPSExtractor(@NonNull String path) { try { - exif = new ExifInterface(path); + ExifInterface exif = new ExifInterface(path); + processCoords(exif); } catch (IOException | IllegalArgumentException e) { Timber.w(e); } } /** - * Extracts geolocation (either of image from EXIF data, or of user) - * @return coordinates as string (needs to be passed as a String in API query) + * Construct from the file path of the image. + * @param exif exif interface of the image + * */ - @Nullable - public String getCoords() { - String latitude; - String longitude; - String latitudeRef; - String longitudeRef; - String decimalCoords; + public GPSExtractor(@NonNull ExifInterface exif){ + processCoords(exif); + } + private void processCoords(ExifInterface exif){ //If image has no EXIF data and user has enabled GPS setting, get user's location - //TODO: Always return null as a temporary fix for #1599 - if (exif == null || exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE) == null) { - return null; - } else { + //Always return null as a temporary fix for #1599 + if (exif != null && exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE) != null) { //If image has EXIF data, extract image coords imageCoordsExists = true; Timber.d("EXIF data has location info"); @@ -72,16 +81,25 @@ public String getCoords() { latitudeRef = exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE_REF); longitude = exif.getAttribute(ExifInterface.TAG_GPS_LONGITUDE); longitudeRef = exif.getAttribute(ExifInterface.TAG_GPS_LONGITUDE_REF); + } + } - if (latitude!=null && latitudeRef!=null && longitude!=null && longitudeRef!=null) { - Timber.d("Latitude: %s %s", latitude, latitudeRef); - Timber.d("Longitude: %s %s", longitude, longitudeRef); - - decimalCoords = getDecimalCoords(latitude, latitudeRef, longitude, longitudeRef); - return decimalCoords; - } else { - return null; - } + /** + * Extracts geolocation (either of image from EXIF data, or of user) + * @return coordinates as string (needs to be passed as a String in API query) + */ + @Nullable + public String getCoords() { + if(decimalCoords!=null){ + return decimalCoords; + }else if (latitude!=null && latitudeRef!=null && longitude!=null && longitudeRef!=null) { + Timber.d("Latitude: %s %s", latitude, latitudeRef); + Timber.d("Longitude: %s %s", longitude, longitudeRef); + + decimalCoords = getDecimalCoords(latitude, latitudeRef, longitude, longitudeRef); + return decimalCoords; + } else { + return null; } } diff --git a/app/src/main/java/fr/free/nrw/commons/upload/HeightLimitedRecyclerView.java b/app/src/main/java/fr/free/nrw/commons/upload/HeightLimitedRecyclerView.java new file mode 100644 index 0000000000..ff100e16e8 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/upload/HeightLimitedRecyclerView.java @@ -0,0 +1,52 @@ +package fr.free.nrw.commons.upload; + +import android.app.Activity; +import android.content.Context; +import android.graphics.Point; +import android.support.annotation.Nullable; +import android.support.v7.widget.RecyclerView; +import android.util.AttributeSet; +import android.util.DisplayMetrics; +import android.view.Display; + +/** + * Created by Ilgaz Er on 8/7/2018. + */ +public class HeightLimitedRecyclerView extends RecyclerView { + + int height; + + + public HeightLimitedRecyclerView(Context context) { + super(context); + DisplayMetrics displayMetrics = new DisplayMetrics(); + ((Activity) getContext()).getWindowManager() + .getDefaultDisplay() + .getMetrics(displayMetrics); + height=displayMetrics.heightPixels; + } + + public HeightLimitedRecyclerView(Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + DisplayMetrics displayMetrics = new DisplayMetrics(); + ((Activity) getContext()).getWindowManager() + .getDefaultDisplay() + .getMetrics(displayMetrics); + height=displayMetrics.heightPixels; + } + + public HeightLimitedRecyclerView(Context context, @Nullable AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + DisplayMetrics displayMetrics = new DisplayMetrics(); + ((Activity) getContext()).getWindowManager() + .getDefaultDisplay() + .getMetrics(displayMetrics); + height=displayMetrics.heightPixels; + } + + @Override + protected void onMeasure(int widthSpec, int heightSpec) { + heightSpec = MeasureSpec.makeMeasureSpec((int) (height*0.3), MeasureSpec.AT_MOST); + super.onMeasure(widthSpec, heightSpec); + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/upload/Language.java b/app/src/main/java/fr/free/nrw/commons/upload/Language.java index 8d4b272397..85e5a53691 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/Language.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/Language.java @@ -2,8 +2,34 @@ import java.util.Locale; +import fr.free.nrw.commons.R; + class Language { + public static int[] languageGroups={R.string.desc_language_Worldwide, + R.string.desc_language_America, + R.string.desc_language_Europe, + R.string.desc_language_Middle_East, + R.string.desc_language_Africa, + R.string.desc_language_Asia, + R.string.desc_language_Pacific }; + public static int[] languageNames={R.array.desc_languages_Worldwide, + R.array.desc_languages_America, + R.array.desc_languages_Europe, + R.array.desc_languages_Middle_East, + R.array.desc_languages_Africa, + R.array.desc_languages_Asia, + R.array.desc_languages_Pacific + }; + public static int[] languageCodes={R.array.desc_language_codes_Worldwide, + R.array.desc_language_codes_America, + R.array.desc_language_codes_Europe, + R.array.desc_language_codes_Middle_East, + R.array.desc_language_codes_Africa, + R.array.desc_language_codes_Asia, + R.array.desc_language_codes_Pacific + }; + private Locale locale; private boolean isSet = false; diff --git a/app/src/main/java/fr/free/nrw/commons/upload/MultipleShareActivity.java b/app/src/main/java/fr/free/nrw/commons/upload/MultipleShareActivity.java deleted file mode 100644 index e429c3ee80..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/upload/MultipleShareActivity.java +++ /dev/null @@ -1,404 +0,0 @@ -package fr.free.nrw.commons.upload; - -import android.Manifest; -import android.app.ProgressDialog; -import android.content.ContentResolver; -import android.content.Context; -import android.content.Intent; -import android.content.SharedPreferences; -import android.content.pm.PackageManager; -import android.database.DataSetObserver; -import android.net.Uri; -import android.os.Build; -import android.os.Bundle; -import android.os.ParcelFileDescriptor; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.v4.app.ActivityCompat; -import android.support.v4.app.FragmentManager; -import android.support.v4.content.ContextCompat; -import android.view.MenuItem; -import android.view.View; -import android.view.inputmethod.InputMethodManager; -import android.widget.AdapterView; -import android.widget.Toast; - -import java.io.FileNotFoundException; -import java.util.ArrayList; -import java.util.List; - -import javax.inject.Inject; -import javax.inject.Named; - -import butterknife.ButterKnife; -import fr.free.nrw.commons.BuildConfig; -import fr.free.nrw.commons.Media; -import fr.free.nrw.commons.R; -import fr.free.nrw.commons.auth.AuthenticatedActivity; -import fr.free.nrw.commons.auth.SessionManager; -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.media.MediaDetailPagerFragment; -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.mwapi.MediaWikiApi; -import fr.free.nrw.commons.utils.ContributionUtils; -import fr.free.nrw.commons.utils.ExternalStorageUtils; -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, - FragmentManager.OnBackStackChangedListener, - MultipleUploadListFragment.OnMultipleUploadInitiatedHandler, - OnCategoriesSaveHandler, - ActivityCompat.OnRequestPermissionsResultCallback{ - - @Inject - MediaWikiApi mwApi; - @Inject - SessionManager sessionManager; - @Inject - UploadController uploadController; - @Inject - ModifierSequenceDao modifierSequenceDao; - @Inject - @Named("default_preferences") - SharedPreferences prefs; - - private ArrayList photosList = null; - - private MultipleUploadListFragment uploadsList; - private MediaDetailPagerFragment mediaDetails; - private CategorizationFragment categorizationFragment; - - private boolean locationPermitted = false; - private boolean isMultipleUploadsPrepared = false; - private boolean isMultipleUploadsFinalised = false; // Checks is user clicked to upload button or regret before this phase - - @Override - public Media getMediaAtPosition(int i) { - return photosList.get(i); - } - - @Override - public int getTotalMediaCount() { - if (photosList == null) { - return 0; - } - return photosList.size(); - } - - @Override - public void notifyDatasetChanged() { - if (uploadsList != null) { - uploadsList.notifyDatasetChanged(); - } - } - - @Override - public void registerDataSetObserver(DataSetObserver observer) { - // fixme implement me if needed - } - - @Override - public void unregisterDataSetObserver(DataSetObserver observer) { - // fixme implement me if needed - } - - @Override - public void onItemClick(AdapterView adapterView, View view, int index, long item) { - showDetail(index); - } - - @Override - public void OnMultipleUploadInitiated() { - // No need to request external permission here, because if user can reach this point, then she permission granted - Timber.d("OnMultipleUploadInitiated"); - multipleUploadBegins(); - } - - @Override - public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { - if (requestCode == 1 && grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - Timber.d("onRequestPermissionsResult external storage permission granted"); - prepareMultipleUpoadList(); - } else { - // Permission is not granted, close activity - finish(); - } - } - - private void multipleUploadBegins() { - - Timber.d("Multiple upload begins"); - final ProgressDialog dialog = new ProgressDialog(this); - dialog.setIndeterminate(false); - dialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); - dialog.setMax(photosList.size()); - dialog.setTitle(getResources().getQuantityString(R.plurals.starting_multiple_uploads, photosList.size(), photosList.size())); - dialog.show(); - - for (int i = 0; i < photosList.size(); i++) { - Contribution up = photosList.get(i); - final int uploadCount = i + 1; // Goddamn Java - - uploadController.startUpload(up, contribution -> { - dialog.setProgress(uploadCount); - if (uploadCount == photosList.size()) { - dialog.dismiss(); - Toast startingToast = Toast.makeText(this, R.string.uploading_started, Toast.LENGTH_LONG); - startingToast.show(); - } - }); - } - - uploadsList.setImageOnlyMode(true); - - categorizationFragment = (CategorizationFragment) getSupportFragmentManager().findFragmentByTag("categorization"); - if (categorizationFragment == null) { - categorizationFragment = new CategorizationFragment(); - } - // FIXME: Stops the keyboard from being shown 'stale' while moving out of this fragment into the next - 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() - .add(R.id.uploadsFragmentContainer, categorizationFragment, "categorization") - .commitAllowingStateLoss(); - isMultipleUploadsFinalised = true; - //See http://stackoverflow.com/questions/7469082/getting-exception-illegalstateexception-can-not-perform-this-action-after-onsa - } - - @Override - public void onCategoriesSave(List categories) { - if (categories.size() > 0) { - for (Contribution contribution : photosList) { - ModifierSequence categoriesSequence = new ModifierSequence(contribution.getContentUri()); - - categoriesSequence.queueModifier(new CategoryModifier(categories.toArray(new String[]{}))); - categoriesSequence.queueModifier(new TemplateRemoveModifier("Uncategorized")); - - modifierSequenceDao.save(categoriesSequence); - } - } - // FIXME: Make sure that the content provider is up - // This is the wrong place for it, but bleh - better than not having it turned on by default for people who don't go throughl ogin - ContentResolver.setSyncAutomatically(sessionManager.getCurrentAccount(), BuildConfig.MODIFICATION_AUTHORITY, true); // Enable sync by default! - finish(); - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case android.R.id.home: - if (mediaDetails.isVisible()) { - getSupportFragmentManager().popBackStack(); - } - return true; - } - return super.onOptionsItemSelected(item); - } - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - setContentView(R.layout.activity_multiple_uploads); - ButterKnife.bind(this); - initDrawer(); - - if (savedInstanceState != null) { - photosList = savedInstanceState.getParcelableArrayList("uploadsList"); - } - - getSupportFragmentManager().addOnBackStackChangedListener(this); - requestAuthToken(); - - //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 && ContextCompat.checkSelfPermission(this,Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) { - { - locationPermitted = true; - } - } - } - - @Override - protected void onDestroy() { - super.onDestroy(); - getSupportFragmentManager().removeOnBackStackChangedListener(this); - uploadController.cleanup(); - } - - private void showDetail(int i) { - if (mediaDetails == null || !mediaDetails.isVisible()) { - mediaDetails = new MediaDetailPagerFragment(true, false); - getSupportFragmentManager() - .beginTransaction() - .replace(R.id.uploadsFragmentContainer, mediaDetails) - .addToBackStack(null) - .commit(); - getSupportFragmentManager().executePendingTransactions(); - } - mediaDetails.showImage(i); - } - - @Override - protected void onSaveInstanceState(Bundle outState) { - /* This will be true if permission request is granted before we request. Otherwise we will - * explicitly call operations under this method again. - */ - if (isMultipleUploadsPrepared) { - super.onSaveInstanceState(outState); - Timber.d("onSaveInstanceState multiple uploads is prepared, permission granted"); - outState.putParcelableArrayList("uploadsList", photosList); - } else { - Timber.d("onSaveInstanceState multiple uploads is not prepared, permission not granted"); - return; - } - } - - @Override - protected void onAuthCookieAcquired(String authCookie) { - // Multiple uploads prepared boolean is used to decide when to call multipleUploadsBegin() - isMultipleUploadsFinalised = false; - isMultipleUploadsPrepared = false; - mwApi.setAuthCookie(authCookie); - if (!ExternalStorageUtils.isStoragePermissionGranted(this)) { - ExternalStorageUtils.requestExternalStoragePermission(this); - isMultipleUploadsPrepared = false; - return; // Postpone operation to do after gettion permission - } else { - isMultipleUploadsPrepared = true; - prepareMultipleUpoadList(); - } - } - - /** - * Prepares a list from files will be uploaded. Saves these files temporarily to external - * storage. Adds them to uploads list - */ - private void prepareMultipleUpoadList() { - Intent intent = getIntent(); - - if (Intent.ACTION_SEND_MULTIPLE.equals(intent.getAction())) { - if (photosList == null) { - photosList = new ArrayList<>(); - ArrayList urisList = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM); - for (int i = 0; i < urisList.size(); i++) { - Contribution up = new Contribution(); - Uri uri = urisList.get(i); - // Use temporarily saved file Uri instead - uri = ContributionUtils.saveFileBeingUploadedTemporarily(this, uri); - up.setLocalUri(uri); - up.setTag("mimeType", intent.getType()); - up.setTag("sequence", i); - up.setSource(Contribution.SOURCE_EXTERNAL); - up.setMultiple(true); - String imageGpsCoordinates = extractImageGpsData(uri); - if (imageGpsCoordinates != null) { - Timber.d("GPS data for image found!"); - up.setDecimalCoords(imageGpsCoordinates); - } - photosList.add(up); - } - } - - uploadsList = (MultipleUploadListFragment) getSupportFragmentManager().findFragmentByTag("uploadsList"); - if (uploadsList == null) { - uploadsList = new MultipleUploadListFragment(); - getSupportFragmentManager() - .beginTransaction() - .add(R.id.uploadsFragmentContainer, uploadsList, "uploadsList") - .commit(); - } - setTitle(getResources().getQuantityString(R.plurals.multiple_uploads_title, photosList.size(), photosList.size())); - uploadController.prepareService(); - } - } - - @Override - protected void onAuthFailure() { - Toast failureToast = Toast.makeText(this, R.string.authentication_failed, Toast.LENGTH_LONG); - failureToast.show(); - finish(); - } - - @Override - public void onBackStackChanged() { - getSupportActionBar().setDisplayHomeAsUpEnabled(mediaDetails != null && mediaDetails.isVisible()); - } - - /** - * Will attempt to extract the gps coordinates using exif data or by using the current - * location if available for the image who's imageUri has been provided. - * @param imageUri The uri of the image who's GPS coordinates data we wish to extract - * @return GPS coordinates as a String as is returned by {@link GPSExtractor} - */ - @Nullable - private String extractImageGpsData(Uri imageUri) { - Timber.d("Entering extractImagesGpsData"); - - if (imageUri == null) { - //now why would you do that??? - return null; - } - - GPSExtractor gpsExtractor = null; - - try { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - ParcelFileDescriptor fd = getContentResolver().openFileDescriptor(imageUri,"r"); - if (fd != null) { - gpsExtractor = new GPSExtractor(fd.getFileDescriptor()); - } - } else { - String filePath = FileUtils.getPath(this,imageUri); - if (filePath != null) { - gpsExtractor = new GPSExtractor(filePath); - } - } - - if (gpsExtractor != null) { - //get image coordinates from exif data or user location - return gpsExtractor.getCoords(); - } - - } catch (FileNotFoundException fnfe) { - Timber.w(fnfe); - return null; - } - - return null; - } - - // If on back pressed before sharing - @Override - public void onBackPressed() { - super.onBackPressed(); - } - - @Override - protected void onStop() { - // Remove saved files if activity is stopped before upload operation, ie user changed mind - if (!isMultipleUploadsFinalised) { - if (photosList != null) { - for (Contribution contribution : photosList) { - Timber.d("User changed mind, didn't click to upload button, deleted file: "+contribution.getLocalUri()); - ContributionUtils.removeTemporaryFile(contribution.getLocalUri()); - } - } - } - super.onStop(); - } -} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/upload/MultipleUploadListFragment.java b/app/src/main/java/fr/free/nrw/commons/upload/MultipleUploadListFragment.java deleted file mode 100644 index f38862da27..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/upload/MultipleUploadListFragment.java +++ /dev/null @@ -1,254 +0,0 @@ -package fr.free.nrw.commons.upload; - -import android.content.Context; -import android.graphics.Point; -import android.net.Uri; -import android.os.Bundle; -import android.support.graphics.drawable.VectorDrawableCompat; -import android.support.v4.app.Fragment; -import android.text.Editable; -import android.text.TextUtils; -import android.text.TextWatcher; -import android.util.DisplayMetrics; -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.widget.AdapterView; -import android.widget.BaseAdapter; -import android.widget.EditText; -import android.widget.FrameLayout; -import android.widget.GridView; -import android.widget.RelativeLayout; -import android.widget.TextView; -import android.widget.Toast; - -import butterknife.BindView; -import butterknife.ButterKnife; -import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder; -import com.facebook.drawee.view.SimpleDraweeView; - -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 { - - public interface OnMultipleUploadInitiatedHandler { - void OnMultipleUploadInitiated(); - } - - @BindView(R.id.multipleShareBackground) - GridView photosGrid; - - @BindView(R.id.multipleBaseTitle) - EditText baseTitle; - - private PhotoDisplayAdapter photosAdapter; - private TitleTextWatcher textWatcher = new TitleTextWatcher(); - - private Point photoSize; - private MediaDetailPagerFragment.MediaDetailProvider detailProvider; - private OnMultipleUploadInitiatedHandler multipleUploadInitiatedHandler; - - private boolean imageOnlyMode; - - private static class UploadHolderView { - private Uri imageUri; - private SimpleDraweeView image; - private TextView title; - private RelativeLayout overlay; - } - - @Override - public void onAttach(Context context) { - AndroidSupportInjection.inject(this); - super.onAttach(context); - } - - private class PhotoDisplayAdapter extends BaseAdapter { - - @Override - public int getCount() { - return detailProvider.getTotalMediaCount(); - } - - @Override - public Object getItem(int i) { - return detailProvider.getMediaAtPosition(i); - } - - @Override - public long getItemId(int i) { - return i; - } - - @Override - public View getView(int i, View view, ViewGroup viewGroup) { - UploadHolderView holder; - - if (view == null) { - view = LayoutInflater.from(getContext()).inflate(R.layout.layout_upload_item, viewGroup, false); - holder = new UploadHolderView(); - 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 - .newInstance(getResources()) - .setPlaceholderImage(VectorDrawableCompat.create(getResources(), - R.drawable.ic_image_black_24dp, getContext().getTheme())) - .setFailureImage(VectorDrawableCompat.create(getResources(), - R.drawable.ic_error_outline_black_24dp, getContext().getTheme())) - .build()); - view.setTag(holder); - } else { - holder = (UploadHolderView) view.getTag(); - } - - Contribution up = (Contribution) this.getItem(i); - - if (holder.imageUri == null || !holder.imageUri.equals(up.getLocalUri())) { - holder.image.setImageURI(up.getLocalUri().toString()); - holder.imageUri = up.getLocalUri(); - } - - if (!imageOnlyMode) { - holder.overlay.setVisibility(View.VISIBLE); - holder.title.setText(up.getFilename()); - } else { - holder.overlay.setVisibility(View.GONE); - } - - return view; - } - } - - @Override - public void onStop() { - super.onStop(); - - // FIXME: Stops the keyboard from being shown 'stale' while moving out of this fragment into the next - View target = getActivity().getCurrentFocus(); - ViewUtil.hideKeyboard(target); - } - - // FIXME: Wrong result type - private Point calculatePicDimension(int count) { - DisplayMetrics screenMetrics = getResources().getDisplayMetrics(); - int screenWidth = screenMetrics.widthPixels; - int screenHeight = screenMetrics.heightPixels; - - int picWidth = Math.min((int) Math.sqrt(screenWidth * screenHeight / count), screenWidth); - picWidth = Math.min((int) (192 * screenMetrics.density), Math.max((int) (120 * screenMetrics.density), picWidth / 48 * 48)); - int picHeight = Math.min(picWidth, (int) (192 * screenMetrics.density)); // Max Height is same as Contributions list - - return new Point(picWidth, picHeight); - } - - public void notifyDatasetChanged() { - if (photosAdapter != null) { - photosAdapter.notifyDataSetChanged(); - } - } - - public void setImageOnlyMode(boolean mode) { - imageOnlyMode = mode; - if (imageOnlyMode) { - baseTitle.setVisibility(View.GONE); - } else { - baseTitle.setVisibility(View.VISIBLE); - } - photosAdapter.notifyDataSetChanged(); - photosGrid.setEnabled(!mode); - } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - View view = inflater.inflate(R.layout.fragment_multiple_uploads_list, container, false); - ButterKnife.bind(this,view); - photosAdapter = new PhotoDisplayAdapter(); - photosGrid.setAdapter(photosAdapter); - photosGrid.setOnItemClickListener((AdapterView.OnItemClickListener) getActivity()); - photoSize = calculatePicDimension(detailProvider.getTotalMediaCount()); - photosGrid.setColumnWidth(photoSize.x); - - baseTitle.addTextChangedListener(textWatcher); - - baseTitle.setOnFocusChangeListener((v, hasFocus) -> { - if (!hasFocus) { - ViewUtil.hideKeyboard(v); - } - }); - - return view; - } - - @Override - public void onDestroyView() { - baseTitle.removeTextChangedListener(textWatcher); - super.onDestroyView(); - } - - @Override - public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { - super.onCreateOptionsMenu(menu, inflater); - menu.clear(); - inflater.inflate(R.menu.fragment_multiple_upload_list, menu); - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case R.id.menu_upload_multiple: - if (baseTitle.getText().toString().trim().isEmpty()) { - Toast.makeText(getContext(), R.string.add_set_name_toast, Toast.LENGTH_LONG).show(); - return false; - } - multipleUploadInitiatedHandler.OnMultipleUploadInitiated(); - return true; - } - return super.onOptionsItemSelected(item); - } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - detailProvider = (MediaDetailPagerFragment.MediaDetailProvider) getActivity(); - multipleUploadInitiatedHandler = (OnMultipleUploadInitiatedHandler) getActivity(); - - setHasOptionsMenu(true); - } - - private class TitleTextWatcher implements TextWatcher { - @Override - public void beforeTextChanged(CharSequence charSequence, int i1, int i2, int i3) { - } - - @Override - public void onTextChanged(CharSequence charSequence, int i1, int i2, int i3) { - for (int i = 0; i < detailProvider.getTotalMediaCount(); i++) { - Contribution up = (Contribution) detailProvider.getMediaAtPosition(i); - Boolean isDirty = (Boolean) up.getTag("isDirty"); - if (isDirty == null || !isDirty) { - if (!TextUtils.isEmpty(charSequence)) { - up.setFilename(charSequence.toString() + " - " + ((Integer) up.getTag("sequence") + 1)); - } else { - up.setFilename(""); - } - } - } - detailProvider.notifyDatasetChanged(); - } - - @Override - public void afterTextChanged(Editable editable) { - } - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/upload/ShareActivity.java b/app/src/main/java/fr/free/nrw/commons/upload/ShareActivity.java deleted file mode 100644 index e2921edb97..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/upload/ShareActivity.java +++ /dev/null @@ -1,670 +0,0 @@ -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.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.support.annotation.NonNull; -import android.support.annotation.RequiresApi; -import android.support.design.widget.FloatingActionButton; -import android.support.graphics.drawable.VectorDrawableCompat; -import android.support.v4.app.ActivityCompat; -import android.support.v4.content.ContextCompat; - -import android.view.KeyEvent; -import android.view.MenuItem; -import android.view.View; -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.FileInputStream; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStream; -import java.lang.ref.WeakReference; -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.BuildConfig; -import fr.free.nrw.commons.R; -import fr.free.nrw.commons.auth.AuthenticatedActivity; -import fr.free.nrw.commons.auth.LoginActivity; -import fr.free.nrw.commons.auth.SessionManager; -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.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.mwapi.CategoryApi; -import fr.free.nrw.commons.mwapi.MediaWikiApi; -import fr.free.nrw.commons.utils.ContributionUtils; -import fr.free.nrw.commons.utils.ExternalStorageUtils; -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; -import static fr.free.nrw.commons.wikidata.WikidataConstants.WIKIDATA_ENTITY_ID_PREF; - -/** - * Activity for the title/desc screen after image is selected. Also starts processing image - * GPS coordinates or user location (if enabled in Settings) for category suggestions. - */ -public class ShareActivity - extends AuthenticatedActivity - implements SingleUploadFragment.OnUploadActionInitiated, - OnCategoriesSaveHandler, - ActivityCompat.OnRequestPermissionsResultCallback { - - private static final int REQUEST_PERM_ON_SUBMIT_STORAGE = 4; - //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 - CacheController cacheController; - @Inject - SessionManager sessionManager; - @Inject - UploadController uploadController; - @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 Uri contentProviderUri; - private Contribution contribution; - 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 boolean duplicateCheckPassed = false; - private boolean isNearbyUpload = false; - private Animator CurrentAnimator; - private long ShortAnimationDuration; - private boolean isFABOpen = false; - private float startScaleFinal; - private Bundle savedInstanceState; - private boolean isUploadFinalised = false; // Checks is user clicked to upload button or regret before this phase - private boolean isZoom = false; - - - - /** - * Called when user taps the submit button. - * Requests Storage permission, if needed. - */ - - @Override - public void uploadActionInitiated(String title, String description) { - - this.title = title; - this.description = description; - - - if (sessionManager.getCurrentAccount() != null) { - 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); - } else { - uploadBegins(); - } - } else { - uploadBegins(); - } - } - else //Send user to login activity - { - Toast.makeText(this, "You need to login first!", Toast.LENGTH_SHORT).show(); - Intent loginIntent = new Intent(ShareActivity.this, LoginActivity.class); - startActivity(loginIntent); - } - } - - /** - * 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() { - return !FileUtils.isSelfOwned(getApplicationContext(), mediaUri) - && (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) - != PackageManager.PERMISSION_GRANTED); - //return false; - } - - - /** - * Called after permission checks are done. - * Gets file metadata for category suggestions, displays toast, caches categories found, calls uploadController - */ - - private void uploadBegins() { - fileObj.processFileCoordinates(locationPermitted); - - Toast startingToast = Toast.makeText(this, R.string.uploading_started, Toast.LENGTH_LONG); - startingToast.show(); - - if (!fileObj.isCacheFound()) { - //Has to be called after apiCall.request() - cacheController.cacheCategory(); - Timber.d("Cache the categories found"); - } - - uploadController.startUpload(title, contentProviderUri, mediaUri, description, mimeType, source, decimalCoords, wikiDataEntityId, c -> { - ShareActivity.this.contribution = c; - showPostUpload(); - }); - isUploadFinalised = true; - } - - /** - * Starts CategorizationFragment after uploadBegins. - */ - - private void showPostUpload() { - if (categorizationFragment == null) { - categorizationFragment = new CategorizationFragment(); - } - getSupportFragmentManager().beginTransaction() - .replace(R.id.single_upload_fragment_container, categorizationFragment, "categorization") - .commit(); - } - - /** - * Send categories to modifications queue after they are selected - * - * @param categories categories selected - */ - @Override - public void onCategoriesSave(List categories) { - if (categories.size() > 0) { - ModifierSequence categoriesSequence = new ModifierSequence(contribution.getContentUri()); - - categoriesSequence.queueModifier(new CategoryModifier(categories.toArray(new String[]{}))); - categoriesSequence.queueModifier(new TemplateRemoveModifier("Uncategorized")); - modifierSequenceDao.save(categoriesSequence); - } - - // FIXME: Make sure that the content provider is up - // This is the wrong place for it, but bleh - better than not having it turned on by default for people who don't go throughl ogin - ContentResolver.setSyncAutomatically(sessionManager.getCurrentAccount(), BuildConfig.MODIFICATION_AUTHORITY, true); // Enable sync by default! - - finish(); - } - - @Override - protected void onSaveInstanceState(Bundle outState) { - super.onSaveInstanceState(outState); - if (contribution != null) { - outState.putParcelable("contribution", contribution); - } - } - - @Override - protected void onAuthCookieAcquired(String authCookie) { - mwApi.setAuthCookie(authCookie); - } - - @Override - protected void onAuthFailure() { - Toast failureToast = Toast.makeText(this, R.string.authentication_failed, Toast.LENGTH_LONG); - failureToast.show(); - finish(); - } - - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - isUploadFinalised = false; - setContentView(R.layout.activity_share); - ButterKnife.bind(this); - initBack(); - backgroundImageView.setHierarchy(GenericDraweeHierarchyBuilder - .newInstance(getResources()) - .setPlaceholderImage(VectorDrawableCompat.create(getResources(), - R.drawable.ic_image_black_24dp, getTheme())) - .setFailureImage(VectorDrawableCompat.create(getResources(), - R.drawable.ic_error_outline_black_24dp, getTheme())) - .build()); - if (!ExternalStorageUtils.isStoragePermissionGranted(this)) { - this.savedInstanceState = savedInstanceState; - ExternalStorageUtils.requestExternalStoragePermission(this); - return; // Postpone operation to do after getting permission - } else { - receiveImageIntent(); - createContributionWithReceivedIntent(savedInstanceState); - } - } - - @Override - protected void onStop() { - // If upload is not finalised with failure or success, but contribution is created, - // we have to remove temp file, to prevent using unnecessary memory - if (!isUploadFinalised) { - if (mediaUri != null) { - ContributionUtils.removeTemporaryFile(mediaUri); - } - } - super.onStop(); - } - - private void createContributionWithReceivedIntent(Bundle savedInstanceState) { - if (savedInstanceState != null) { - contribution = savedInstanceState.getParcelable("contribution"); - } - - requestAuthToken(); - Timber.d("Uri: %s", mediaUri.toString()); - Timber.d("Ext storage dir: %s", Environment.getExternalStorageDirectory()); - - 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())) { - mediaUri = intent.getParcelableExtra(Intent.EXTRA_STREAM); - - contentProviderUri = mediaUri; - - mediaUri = ContributionUtils.saveFileBeingUploadedTemporarily(this, mediaUri); - - if (intent.hasExtra(UploadService.EXTRA_SOURCE)) { - source = intent.getStringExtra(UploadService.EXTRA_SOURCE); - } else { - source = Contribution.SOURCE_EXTERNAL; - } - if (intent.hasExtra("isDirectUpload")) { - Timber.d("This was initiated by a direct upload from Nearby"); - isNearbyUpload = true; - wikiDataEntityId = intent.getStringExtra(WIKIDATA_ENTITY_ID_PREF); - Timber.d("Received wikiDataEntityId from contribution controller %s", wikiDataEntityId); - } - mimeType = intent.getType(); - } - - if (mediaUri != null) { - backgroundImageView.setImageURI(mediaUri); - } - } - - /** - * Function to display the zoom and map FAB - */ - private void showFABMenu() { - isFABOpen = true; - - if (gpsObj != null && gpsObj.imageCoordsExists) - mapButton.setVisibility(View.VISIBLE); - zoomInButton.setVisibility(View.VISIBLE); - - mainFab.animate().rotationBy(180); - mapButton.animate().translationY(-getResources().getDimension(R.dimen.second_fab)); - zoomInButton.animate().translationY(-getResources().getDimension(R.dimen.first_fab)); - } - - /** - * Function to close the zoom and map FAB - */ - private void closeFABMenu() { - isFABOpen = false; - mainFab.animate().rotationBy(-180); - mapButton.animate().translationY(0); - zoomInButton.animate().translationY(0).setListener(new Animator.AnimatorListener() { - @Override - public void onAnimationStart(Animator animator) { - } - - @Override - public void onAnimationEnd(Animator animator) { - if (!isFABOpen) { - mapButton.setVisibility(View.GONE); - zoomInButton.setVisibility(View.GONE); - } - } - - @Override - public void onAnimationCancel(Animator animator) { - } - - @Override - public void onAnimationRepeat(Animator animator) { - } - }); - } - - /** - * Checks if upload was initiated via Nearby - * - * @return true if upload was initiated via Nearby - */ - protected boolean isNearbyUpload() { - return isNearbyUpload; - } - - /** - * Handles 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) { - if (requestCode == 1 && grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - Timber.d("onRequestPermissionsResult external storage permission granted"); - // You can receive image intent and save image to a temp file only if ext storage permission is granted - receiveImageIntent(); - createContributionWithReceivedIntent(savedInstanceState); - - if (requestCode == REQUEST_PERM_ON_SUBMIT_STORAGE) { - checkIfFileExists(); - //Uploading only begins if storage permission granted from arrow icon - uploadBegins(); - } - - } else { - finish(); - } - } - - /** - * 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 { - 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(this), fileSHA1, new WeakReference(this), result -> { - Timber.d("%s duplicate check: %s", mediaUri.toString(), result); - duplicateCheckPassed = (result == DUPLICATE_PROCEED || result == NO_DUPLICATE); - if (duplicateCheckPassed) { - //image is not a duplicate, so now check if its a unwanted picture or not - fileObj.detectUnwantedPictures(); - } - }, mwApi); - fileAsyncTask.execute(); - } catch (IOException e) { - Timber.e(e, "IO Exception: "); - } - } - } else { - Timber.w("not ready for preprocessing: useNewPermissions=%s storage=%s location=%s", - useNewPermissions, storagePermitted, locationPermitted); - } - } - - @Override - public void onPause() { - super.onPause(); - } - - @Override - protected void onDestroy() { - super.onDestroy(); - uploadController.cleanup(); - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case android.R.id.home: - if (categorizationFragment != null && categorizationFragment.isVisible()) { - categorizationFragment.showBackButtonDialog(); - } else { - onBackPressed(); - } - return true; - } - return super.onOptionsItemSelected(item); - } - - /** - * 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)); - closeFABMenu(); - mainFab.setVisibility(View.GONE); - - InputStream input = null; - try { - input = this.getContentResolver().openInputStream(imageuri); - } catch (FileNotFoundException e) { - e.printStackTrace(); - } - - Zoom zoomObj = new Zoom(thumbView, flContainer, this.getContentResolver()); - Bitmap scaledImage = zoomObj.createScaledImage(input, imageuri); - - // 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 { - 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); - - } -} - diff --git a/app/src/main/java/fr/free/nrw/commons/upload/SingleUploadFragment.java b/app/src/main/java/fr/free/nrw/commons/upload/SingleUploadFragment.java deleted file mode 100644 index bacefd5242..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/upload/SingleUploadFragment.java +++ /dev/null @@ -1,392 +0,0 @@ -package fr.free.nrw.commons.upload; - -import android.annotation.SuppressLint; -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.support.v7.widget.LinearLayoutManager; -import android.support.v7.widget.RecyclerView; -import android.text.Editable; -import android.text.Html; -import android.text.TextWatcher; -import android.text.method.LinkMovementMethod; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.MotionEvent; -import android.view.View; -import android.view.ViewGroup; -import android.widget.AdapterView; -import android.widget.ArrayAdapter; -import android.widget.Button; -import android.widget.EditText; -import android.widget.Spinner; -import android.widget.TextView; -import android.widget.Toast; - -import com.google.gson.Gson; -import com.google.gson.reflect.TypeToken; -import fr.free.nrw.commons.upload.DescriptionsAdapter.Callback; -import java.lang.reflect.Type; -import java.util.ArrayList; -import java.util.Locale; - -import java.util.Arrays; -import java.util.List; -import java.util.Locale; -import javax.inject.Inject; -import javax.inject.Named; - -import butterknife.BindView; -import butterknife.ButterKnife; -import butterknife.OnClick; -import butterknife.OnItemSelected; -import butterknife.OnTouch; -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_UP; - -public class SingleUploadFragment extends CommonsDaggerSupportFragment { - - @BindView(R.id.titleEdit) EditText titleEdit; - @BindView(R.id.rv_descriptions) RecyclerView rvDescriptions; - @BindView(R.id.titleDescButton) Button titleDescButton; - @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; - - private String license; - private OnUploadActionInitiated uploadActionInitiatedHandler; - private TitleTextWatcher textWatcher = new TitleTextWatcher(); - private DescriptionsAdapter descriptionsAdapter; - - @Override - public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { - inflater.inflate(R.menu.activity_share, menu); - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - //What happens when the 'submit' icon is tapped - case R.id.menu_upload_single: - - if (titleEdit.getText().toString().trim().isEmpty()) { - Toast.makeText(getContext(), R.string.add_title_toast, Toast.LENGTH_LONG).show(); - return false; - } - - String title = titleEdit.getText().toString(); - String descriptionsInVariousLanguages = getDescriptionsInAppropriateFormat(); - - //Save the title/desc in short-lived cache so next time this fragment is loaded, we can access these - prefs.edit() - .putString("Title", title) - .putString("Desc", new Gson().toJson(descriptionsAdapter - .getDescriptions()))//Description, now is not just a string, its a list of description objects - .apply(); - - uploadActionInitiatedHandler - .uploadActionInitiated(title, descriptionsInVariousLanguages); - return true; - } - return super.onOptionsItemSelected(item); - } - - private String getDescriptionsInAppropriateFormat() { - List descriptions = descriptionsAdapter.getDescriptions(); - StringBuilder descriptionsInAppropriateFormat = new StringBuilder(); - for (Description description : descriptions) { - String individualDescription = String.format("{{%s|1=%s}}", description.getLanguageId(), - description.getDescriptionText()); - descriptionsInAppropriateFormat.append(individualDescription); - } - return descriptionsInAppropriateFormat.toString(); - - } - - private List getDescriptions() { - List descriptions = descriptionsAdapter.getDescriptions(); - return descriptions; - } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { - View rootView = inflater.inflate(R.layout.fragment_single_upload, container, false); - ButterKnife.bind(this, rootView); - - initRecyclerView(); - - Intent activityIntent = getActivity().getIntent(); - if (activityIntent.hasExtra("title")) { - titleEdit.setText(activityIntent.getStringExtra("title")); - } - if (activityIntent.hasExtra("description") && descriptionsAdapter.getDescriptions() != null - && descriptionsAdapter.getDescriptions().size() > 0) { - descriptionsAdapter.getDescriptions().get(0) - .setDescriptionText(activityIntent.getStringExtra("description")); - descriptionsAdapter.notifyItemChanged(0); - } - - - ArrayList licenseItems = new ArrayList<>(); - licenseItems.add(getString(R.string.license_name_cc0)); - licenseItems.add(getString(R.string.license_name_cc_by)); - licenseItems.add(getString(R.string.license_name_cc_by_sa)); - licenseItems.add(getString(R.string.license_name_cc_by_four)); - licenseItems.add(getString(R.string.license_name_cc_by_sa_four)); - - license = prefs.getString(Prefs.DEFAULT_LICENSE, Prefs.Licenses.CC_BY_SA_3); - - // If this is a direct upload from Nearby, autofill title and desc fields with the Place's values - boolean isNearbyUpload = ((ShareActivity) getActivity()).isNearbyUpload(); - - if (isNearbyUpload) { - String imageTitle = directPrefs.getString("Title", ""); - String imageDesc = directPrefs.getString("Desc", ""); - String imageCats = directPrefs.getString("Category", ""); - Timber.d("Image title: " + imageTitle + ", image desc: " + imageDesc + ", image categories: " + imageCats); - titleEdit.setText(imageTitle); - if (descriptionsAdapter.getDescriptions() != null - && descriptionsAdapter.getDescriptions().size() > 0) { - descriptionsAdapter.getDescriptions().get(0).setDescriptionText(imageDesc); - descriptionsAdapter.notifyItemChanged(0); - } - } - - // check if this is the first time we have uploaded - if (prefs.getString("Title", "").trim().length() == 0 - && prefs.getString("Desc", "").trim().length() == 0) { - titleDescButton.setVisibility(View.GONE); - } - - Timber.d(license); - - ArrayAdapter adapter; - if (PreferenceManager.getDefaultSharedPreferences(getActivity()).getBoolean("theme", false)) { - // dark theme - adapter = new ArrayAdapter<>(getActivity(), android.R.layout.simple_spinner_dropdown_item, licenseItems); - } else { - // light theme - adapter = new ArrayAdapter<>(getActivity(), R.layout.light_simple_spinner_dropdown_item, licenseItems); - } - - licenseSpinner.setAdapter(adapter); - - int position = licenseItems.indexOf(getString(Utils.licenseNameFor(license))); - - // Check position is valid - if (position < 0) { - Timber.d("Invalid position: %d. Using default license", position); - position = 4; - } - - Timber.d("Position: %d %s", position, getString(Utils.licenseNameFor(license))); - licenseSpinner.setSelection(position); - - titleEdit.addTextChangedListener(textWatcher); - - titleEdit.setOnFocusChangeListener((v, hasFocus) -> { - if (!hasFocus) { - ViewUtil.hideKeyboard(v); - } - }); - - setLicenseSummary(license); - - return rootView; - } - - private void initRecyclerView() { - descriptionsAdapter = new DescriptionsAdapter(); - descriptionsAdapter.setCallback((mediaDetailDescription, descriptionInfo) -> showInfoAlert(mediaDetailDescription,descriptionInfo)); - descriptionsAdapter.setLanguages(getLocaleSupportedByDevice()); - rvDescriptions.setLayoutManager(new LinearLayoutManager(getContext())); - rvDescriptions.setAdapter(descriptionsAdapter); - } - - private List getLocaleSupportedByDevice() { - List languages = new ArrayList<>(); - Locale[] localesArray = Locale.getAvailableLocales(); - List locales = Arrays.asList(localesArray); - for (Locale locale : locales) { - languages.add(new Language(locale)); - } - return languages; - } - - @Override - public void onDestroyView() { - titleEdit.removeTextChangedListener(textWatcher); - super.onDestroyView(); - } - - @OnItemSelected(R.id.licenseSpinner) - void onLicenseSelected(AdapterView parent, View view, int position, long id) { - String licenseName = parent.getItemAtPosition(position).toString(); - - // Set selected color to white because it should be readable on random images. - TextView selectedText = (TextView) licenseSpinner.getChildAt(0); - if (selectedText != null) { - selectedText.setTextColor(Color.WHITE); - selectedText.setBackgroundColor(Color.TRANSPARENT); - } - - String license; - if (getString(R.string.license_name_cc0).equals(licenseName)) { - license = Prefs.Licenses.CC0; - } else if (getString(R.string.license_name_cc_by).equals(licenseName)) { - license = Prefs.Licenses.CC_BY_3; - } else if (getString(R.string.license_name_cc_by_sa).equals(licenseName)) { - license = Prefs.Licenses.CC_BY_SA_3; - } else if (getString(R.string.license_name_cc_by_four).equals(licenseName)) { - license = Prefs.Licenses.CC_BY_4; - } else if (getString(R.string.license_name_cc_by_sa_four).equals(licenseName)) { - license = Prefs.Licenses.CC_BY_SA_4; - } else { - throw new IllegalStateException("Unknown licenseName: " + licenseName); - } - - setLicenseSummary(license); - prefs.edit() - .putString(Prefs.DEFAULT_LICENSE, license) - .apply(); - } - - - @OnClick(R.id.titleDescButton) - void setTitleDescButton() { - //Retrieve last title and desc entered - String title = prefs.getString("Title", ""); - String descriptionJson = prefs.getString("Desc", ""); - Timber.d("Title: %s, Desc: %s", title, descriptionJson); - - titleEdit.setText(title); - Type typeOfDest = new TypeToken>() { - }.getType(); - - List descriptions = new Gson().fromJson(descriptionJson, typeOfDest); - descriptionsAdapter.setDescriptions(descriptions); - - } - - /** - * Copied from https://stackoverflow.com/a/26269435/8065933 - */ - @OnTouch(R.id.titleEdit) - boolean titleInfo(View view, MotionEvent motionEvent) { - final int value; - if (ViewCompat.getLayoutDirection(getView()) == ViewCompat.LAYOUT_DIRECTION_LTR) { - value = titleEdit.getRight() - titleEdit.getCompoundDrawables()[2].getBounds().width(); - if (motionEvent.getAction() == ACTION_UP && motionEvent.getRawX() >= value) { - showInfoAlert(R.string.media_detail_title, R.string.title_info); - return true; - } - } - else { - value = titleEdit.getLeft() + titleEdit.getCompoundDrawables()[0].getBounds().width(); - if (motionEvent.getAction() == ACTION_UP && motionEvent.getRawX() <= value) { - showInfoAlert(R.string.media_detail_title, R.string.title_info); - return true; - } - } - return false; - } - - @SuppressLint("StringFormatInvalid") - private void setLicenseSummary(String license) { - String licenseHyperLink = ""+ getString(Utils.licenseNameFor(license)) + "
"; - licenseSummaryView.setMovementMethod(LinkMovementMethod.getInstance()); - licenseSummaryView.setText(Html.fromHtml(getString(R.string.share_license_summary, licenseHyperLink))); - } - - @Override - public void onActivityCreated(Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - setHasOptionsMenu(true); - uploadActionInitiatedHandler = (OnUploadActionInitiated) getActivity(); - } - - @Override - public void onStop() { - super.onStop(); - - // FIXME: Stops the keyboard from being shown 'stale' while moving out of this fragment into the next - View target = getActivity().getCurrentFocus(); - ViewUtil.hideKeyboard(target); - } - - @NonNull - private String licenseUrlFor(String license) { - switch (license) { - case Prefs.Licenses.CC_BY_3: - return "https://creativecommons.org/licenses/by/3.0/"; - case Prefs.Licenses.CC_BY_4: - return "https://creativecommons.org/licenses/by/4.0/"; - case Prefs.Licenses.CC_BY_SA_3: - return "https://creativecommons.org/licenses/by-sa/3.0/"; - case Prefs.Licenses.CC_BY_SA_4: - return "https://creativecommons.org/licenses/by-sa/4.0/"; - case Prefs.Licenses.CC0: - return "https://creativecommons.org/publicdomain/zero/1.0/"; - } - throw new RuntimeException("Unrecognized license value: " + license); - } - - public interface OnUploadActionInitiated { - - void uploadActionInitiated(String title, String description); - } - - private class TitleTextWatcher implements TextWatcher { - - @Override - public void beforeTextChanged(CharSequence charSequence, int i, int i2, int i3) { - } - - @Override - public void onTextChanged(CharSequence charSequence, int i, int i2, int i3) { - } - - @Override - public void afterTextChanged(Editable editable) { - if (getActivity() != null) { - getActivity().invalidateOptionsMenu(); - } - } - } - - - private void showInfoAlert (int titleStringID, int messageStringID){ - new AlertDialog.Builder(getContext()) - .setTitle(titleStringID) - .setMessage(messageStringID) - .setCancelable(true) - .setNeutralButton(android.R.string.ok, (dialog, id) -> dialog.cancel()) - .create() - .show(); - } - - @OnClick(R.id.ll_add_description) - public void onLLAddDescriptionClicked() { - descriptionsAdapter.addDescription(new Description()); - rvDescriptions.scrollToPosition(descriptionsAdapter.getItemCount() - 1); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/upload/SpinnerLanguagesAdapter.java b/app/src/main/java/fr/free/nrw/commons/upload/SpinnerLanguagesAdapter.java index 5667aa55c2..46430a6da3 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/SpinnerLanguagesAdapter.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/SpinnerLanguagesAdapter.java @@ -1,47 +1,75 @@ package fr.free.nrw.commons.upload; import android.content.Context; +import android.content.res.Resources; +import android.graphics.Color; +import android.os.Build; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.widget.AdapterView; import android.widget.ArrayAdapter; import android.widget.LinearLayout; import android.widget.TextView; + import butterknife.BindView; import butterknife.ButterKnife; import fr.free.nrw.commons.R; +import fr.free.nrw.commons.utils.BiMap; + import java.util.ArrayList; +import java.util.Arrays; import java.util.List; -import java.util.Locale; public class SpinnerLanguagesAdapter extends ArrayAdapter { private final int resource; private final LayoutInflater layoutInflater; - List languages; + private List languageNamesList; + private List languageCodesList; + private final BiMap selectedLanguages; + public String selectedLangCode=""; + + public SpinnerLanguagesAdapter(@NonNull Context context, - int resource) { + int resource, BiMap selectedLanguages) { super(context, resource); this.resource = resource; this.layoutInflater = LayoutInflater.from(context); - languages = new ArrayList<>(); + languageNamesList = new ArrayList<>(); + languageCodesList = new ArrayList<>(); + prepareLanguages(context); + this.selectedLanguages = selectedLanguages; + } + + private void prepareLanguages(Context context) { + Resources resources = context.getResources(); + for (int i = 0; i < Language.languageNames.length; i++) { + languageNamesList.add(resources.getString(Language.languageGroups[i])); + languageCodesList.add(""); + languageNamesList.addAll(Arrays.asList(resources.getStringArray(Language.languageNames[i]))); + languageCodesList.addAll(Arrays.asList(resources.getStringArray(Language.languageCodes[i]))); + } } - public void setLanguages(List languages) { - this.languages = languages; + @Override + public boolean isEnabled(int position) { + return !languageCodesList.get(position).isEmpty()&& + (!selectedLanguages.containsKey(languageCodesList.get(position)) || + languageCodesList.get(position).equals(selectedLangCode)); } @Override public int getCount() { - return languages.size(); + return languageNamesList.size(); } @Override public View getDropDownView(int position, @Nullable View convertView, - @NonNull ViewGroup parent) { + @NonNull ViewGroup parent) { View view = layoutInflater.inflate(resource, parent, false); ViewHolder holder = new ViewHolder(view); holder.init(position, true); @@ -74,19 +102,38 @@ public ViewHolder(View itemView) { } public void init(int position, boolean isDropDownView) { - Language language = languages.get(position); if (!isDropDownView) { view.setVisibility(View.GONE); - tvLanguage.setText( - language.getLocale().getLanguage()); + if(languageCodesList.get(position).length()>2) + tvLanguage.setText(languageCodesList.get(position).subSequence(0,2)); + else + tvLanguage.setText(languageCodesList.get(position)); + } else { view.setVisibility(View.VISIBLE); - tvLanguage.setText( - String.format("%s [%s]", language.getLocale().getDisplayName(), - language.getLocale().getLanguage())); + if (languageCodesList.get(position).isEmpty()) { + tvLanguage.setText(languageNamesList.get(position)); + tvLanguage.setTextColor(Color.GRAY); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + tvLanguage.setTextAlignment(View.TEXT_ALIGNMENT_CENTER); + } + } else { + tvLanguage.setText( + String.format("%s [%s]", languageNamesList.get(position), languageCodesList.get(position))); + if(selectedLanguages.containsKey(languageCodesList.get(position))&& + !languageCodesList.get(position).equals(selectedLangCode)) + tvLanguage.setTextColor(Color.GRAY); + else + tvLanguage.setTextColor(Color.BLACK); + } } - } } + public String getLanguageCode(int position){ + return languageCodesList.get(position); + } + public int getIndexOfUserDefaultLocale(Context context) { + return languageCodesList.indexOf(context.getResources().getConfiguration().locale.getLanguage()); + } } diff --git a/app/src/main/java/fr/free/nrw/commons/upload/ThumbnailClickedListener.java b/app/src/main/java/fr/free/nrw/commons/upload/ThumbnailClickedListener.java new file mode 100644 index 0000000000..8963d0e257 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/upload/ThumbnailClickedListener.java @@ -0,0 +1,5 @@ +package fr.free.nrw.commons.upload; + +public interface ThumbnailClickedListener { + void thumbnailClicked(UploadModel.UploadItem content); +} diff --git a/app/src/main/java/fr/free/nrw/commons/upload/Title.java b/app/src/main/java/fr/free/nrw/commons/upload/Title.java new file mode 100644 index 0000000000..ee8fdaac8a --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/upload/Title.java @@ -0,0 +1,38 @@ +package fr.free.nrw.commons.upload; + +import android.text.TextUtils; + +import io.reactivex.subjects.BehaviorSubject; +import timber.log.Timber; + +class Title{ + + private String titleText; + private boolean set; + + @Override + public String toString() { + return titleText; + } + + public void setTitleText(String titleText) { + Timber.i("Setting title text to "+titleText); + this.titleText = titleText; + + if (!TextUtils.isEmpty(titleText)) { + set = true; + } + } + + public boolean isSet() { + return set; + } + + public void setSet(boolean set) { + this.set = set; + } + + public boolean isEmpty() { + return titleText==null || titleText.isEmpty(); + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadActivity.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadActivity.java new file mode 100644 index 0000000000..33e8ce29b6 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadActivity.java @@ -0,0 +1,656 @@ +package fr.free.nrw.commons.upload; + +import android.Manifest; +import android.animation.LayoutTransition; +import android.annotation.SuppressLint; +import android.content.ActivityNotFoundException; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.SharedPreferences; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.support.constraint.ConstraintLayout; +import android.support.design.widget.TextInputLayout; +import android.support.v7.app.AlertDialog; +import android.support.v7.widget.CardView; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.text.Html; +import android.text.TextUtils; +import android.text.method.LinkMovementMethod; +import android.view.View; +import android.view.inputmethod.InputMethodManager; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.Button; +import android.widget.EditText; +import android.widget.ImageView; +import android.widget.ProgressBar; +import android.widget.Spinner; +import android.widget.TextView; +import android.widget.Toast; +import android.widget.ViewFlipper; + +import com.github.chrisbanes.photoview.PhotoView; +import com.jakewharton.rxbinding2.view.RxView; +import com.jakewharton.rxbinding2.widget.RxTextView; +import com.pedrogomez.renderers.RVRendererAdapter; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import javax.inject.Inject; +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.auth.AuthenticatedActivity; +import fr.free.nrw.commons.auth.LoginActivity; +import fr.free.nrw.commons.category.CategoriesModel; +import fr.free.nrw.commons.category.CategoryItem; +import fr.free.nrw.commons.contributions.Contribution; +import fr.free.nrw.commons.mwapi.MediaWikiApi; +import fr.free.nrw.commons.utils.ImageUtils; +import fr.free.nrw.commons.utils.ViewUtil; +import io.reactivex.Observable; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.CompositeDisposable; +import io.reactivex.schedulers.Schedulers; +import timber.log.Timber; + +import static fr.free.nrw.commons.wikidata.WikidataConstants.WIKIDATA_ENTITY_ID_PREF; + +public class UploadActivity extends AuthenticatedActivity implements UploadView { + @Inject + InputMethodManager inputMethodManager; + @Inject + MediaWikiApi mwApi; + + + @Inject + @Named("direct_nearby_upload_prefs") + SharedPreferences directPrefs; + + @Inject + UploadPresenter presenter; + @Inject + CategoriesModel categoriesModel; + + // Main GUI + @BindView(R.id.backgroundImage) + PhotoView background; + @BindView(R.id.activity_upload_cards) + ConstraintLayout cardLayout; + @BindView(R.id.view_flipper) + ViewFlipper viewFlipper; + + // Top Card + @BindView(R.id.top_card) + CardView topCard; + @BindView(R.id.top_card_expand_button) + ImageView topCardExpandButton; + @BindView(R.id.top_card_title) + TextView topCardTitle; + @BindView(R.id.top_card_thumbnails) + RecyclerView topCardThumbnails; + + // Bottom Card + @BindView(R.id.bottom_card) + CardView bottomCard; + @BindView(R.id.bottom_card_expand_button) + ImageView bottomCardExpandButton; + @BindView(R.id.bottom_card_title) + TextView bottomCardTitle; + @BindView(R.id.bottom_card_next) + Button next; + @BindView(R.id.bottom_card_previous) + Button previous; + @BindView(R.id.bottom_card_add_desc) + Button bottomCardAddDescription; + + //Right Card + @BindView(R.id.right_card) + CardView rightCard; + @BindView(R.id.right_card_expand_button) + ImageView rightCardExpandButton; + @BindView(R.id.right_card_map_button) + View rightCardMapButton; + + // Category Search + @BindView(R.id.categories_title) + TextView categoryTitle; + @BindView(R.id.category_next) + Button categoryNext; + @BindView(R.id.category_previous) + Button categoryPrevious; + @BindView(R.id.categoriesSearchInProgress) + ProgressBar categoriesSearchInProgress; + @BindView(R.id.category_search) + EditText categoriesSearch; + @BindView(R.id.category_search_container) + TextInputLayout categoriesSearchContainer; + @BindView(R.id.categories) + RecyclerView categoriesList; + + // Final Submission + @BindView(R.id.license_title) + TextView licenseTitle; + @BindView(R.id.share_license_summary) + TextView licenseSummary; + @BindView(R.id.media_upload_policy) + TextView licensePolicy; + @BindView(R.id.license_list) + Spinner licenseSpinner; + @BindView(R.id.submit) + Button submit; + @BindView(R.id.license_previous) + Button licensePrevious; + @BindView(R.id.rv_descriptions) + RecyclerView rvDescriptions; + + private DescriptionsAdapter descriptionsAdapter; + private RVRendererAdapter categoriesAdapter; + private CompositeDisposable compositeDisposable; + + DexterPermissionObtainer dexterPermissionObtainer; + + + @SuppressLint("CheckResult") + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setContentView(R.layout.activity_upload); + ButterKnife.bind(this); + compositeDisposable = new CompositeDisposable(); + + configureCategories(savedInstanceState); + configureLicenses(); + configureLayout(); + configureTopCard(); + configureBottomCard(); + initRecyclerView(); + configureRightCard(); + configureNavigationButtons(); + + //storagePermissionReady = BehaviorSubject.createDefault(false); + //storagePermissionReady.subscribe(b -> Timber.i("storagePermissionReady:" + b)); + presenter.initFromSavedState(savedInstanceState); + + dexterPermissionObtainer = new DexterPermissionObtainer(this, + Manifest.permission.WRITE_EXTERNAL_STORAGE, + getString(R.string.storage_permission), + getString(R.string.write_storage_permission_rationale_for_image_share)); + + dexterPermissionObtainer.confirmStoragePermissions().subscribe(this::receiveSharedItems); + } + + @Override + public boolean checkIfLoggedIn() { + if (!sessionManager.isUserLoggedIn()) { + Timber.d("Current account is null"); + ViewUtil.showLongToast(this, getString(R.string.user_not_logged_in)); + Intent loginIntent = new Intent(UploadActivity.this, LoginActivity.class); + startActivity(loginIntent); + return false; + } + return true; + } + + @Override + protected void onDestroy() { + presenter.cleanup(); + super.onDestroy(); + } + + @Override + protected void onResume() { + super.onResume(); + checkIfLoggedIn(); + compositeDisposable.add( + dexterPermissionObtainer.confirmStoragePermissions() + .subscribe(() -> presenter.addView(this))); + compositeDisposable.add( + RxTextView.textChanges(categoriesSearch) + .doOnEach(v -> categoriesSearchContainer.setError(null)) + .takeUntil(RxView.detaches(categoriesSearch)) + .debounce(500, TimeUnit.MILLISECONDS) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(filter -> updateCategoryList(filter.toString()), Timber::e) + ); + } + + @Override + protected void onPause() { + presenter.removeView(); +// imageTitle.removeTextChangedListener(titleWatcher); +// imageDescription.removeTextChangedListener(descriptionWatcher); + compositeDisposable.dispose(); + compositeDisposable = new CompositeDisposable(); + super.onPause(); + } + + @Override + public void updateThumbnails(List uploads) { + int uploadCount = uploads.size(); + topCardThumbnails.setAdapter(new UploadThumbnailsAdapterFactory(presenter::thumbnailClicked).create(uploads)); + topCardTitle.setText(getResources().getQuantityString(R.plurals.upload_count_title, uploadCount, uploadCount)); + } + + @Override + public void updateRightCardContent(boolean gpsPresent) { + if(gpsPresent){ + rightCardMapButton.setVisibility(View.VISIBLE); + }else{ + rightCardMapButton.setVisibility(View.GONE); + } + //The card should be disabled if it has no buttons. + setRightCardVisibility(gpsPresent); + } + + @Override + public void updateBottomCardContent(int currentStep, int stepCount, UploadModel.UploadItem uploadItem) { + String cardTitle = getResources().getString(R.string.step_count, currentStep, stepCount); + bottomCardTitle.setText(cardTitle); + categoryTitle.setText(cardTitle); + licenseTitle.setText(cardTitle); + if (!uploadItem.isDummy()) { + descriptionsAdapter.setItems(uploadItem.title, uploadItem.descriptions); + rvDescriptions.setAdapter(descriptionsAdapter); + } + } + + @Override + public void updateLicenses(List licenses, String selectedLicense) { + ArrayAdapter adapter = new ArrayAdapter<>(this, android.R.layout.simple_spinner_dropdown_item, licenses); + licenseSpinner.setAdapter(adapter); + + int position = licenses.indexOf(getString(Utils.licenseNameFor(selectedLicense))); + + // Check position is valid + if (position < 0) { + Timber.d("Invalid position: %d. Using default license", position); + position = licenses.size() - 1; + } + + Timber.d("Position: %d %s", position, getString(Utils.licenseNameFor(selectedLicense))); + licenseSpinner.setSelection(position); + } + + @Override + public void updateLicenseSummary(String selectedLicense) { + String licenseHyperLink = "" + + getString(Utils.licenseNameFor(selectedLicense)) + "
"; + licenseSummary.setMovementMethod(LinkMovementMethod.getInstance()); + licenseSummary.setText( + Html.fromHtml( + getString(R.string.share_license_summary, licenseHyperLink))); + } + + @Override + public void updateTopCardContent() { + RecyclerView.Adapter adapter = topCardThumbnails.getAdapter(); + if (adapter != null) { + adapter.notifyDataSetChanged(); + } + } + + @Override + public void setNextEnabled(boolean available) { + next.setEnabled(available); + categoryNext.setEnabled(available); + } + + @Override + public void setSubmitEnabled(boolean available) { + submit.setEnabled(available); + } + + @Override + public void setPreviousEnabled(boolean available) { + previous.setEnabled(available); + categoryPrevious.setEnabled(available); + licensePrevious.setEnabled(available); + } + + @Override + public void setTopCardState(boolean state) { + updateCardState(state, topCardExpandButton, topCardThumbnails); + } + + @Override + public void setTopCardVisibility(boolean visible) { + topCard.setVisibility(visible ? View.VISIBLE : View.GONE); + } + + @Override + public void setBottomCardVisibility(boolean visible) { + bottomCard.setVisibility(visible ? View.VISIBLE : View.GONE); + } + + @Override + public void setRightCardVisibility(boolean visible) { + rightCard.setVisibility(visible ? View.VISIBLE : View.GONE); + } + + @Override + public void setBottomCardVisibility(@UploadPage int page) { + if (page == TITLE_CARD) { + viewFlipper.setDisplayedChild(0); + } else if (page == CATEGORIES) { + viewFlipper.setDisplayedChild(1); + } else if (page == LICENSE) { + viewFlipper.setDisplayedChild(2); + } else if (page == PLEASE_WAIT) { + viewFlipper.setDisplayedChild(3); + } + } + + @Override + public void setBottomCardState(boolean state) { + updateCardState(state, bottomCardExpandButton, rvDescriptions, previous, next, bottomCardAddDescription); + } + + @Override + public void setRightCardState(boolean state) { + rightCardExpandButton.animate().rotation(rightCardExpandButton.getRotation() + (state ? -180 : 180)).start(); + //Add all items in rightCard here + rightCardMapButton.setVisibility(state ? View.VISIBLE : View.GONE); + } + + @Override + public void setBackground(Uri mediaUri) { + background.setImageURI(mediaUri); + } + + + @Override + public void dismissKeyboard() { +// inputMethodManager.hideSoftInputFromWindow(imageTitle.getWindowToken(), 0); + } + + @Override + public void showBadPicturePopup(@ImageUtils.Result int result) { + int errorMessage; + if (result == ImageUtils.IMAGE_DARK) + errorMessage = R.string.upload_image_problem_dark; + else if (result == ImageUtils.IMAGE_BLURRY) + errorMessage = R.string.upload_image_problem_blurry; + else if (result == ImageUtils.IMAGE_DUPLICATE) + errorMessage = R.string.upload_image_problem_duplicate; + else if (result == (ImageUtils.IMAGE_DARK|ImageUtils.IMAGE_BLURRY)) + errorMessage = R.string.upload_image_problem_dark_blurry; + else if (result == (ImageUtils.IMAGE_DARK|ImageUtils.IMAGE_DUPLICATE)) + errorMessage = R.string.upload_image_problem_dark_duplicate; + else if (result == (ImageUtils.IMAGE_BLURRY|ImageUtils.IMAGE_DUPLICATE)) + errorMessage = R.string.upload_image_problem_blurry_duplicate; + else if (result == (ImageUtils.IMAGE_DARK|ImageUtils.IMAGE_BLURRY|ImageUtils.IMAGE_DUPLICATE)) + errorMessage = R.string.upload_image_problem_dark_blurry_duplicate; + else + return; + AlertDialog.Builder errorDialogBuilder = new AlertDialog.Builder(this); + errorDialogBuilder.setMessage(errorMessage); + errorDialogBuilder.setTitle(R.string.warning); + //user does not wish to upload the picture, delete it + errorDialogBuilder.setPositiveButton(R.string.no, (dialogInterface, i) -> { + presenter.deletePicture(); + dialogInterface.dismiss(); + }); + //user wishes to go ahead with the upload of this picture, just dismiss this dialog + errorDialogBuilder.setNegativeButton(R.string.yes, (DialogInterface dialogInterface, int i) -> { + presenter.keepPicture(); + dialogInterface.dismiss(); + }); + + AlertDialog errorDialog = errorDialogBuilder.create(); + if (!isFinishing()) { + errorDialog.show(); + } + } + + public void showDuplicateTitlePopup(String title) { + showInfoAlert(R.string.warning, R.string.upload_title_duplicate, title); + } + + @Override + protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + Bundle state = presenter.getSavedState(); + outState.putAll(state); + int itemCount = categoriesAdapter.getItemCount(); + ArrayList items = new ArrayList<>(itemCount); + for (int i = 0; i < itemCount; i++) { + items.add(categoriesAdapter.getItem(i)); + } + outState.putParcelableArrayList("currentCategories", items); + outState.putSerializable("categoriesCache", categoriesModel.getCategoriesCache()); + } + + @Override + protected void onAuthCookieAcquired(String authCookie) { + mwApi.setAuthCookie(authCookie); + } + + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if (requestCode == CommonsApplication.OPEN_APPLICATION_DETAIL_SETTINGS) { + dexterPermissionObtainer.onManualPermissionReturned(); + } + } + + + @Override + protected void onAuthFailure() { + Toast.makeText(this, R.string.authentication_failed, Toast.LENGTH_LONG).show(); + finish(); + } + + private void configureLicenses() { + licenseSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + String licenseName = parent.getItemAtPosition(position).toString(); + presenter.selectLicense(licenseName); + } + + @Override + public void onNothingSelected(AdapterView parent) { + presenter.selectLicense(null); + } + }); + } + + private void configureLayout() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + cardLayout.getLayoutTransition().enableTransitionType(LayoutTransition.CHANGING); + } + background.setScaleType(ImageView.ScaleType.CENTER_CROP); + background.setOnScaleChangeListener((scaleFactor, x, y) -> presenter.closeAllCards()); + } + + private void configureTopCard() { + topCardExpandButton.setOnClickListener(v -> presenter.toggleTopCardState()); + topCardThumbnails.setLayoutManager(new LinearLayoutManager(this, + LinearLayoutManager.HORIZONTAL, false)); + } + + private void configureBottomCard() { + bottomCardExpandButton.setOnClickListener(v -> presenter.toggleBottomCardState()); + bottomCardAddDescription.setOnClickListener(v -> { + descriptionsAdapter.addDescription(new Description()); + rvDescriptions.scrollToPosition(descriptionsAdapter.getItemCount() - 1); + }); + } + + private void configureRightCard() { + rightCardExpandButton.setOnClickListener(v -> presenter.toggleRightCardState()); + rightCardMapButton.setOnClickListener(v -> presenter.openCoordinateMap()); + } + + private void configureNavigationButtons() { + // Navigation next / previous for each image as we're collecting title + description + next.setOnClickListener(v -> presenter.handleNext()); + previous.setOnClickListener(v -> presenter.handlePrevious()); + + // Next / previous for the category selection page + categoryNext.setOnClickListener(v -> presenter.handleNext()); + categoryPrevious.setOnClickListener(v -> presenter.handlePrevious()); + + // Finally, the previous / submit buttons on the final page of the wizard + licensePrevious.setOnClickListener(v -> presenter.handlePrevious()); + submit.setOnClickListener(v -> { + Toast.makeText(this, R.string.uploading_started, Toast.LENGTH_LONG).show(); + presenter.handleSubmit(categoriesModel); + finish(); + }); + + } + + private void configureCategories(Bundle savedInstanceState) { + ArrayList items = new ArrayList<>(); + if (savedInstanceState != null) { + items.addAll(savedInstanceState.getParcelableArrayList("currentCategories")); + //noinspection unchecked + categoriesModel.cacheAll((HashMap>) savedInstanceState + .getSerializable("categoriesCache")); + } + categoriesAdapter = new UploadCategoriesAdapterFactory(categoriesModel).create(items); + categoriesList.setLayoutManager(new LinearLayoutManager(this)); + categoriesList.setAdapter(categoriesAdapter); + } + + @SuppressLint("CheckResult") + private void updateCategoryList(String filter) { + Observable.fromIterable(categoriesModel.getSelectedCategories()) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .doOnSubscribe(disposable -> { + categoriesSearchInProgress.setVisibility(View.VISIBLE); + categoriesSearchContainer.setError(null); + categoriesAdapter.clear(); + }) + .observeOn(Schedulers.io()) + .concatWith( + categoriesModel.searchAll(filter) + .mergeWith(categoriesModel.searchCategories(filter)) + .concatWith(TextUtils.isEmpty(filter) + ? categoriesModel.defaultCategories() : Observable.empty()) + ) + .filter(categoryItem -> !categoriesModel.containsYear(categoryItem.getName())) + .distinct() + .sorted(categoriesModel.sortBySimilarity(filter)) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + s -> categoriesAdapter.add(s), + Timber::e, + () -> { + categoriesAdapter.notifyDataSetChanged(); + categoriesSearchInProgress.setVisibility(View.GONE); + + if (categoriesAdapter.getItemCount() == categoriesModel.selectedCategoriesCount() + && !categoriesSearch.getText().toString().isEmpty()) { + categoriesSearchContainer.setError("No categories found"); + } + } + ); + } + + private void receiveSharedItems() { + Intent intent = getIntent(); + String mimeType = intent.getType(); + String source; + + if (intent.hasExtra(UploadService.EXTRA_SOURCE)) { + source = intent.getStringExtra(UploadService.EXTRA_SOURCE); + } else { + source = Contribution.SOURCE_EXTERNAL; + } + + if (Intent.ACTION_SEND.equals(intent.getAction())) { + Uri mediaUri = intent.getParcelableExtra(Intent.EXTRA_STREAM); + if (intent.getBooleanExtra("isDirectUpload", false)) { + String imageTitle = directPrefs.getString("Title", ""); + String imageDesc = directPrefs.getString("Desc", ""); + Timber.i("Received direct upload with title" + imageTitle + "and description %s" + imageDesc); + String wikidataEntityIdPref = intent.getStringExtra(WIKIDATA_ENTITY_ID_PREF); + presenter.receiveDirect(mediaUri, mimeType, source, wikidataEntityIdPref, imageTitle, imageDesc); + } else { + Timber.i("Received single upload"); + presenter.receive(mediaUri, mimeType, source); + } + } else if (Intent.ACTION_SEND_MULTIPLE.equals(intent.getAction())) { + ArrayList urisList = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM); + Timber.i("Received multiple upload"); + presenter.receive(urisList, mimeType, source); + } + } + + private void updateCardState(boolean state, ImageView button, View... content) { + button.animate().rotation(button.getRotation() + (state ? 180 : -180)).start(); + if (content != null) { + for (View view : content) { + view.setVisibility(state ? View.VISIBLE : View.GONE); + } + } + } + + public void launchMapActivity(String decCoords) { + try { + Uri gmmIntentUri = Uri.parse("google.streetview:cbll=" + decCoords); + Intent mapIntent = new Intent(Intent.ACTION_VIEW, gmmIntentUri); + mapIntent.setPackage("com.google.android.apps.maps"); + startActivity(mapIntent); + } catch (ActivityNotFoundException ex) { + AlertDialog.Builder errorDialogBuilder = new AlertDialog.Builder(this); + errorDialogBuilder.setMessage(R.string.map_application_missing); + errorDialogBuilder.setTitle(R.string.warning); + //just dismiss the dialog + errorDialogBuilder.setPositiveButton(R.string.ok, (dialogInterface, i) -> { + }); + + AlertDialog errorDialog = errorDialogBuilder.create(); + if (!isFinishing()) { + errorDialog.show(); + } + } + } + + @Override + public List getDescriptions() { + return descriptionsAdapter.getDescriptions(); + } + + private void initRecyclerView() { + descriptionsAdapter = new DescriptionsAdapter(); + descriptionsAdapter.setCallback(this::showInfoAlert); + rvDescriptions.setLayoutManager(new LinearLayoutManager(getApplicationContext())); + rvDescriptions.setAdapter(descriptionsAdapter); + compositeDisposable.add( + descriptionsAdapter.getTitleChangeObserver() + .debounce(1000, TimeUnit.MILLISECONDS) + .observeOn(Schedulers.io()) + .filter(title -> mwApi.fileExistsWithName(title + "." + presenter.getCurrentItem().fileExt)) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(title -> showDuplicateTitlePopup(title + "." + presenter.getCurrentItem().fileExt), Timber::e) + ); + } + + + private void showInfoAlert(int titleStringID, int messageStringId, String... formatArgs) { + new AlertDialog.Builder(this) + .setTitle(titleStringID) + .setMessage(getString(messageStringId, (Object[]) formatArgs)) + .setCancelable(true) + .setNeutralButton(android.R.string.ok, (dialog, id) -> dialog.cancel()) + .create() + .show(); + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadCategoriesAdapterFactory.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadCategoriesAdapterFactory.java new file mode 100644 index 0000000000..1797cbe806 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadCategoriesAdapterFactory.java @@ -0,0 +1,27 @@ +package fr.free.nrw.commons.upload; + +import com.pedrogomez.renderers.ListAdapteeCollection; +import com.pedrogomez.renderers.RVRendererAdapter; +import com.pedrogomez.renderers.RendererBuilder; + +import java.util.Collections; +import java.util.List; + +import fr.free.nrw.commons.category.CategoryClickedListener; +import fr.free.nrw.commons.category.CategoryItem; + +public class UploadCategoriesAdapterFactory { + private final CategoryClickedListener listener; + + public UploadCategoriesAdapterFactory(CategoryClickedListener listener) { + this.listener = listener; + } + + public RVRendererAdapter create(List placeList) { + RendererBuilder builder = new RendererBuilder() + .bind(CategoryItem.class, new UploadCategoriesRenderer(listener)); + ListAdapteeCollection collection = new ListAdapteeCollection<>( + placeList != null ? placeList : Collections.emptyList()); + return new RVRendererAdapter<>(builder, collection); + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadCategoriesRenderer.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadCategoriesRenderer.java new file mode 100644 index 0000000000..d0862b964b --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadCategoriesRenderer.java @@ -0,0 +1,52 @@ +package fr.free.nrw.commons.upload; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CheckBox; + +import com.pedrogomez.renderers.Renderer; + +import butterknife.BindView; +import butterknife.ButterKnife; +import fr.free.nrw.commons.R; +import fr.free.nrw.commons.category.CategoryClickedListener; +import fr.free.nrw.commons.category.CategoryItem; + +public class UploadCategoriesRenderer extends Renderer { + @BindView(R.id.tvName) CheckBox checkedView; + private final CategoryClickedListener listener; + + UploadCategoriesRenderer(CategoryClickedListener listener) { + this.listener = listener; + } + + @Override + protected View inflate(LayoutInflater layoutInflater, ViewGroup viewGroup) { + return layoutInflater.inflate(R.layout.layout_upload_categories_item, viewGroup, false); + } + + @Override + protected void setUpView(View view) { + ButterKnife.bind(this, view); + } + + @Override + protected void hookListeners(View view) { + view.setOnClickListener(v -> { + CategoryItem item = getContent(); + item.setSelected(!item.isSelected()); + checkedView.setChecked(item.isSelected()); + if (listener != null) { + listener.categoryClicked(item); + } + }); + } + + @Override + public void render() { + CategoryItem item = getContent(); + checkedView.setChecked(item.isSelected()); + checkedView.setText(item.getName()); + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadController.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadController.java index b5aa4f5149..817a4f98b4 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/UploadController.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadController.java @@ -92,35 +92,23 @@ public void cleanup() { /** * Starts a new upload task. - * @param title the title of the contribution - * @param mediaUri the media URI of the contribution - * @param description the description of the contribution - * @param mimeType the MIME type of the contribution - * @param source the source of the contribution - * @param decimalCoords the coordinates in decimal. (e.g. "37.51136|-77.602615") + * + * @param title the title of the contribution + * @param mediaUri the media URI of the contribution + * @param description the description of the contribution + * @param mimeType the MIME type of the contribution + * @param source the source of the contribution + * @param decimalCoords the coordinates in decimal. (e.g. "37.51136|-77.602615") * @param wikiDataEntityId - * @param onComplete the progress tracker + * @param onComplete the progress tracker */ public void startUpload(String title, Uri contentProviderUri, Uri mediaUri, String description, String mimeType, String source, String decimalCoords, String wikiDataEntityId, ContributionUploadProgress onComplete) { Contribution contribution; - - //TODO: Modify this to include coords - contribution = new Contribution(mediaUri, null, title, description, -1, - null, null, sessionManager.getCurrentAccount().name, - CommonsApplication.DEFAULT_EDIT_SUMMARY, decimalCoords); - - - contribution.setTag("mimeType", mimeType); - contribution.setSource(source); - - //Calls the next overloaded method - startUpload(contribution, onComplete); - Timber.d("Wikidata entity ID received from Share activity is %s", wikiDataEntityId); //TODO: Modify this to include coords Account currentAccount = sessionManager.getCurrentAccount(); - if(currentAccount == null) { + if (currentAccount == null) { Timber.d("Current account is null"); ViewUtil.showLongToast(context, context.getString(R.string.user_not_logged_in)); sessionManager.forceLogin(context); @@ -136,7 +124,17 @@ public void startUpload(String title, Uri contentProviderUri, Uri mediaUri, Stri contribution.setWikiDataEntityId(wikiDataEntityId); contribution.setContentProviderUri(contentProviderUri); + //Calls the next overloaded method + startUpload(contribution, onComplete); + } + /** + * Starts a new upload task. + * + * @param contribution the contribution object + */ + public void startUpload(Contribution contribution) { + startUpload(contribution, c -> {}); } /** @@ -149,7 +147,14 @@ public void startUpload(String title, Uri contentProviderUri, Uri mediaUri, Stri public void startUpload(final Contribution contribution, final ContributionUploadProgress onComplete) { //Set creator, desc, and license if (TextUtils.isEmpty(contribution.getCreator())) { - contribution.setCreator(sessionManager.getCurrentAccount().name); + Account currentAccount = sessionManager.getCurrentAccount(); + if (currentAccount == null) { + Timber.d("Current account is null"); + ViewUtil.showLongToast(context, context.getString(R.string.user_not_logged_in)); + sessionManager.forceLogin(context); + return; + } + contribution.setCreator(currentAccount.name); } if (contribution.getDescription() == null) { @@ -170,10 +175,8 @@ protected Contribution doInBackground(Void... voids /* stare into you */) { long length; ContentResolver contentResolver = context.getContentResolver(); try { - - //TODO: understand do we really need this code if (contribution.getDataLength() <= 0) { - Log.d("deneme","UploadController/doInBackground, contribution.getLocalUri():"+contribution.getLocalUri()); + Log.d("deneme", "UploadController/doInBackground, contribution.getLocalUri():" + contribution.getLocalUri()); AssetFileDescriptor assetFileDescriptor = contentResolver .openAssetFileDescriptor(Uri.fromFile(new File(contribution.getLocalUri().getPath())), "r"); if (assetFileDescriptor != null) { @@ -225,7 +228,7 @@ protected Contribution doInBackground(Void... voids /* stare into you */) { contribution.setDateCreated(new Date()); } } - return contribution; + return contribution; } @Override diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadModel.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadModel.java new file mode 100644 index 0000000000..1978db339b --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadModel.java @@ -0,0 +1,351 @@ +package fr.free.nrw.commons.upload; + +import android.accounts.Account; +import android.annotation.SuppressLint; +import android.content.ContentResolver; +import android.content.Context; +import android.content.SharedPreferences; +import android.graphics.BitmapRegionDecoder; +import android.net.Uri; +import android.support.annotation.Nullable; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import javax.inject.Inject; +import javax.inject.Named; + +import fr.free.nrw.commons.CommonsApplication; +import fr.free.nrw.commons.auth.SessionManager; +import fr.free.nrw.commons.contributions.Contribution; +import fr.free.nrw.commons.mwapi.MediaWikiApi; +import fr.free.nrw.commons.settings.Prefs; +import fr.free.nrw.commons.utils.ImageUtils; +import io.reactivex.Observable; +import io.reactivex.Single; +import io.reactivex.disposables.Disposable; +import io.reactivex.functions.Consumer; +import io.reactivex.schedulers.Schedulers; +import io.reactivex.subjects.BehaviorSubject; +import timber.log.Timber; + +public class UploadModel { + + MediaWikiApi mwApi; + private static UploadItem DUMMY = new UploadItem(Uri.EMPTY, "", "", GPSExtractor.DUMMY, "", null) { + @Override + public boolean isDummy() { + return true; + } + }; + private final SharedPreferences prefs; + private final List licenses; + private String license; + private final Map licensesByName; + private List items = new ArrayList<>(); + private boolean topCardState = true; + private boolean bottomCardState = true; + private boolean rightCardState = true; + private int currentStepIndex = 0; + private Context context; + private ContentResolver contentResolver; + private boolean useExtStorage; + private Disposable badImageSubscription; + + @Inject + SessionManager sessionManager; + + @Inject + UploadModel(@Named("licenses") List licenses, + @Named("default_preferences") SharedPreferences prefs, + @Named("licenses_by_name") Map licensesByName, + Context context, + MediaWikiApi mwApi) { + this.licenses = licenses; + this.prefs = prefs; + this.license = Prefs.Licenses.CC_BY_SA_3; + this.licensesByName = licensesByName; + this.context = context; + this.mwApi = mwApi; + this.contentResolver = context.getContentResolver(); + useExtStorage = this.prefs.getBoolean("useExternalStorage", false); + } + + @SuppressLint("CheckResult") + public void receive(List mediaUri, String mimeType, String source) { + currentStepIndex = 0; + Observable itemObservable = Observable.fromIterable(mediaUri) + .map(this::cacheFileUpload) + .map(filePath -> { + Uri uri = Uri.fromFile(new File(filePath)); + FileProcessor fp = new FileProcessor(filePath, context.getContentResolver(), context); + UploadItem item = new UploadItem(uri, mimeType, source, fp.processFileCoordinates(), + FileUtils.getFileExt(filePath), null); + Single.zip( + Single.fromCallable(() -> + new FileInputStream(filePath)) + .map(FileUtils::getSHA1) + .map(mwApi::existingFile) + .map(b -> b ? ImageUtils.IMAGE_DUPLICATE : ImageUtils.IMAGE_OK), + Single.fromCallable(() -> + new FileInputStream(filePath)) + .map(file -> BitmapRegionDecoder.newInstance(file, false)) + .map(ImageUtils::checkIfImageIsTooDark), //Returns IMAGE_DARK or IMAGE_OK + (dupe, dark) -> dupe | dark) + .observeOn(Schedulers.io()) + .subscribe(item.imageQuality::onNext, Timber::e); + return item; + }); + items = itemObservable.toList().blockingGet(); + items.get(0).selected = true; + items.get(0).first = true; + } + + @SuppressLint("CheckResult") + public void receiveDirect(Uri media, String mimeType, String source, String wikidataEntityIdPref, String title, String desc) { + currentStepIndex = 0; + items = new ArrayList<>(); + String filePath = this.cacheFileUpload(media); + Uri uri = Uri.fromFile(new File(filePath)); + FileProcessor fp = new FileProcessor(filePath, context.getContentResolver(), context); + UploadItem item = new UploadItem(uri, mimeType, source, fp.processFileCoordinates(), + FileUtils.getFileExt(filePath), wikidataEntityIdPref); + item.title.setTitleText(title); + item.descriptions.get(0).setDescriptionText(desc); + //TODO figure out if default descriptions in other languages exist + item.descriptions.get(0).setLanguageCode("en"); + Single.zip( + Single.fromCallable(() -> + new FileInputStream(filePath)) + .map(FileUtils::getSHA1) + .map(mwApi::existingFile) + .map(b -> b ? ImageUtils.IMAGE_DUPLICATE : ImageUtils.IMAGE_OK), + Single.fromCallable(() -> + new FileInputStream(filePath)) + .map(file -> BitmapRegionDecoder.newInstance(file, false)) + .map(ImageUtils::checkIfImageIsTooDark), //Returns IMAGE_DARK or IMAGE_OK + (dupe, dark) -> dupe | dark).subscribe(item.imageQuality::onNext); + items.add(item); + items.get(0).selected = true; + items.get(0).first = true; + } + + public boolean isPreviousAvailable() { + return currentStepIndex > 0; + } + + public boolean isNextAvailable() { + return currentStepIndex < (items.size() + 1); + } + + public boolean isSubmitAvailable() { + int count = items.size(); + boolean hasError = license == null; + for (int i = 0; i < count; i++) { + UploadItem item = items.get(i); + hasError |= item.error; + } + return !hasError; + } + + public int getCurrentStep() { + return currentStepIndex + 1; + } + + public int getStepCount() { + return items.size() + 2; + } + + public int getCount() { + return items.size(); + } + + public List getUploads() { + return items; + } + + public boolean isTopCardState() { + return topCardState; + } + + public void setTopCardState(boolean topCardState) { + this.topCardState = topCardState; + } + + public boolean isBottomCardState() { + return bottomCardState; + } + + public void setRightCardState(boolean rightCardState) { + this.rightCardState = rightCardState; + } + + public boolean isRightCardState() { + return rightCardState; + } + + public void setBottomCardState(boolean bottomCardState) { + this.bottomCardState = bottomCardState; + } + + public void next() { + if (badImageSubscription != null) + badImageSubscription.dispose(); + markCurrentUploadVisited(); + if (currentStepIndex < items.size() + 1) { + currentStepIndex++; + } + updateItemState(); + } + + public void previous() { + if (badImageSubscription != null) + badImageSubscription.dispose(); + markCurrentUploadVisited(); + if (currentStepIndex > 0) { + currentStepIndex--; + } + updateItemState(); + } + + public void jumpTo(UploadItem item) { + currentStepIndex = items.indexOf(item); + item.visited = true; + updateItemState(); + } + + public UploadItem getCurrentItem() { + return isShowingItem() ? items.get(currentStepIndex) : DUMMY; + } + + public boolean isShowingItem() { + return currentStepIndex < items.size(); + } + + private void updateItemState() { + int count = items.size(); + for (int i = 0; i < count; i++) { + UploadItem item = items.get(i); + item.selected = (currentStepIndex >= count || i == currentStepIndex); + item.error = item.title == null || item.title.isEmpty(); + } + } + + private void markCurrentUploadVisited() { + if (currentStepIndex < items.size() && currentStepIndex >= 0) { + items.get(currentStepIndex).visited = true; + } + } + + public List getLicenses() { + return licenses; + } + + public String getSelectedLicense() { + return license; + } + + public void setSelectedLicense(String licenseName) { + this.license = licensesByName.get(licenseName); + } + + public Observable buildContributions(List categoryStringList) { + return Observable.fromIterable(items).map(item -> + { + Contribution contribution = new Contribution(item.mediaUri, null, item.title + "." + item.fileExt, + Description.formatList(item.descriptions), -1, + null, null, sessionManager.getCurrentAccount().name, + CommonsApplication.DEFAULT_EDIT_SUMMARY, item.gpsCoords.getCoords()); + contribution.setWikiDataEntityId(item.wikidataEntityId); + contribution.setCategories(categoryStringList); + contribution.setTag("mimeType", item.mimeType); + contribution.setSource(item.source); + contribution.setContentProviderUri(item.mediaUri); + return contribution; + }); + } + + /** + * Copy files into local storage and return file path + * + * @param media Uri of the file + * @return path of the enw file + */ + private String cacheFileUpload(Uri media) { + try { + String copyPath; + if (useExtStorage) + copyPath = FileUtils.createExternalCopyPathAndCopy(media, contentResolver); + else + copyPath = FileUtils.createCopyPathAndCopy(media, context); + Timber.i("File path is " + copyPath); + return copyPath; + } catch (IOException e) { + Timber.w(e, "Error in copying URI " + media.getPath()); + return null; + } + } + + public void keepPicture() { + items.get(currentStepIndex).imageQuality.onNext(ImageUtils.IMAGE_KEEP); + } + + public void deletePicture() { + badImageSubscription.dispose(); + items.remove(currentStepIndex).imageQuality.onComplete(); + updateItemState(); + } + + public void subscribeBadPicture(Consumer consumer) { + badImageSubscription = getCurrentItem().imageQuality.subscribe(consumer, Timber::e); + } + + public boolean isLoggedIn() { + Account currentAccount = sessionManager.getCurrentAccount(); + return currentAccount != null; + } + + + @SuppressWarnings("WeakerAccess") + static class UploadItem { + public final Uri mediaUri; + public final String mimeType; + public final String source; + public final GPSExtractor gpsCoords; + + public boolean selected = false; + public boolean first = false; + public String fileExt; + public BehaviorSubject imageQuality; + Title title; + List descriptions; + public String wikidataEntityId; + public boolean visited; + public boolean error; + + @SuppressLint("CheckResult") + UploadItem(Uri mediaUri, String mimeType, String source, GPSExtractor gpsCoords, String fileExt, @Nullable String wikidataEntityId) { + title = new Title(); + descriptions = new ArrayList<>(); + descriptions.add(new Description()); + this.wikidataEntityId = wikidataEntityId; + + this.mediaUri = mediaUri; + this.mimeType = mimeType; + this.source = source; + this.gpsCoords = gpsCoords; + this.fileExt = fileExt; + imageQuality = BehaviorSubject.createDefault(ImageUtils.IMAGE_WAIT); +// imageQuality.subscribe(iq->Timber.i("New value of imageQuality:"+ImageUtils.IMAGE_OK)); + } + + public boolean isDummy() { + return false; + } + } + +} diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadPresenter.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadPresenter.java new file mode 100644 index 0000000000..a581e6e2d9 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadPresenter.java @@ -0,0 +1,333 @@ +package fr.free.nrw.commons.upload; + +import android.annotation.SuppressLint; +import android.net.Uri; +import android.os.Bundle; + +import java.lang.reflect.Proxy; +import java.util.Collections; +import java.util.List; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import fr.free.nrw.commons.category.CategoriesModel; +import fr.free.nrw.commons.contributions.Contribution; +import fr.free.nrw.commons.utils.ImageUtils; +import io.reactivex.Completable; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.schedulers.Schedulers; +import timber.log.Timber; + +/** + * The MVP pattern presenter of Upload GUI + */ +@Singleton +public class UploadPresenter { + private static final String TOP_CARD_STATE = "fr.free.nrw.commons.upload.top_card_state"; + private static final String BOTTOM_CARD_STATE = "fr.free.nrw.commons.upload.bottom_card_state"; + + private final UploadModel uploadModel; + private final UploadController uploadController; + private static final UploadView DUMMY = (UploadView) Proxy.newProxyInstance(UploadView.class.getClassLoader(), + new Class[]{UploadView.class}, (proxy, method, methodArgs) -> null); + private UploadView view = DUMMY; + + + @Inject + public UploadPresenter(UploadModel uploadModel, UploadController uploadController) { + this.uploadModel = uploadModel; + this.uploadController = uploadController; + } + + public void receive(Uri mediaUri, String mimeType, String source) { + receive(Collections.singletonList(mediaUri), mimeType, source); + } + + /** + * Passes the items received to {@link #uploadModel} and displays the items. + * + * @param media The Uri's of the media being uploaded. + * @param mimeType the mimeType of the files. + * @param source File source from {@link Contribution.FileSource} + */ + @SuppressLint("CheckResult") + public void receive(List media, String mimeType, @Contribution.FileSource String source) { + Completable.fromRunnable(() -> uploadModel.receive(media, mimeType, source)) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(() -> { + updateCards(); + updateLicenses(); + updateContent(); + if (uploadModel.isShowingItem()) + uploadModel.subscribeBadPicture(this::handleBadPicture); + }, Timber::e); + } + + /** + * Passes the direct upload item received to {@link #uploadModel} and displays the items. + * + * @param media The Uri's of the media being uploaded. + * @param mimeType the mimeType of the files. + * @param source File source from {@link Contribution.FileSource} + */ + @SuppressLint("CheckResult") + public void receiveDirect(Uri media, String mimeType, @Contribution.FileSource String source, String wikidataEntityIdPref, String title, String desc) { + Completable.fromRunnable(() -> uploadModel.receiveDirect(media, mimeType, source, wikidataEntityIdPref, title, desc)) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(() -> { + updateCards(); + updateLicenses(); + updateContent(); + if (uploadModel.isShowingItem()) + uploadModel.subscribeBadPicture(this::handleBadPicture); + }, Timber::e); + } + /** + * Sets the license to parameter and updates {@link UploadActivity} + * + * @param licenseName license name + */ + public void selectLicense(String licenseName) { + uploadModel.setSelectedLicense(licenseName); + view.updateLicenseSummary(uploadModel.getSelectedLicense()); + } + + //region Wizard step management + + /** + * Called by the next button in {@link UploadActivity} + */ + public void handleNext() { + uploadModel.next(); + updateContent(); + if (uploadModel.isShowingItem()) uploadModel.subscribeBadPicture(this::handleBadPicture); + view.dismissKeyboard(); + } + + /** + * Called by the previous button in {@link UploadActivity} + */ + public void handlePrevious() { + uploadModel.previous(); + updateContent(); + if (uploadModel.isShowingItem()) uploadModel.subscribeBadPicture(this::handleBadPicture); + view.dismissKeyboard(); + } + + /** + * Called when one of the pictures on the top card is clicked on in {@link UploadActivity} + */ + public void thumbnailClicked(UploadModel.UploadItem item) { + uploadModel.jumpTo(item); + updateContent(); + } + + /** + * Called by the submit button in {@link UploadActivity} + */ + @SuppressLint("CheckResult") + public void handleSubmit(CategoriesModel categoriesModel) { + if (view.checkIfLoggedIn()) + uploadModel.buildContributions(categoriesModel.getCategoryStringList()) + .observeOn(Schedulers.io()) + .subscribe(uploadController::startUpload); + } + + /** + * Called by the map button on the right card in {@link UploadActivity} + */ + public void openCoordinateMap() { + GPSExtractor gpsObj = uploadModel.getCurrentItem().gpsCoords; + if (gpsObj != null && gpsObj.imageCoordsExists) { + view.launchMapActivity(gpsObj.getDecLatitude() + "," + gpsObj.getDecLongitude()); + } + } + + + /** + * Called by the image processors when a result is obtained. + * + * @param result the result returned by the image procesors. + */ + private void handleBadPicture(@ImageUtils.Result int result) { + view.showBadPicturePopup(result); + } + + public void keepPicture() { + uploadModel.keepPicture(); + } + + public void deletePicture() { + if (uploadModel.getCount() == 1) + view.finish(); + else { + uploadModel.deletePicture(); + updateCards(); + updateContent(); + if (uploadModel.isShowingItem()) + uploadModel.subscribeBadPicture(this::handleBadPicture); + view.dismissKeyboard(); + } + } + //endregion + + //region Top Bottom and Right card state management + + + /** + * Toggles the top card's state between open and closed. + */ + public void toggleTopCardState() { + uploadModel.setTopCardState(!uploadModel.isTopCardState()); + view.setTopCardState(uploadModel.isTopCardState()); + } + + /** + * Toggles the bottom card's state between open and closed. + */ + public void toggleBottomCardState() { + uploadModel.setBottomCardState(!uploadModel.isBottomCardState()); + view.setBottomCardState(uploadModel.isBottomCardState()); + } + + /** + * Toggles the right card's state between open and closed. + */ + public void toggleRightCardState() { + uploadModel.setRightCardState(!uploadModel.isRightCardState()); + view.setRightCardState(uploadModel.isRightCardState()); + } + + /** + * Sets all the cards' states to closed. + */ + public void closeAllCards() { + if (uploadModel.isTopCardState()) { + uploadModel.setTopCardState(false); + view.setTopCardState(false); + } + if (uploadModel.isRightCardState()) { + uploadModel.setRightCardState(false); + view.setRightCardState(false); + } + if (uploadModel.isBottomCardState()) { + uploadModel.setBottomCardState(false); + view.setBottomCardState(false); + } + } + //endregion + + //region View / Lifecycle management + public void initFromSavedState(Bundle state) { + if (state != null) { + Timber.i("Saved state is not null."); + uploadModel.setTopCardState(state.getBoolean(TOP_CARD_STATE, true)); + uploadModel.setBottomCardState(state.getBoolean(BOTTOM_CARD_STATE, true)); + } + uploadController.prepareService(); + } + + public void cleanup() { + uploadController.cleanup(); + } + + public Bundle getSavedState() { + Bundle bundle = new Bundle(); + bundle.putBoolean(TOP_CARD_STATE, uploadModel.isTopCardState()); + bundle.putBoolean(BOTTOM_CARD_STATE, uploadModel.isBottomCardState()); + return bundle; + } + + public void removeView() { + this.view = DUMMY; + } + + public void addView(UploadView view) { + this.view = view; + + updateCards(); + updateLicenses(); + updateContent(); + } + + + /** + * Updates the cards for when there is a change to the amount of items being uploaded. + */ + private void updateCards() { + Timber.i("uploadModel.getCount():" + uploadModel.getCount()); + view.updateThumbnails(uploadModel.getUploads()); + view.setTopCardVisibility(uploadModel.getCount() > 1); + view.setBottomCardVisibility(uploadModel.getCount() > 0); + view.setTopCardState(uploadModel.isTopCardState()); + view.setBottomCardState(uploadModel.isBottomCardState()); + } + + /** + * Sets the list of licences and the default license. + */ + private void updateLicenses() { + String selectedLicense = uploadModel.getSelectedLicense(); + view.updateLicenses(uploadModel.getLicenses(), selectedLicense); + view.updateLicenseSummary(selectedLicense); + } + + /** + * Updates the cards and the background when a new page is selected. + */ + private void updateContent() { + Timber.i("Updating content for page" + uploadModel.getCurrentStep()); + view.setNextEnabled(uploadModel.isNextAvailable()); + view.setPreviousEnabled(uploadModel.isPreviousAvailable()); + view.setSubmitEnabled(uploadModel.isSubmitAvailable()); + + view.setBackground(uploadModel.getCurrentItem().mediaUri); + + view.updateBottomCardContent(uploadModel.getCurrentStep(), uploadModel.getStepCount(), uploadModel.getCurrentItem()); + + view.updateTopCardContent(); + + GPSExtractor gpsObj = uploadModel.getCurrentItem().gpsCoords; + view.updateRightCardContent(gpsObj != null && gpsObj.imageCoordsExists); + + showCorrectCards(uploadModel.getCurrentStep(), uploadModel.getCount()); + } + + /** + * Updates the layout to show the correct bottom card. + * + * @param currentStep the current step + * @param uploadCount how many items are being uploaded + */ + private void showCorrectCards(int currentStep, int uploadCount) { + @UploadView.UploadPage int page; + if (uploadCount == 0) { + page = UploadView.PLEASE_WAIT; + } else if (currentStep <= uploadCount) { + page = UploadView.TITLE_CARD; + view.setTopCardVisibility(uploadModel.getCount() > 1); + } else if (currentStep == uploadCount + 1) { + page = UploadView.CATEGORIES; + view.setTopCardVisibility(false); + view.setRightCardVisibility(false); + } else { + page = UploadView.LICENSE; + view.setTopCardVisibility(false); + view.setRightCardVisibility(false); + } + view.setBottomCardVisibility(page); + } + + //endregion + + /** + * @return the item currently being displayed + */ + public UploadModel.UploadItem getCurrentItem() { + return uploadModel.getCurrentItem(); + } + +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadService.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadService.java index 505811ab1e..42f9327279 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/UploadService.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadService.java @@ -10,7 +10,6 @@ import android.graphics.BitmapFactory; import android.os.Bundle; import android.support.v4.app.NotificationCompat; -import android.util.Log; import android.webkit.MimeTypeMap; import android.widget.Toast; @@ -27,6 +26,7 @@ import javax.inject.Inject; import fr.free.nrw.commons.BuildConfig; +import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.HandlerService; import fr.free.nrw.commons.R; import fr.free.nrw.commons.Utils; @@ -35,7 +35,6 @@ import fr.free.nrw.commons.contributions.ContributionDao; import fr.free.nrw.commons.contributions.ContributionsActivity; 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; @@ -60,7 +59,9 @@ public class UploadService extends HandlerService { private NotificationCompat.Builder curProgressNotification; private int toUpload; - // The file names of unfinished uploads, used to prevent overwriting + /** + * The file names of unfinished uploads, used to prevent overwriting + */ private Set unfinishedUploads = new HashSet<>(); // DO NOT HAVE NOTIFICATION ID OF 0 FOR ANYTHING @@ -182,6 +183,19 @@ public int onStartCommand(Intent intent, int flags, int startId) { } @SuppressLint("StringFormatInvalid") + private NotificationCompat.Builder getNotificationBuilder(Contribution contribution, String channelId) { + return new NotificationCompat.Builder(this, channelId).setAutoCancel(true) + .setSmallIcon(R.drawable.ic_launcher) + .setLargeIcon(BitmapFactory.decodeResource(getResources(), R.drawable.ic_launcher)) + .setAutoCancel(true) + .setContentTitle(getString(R.string.upload_progress_notification_title_start, contribution.getDisplayTitle())) + .setContentText(getResources().getQuantityString(R.plurals.uploads_pending_notification_indicator, toUpload, toUpload)) + .setOngoing(true) + .setProgress(100, 0, true) + .setContentIntent(PendingIntent.getActivity(this, 0, new Intent(this, ContributionsActivity.class), 0)) + .setTicker(getString(R.string.upload_progress_notification_title_in_progress, contribution.getDisplayTitle())); + } + private void uploadContribution(Contribution contribution) { InputStream fileInputStream; @@ -199,25 +213,10 @@ private void uploadContribution(Contribution contribution) { return; } - //As the fileInputStream is null there's no point in continuing the upload process - //mwapi.upload accepts a NonNull input stream - if(fileInputStream == null) { - Timber.d("File not found"); - return; - } - Timber.d("Before execution!"); - curProgressNotification = new NotificationCompat.Builder(this).setAutoCancel(true) - .setSmallIcon(R.drawable.ic_launcher) - .setLargeIcon(BitmapFactory.decodeResource(getResources(), R.drawable.ic_launcher)) - .setAutoCancel(true) - .setContentTitle(getString(R.string.upload_progress_notification_title_start, contribution.getDisplayTitle())) - .setContentText(getResources().getQuantityString(R.plurals.uploads_pending_notification_indicator, toUpload, toUpload)) - .setOngoing(true) - .setProgress(100, 0, true) - .setContentIntent(PendingIntent.getActivity(this, 0, new Intent(this, ContributionsActivity.class), 0)) - .setTicker(getString(R.string.upload_progress_notification_title_in_progress, contribution.getDisplayTitle())); - + curProgressNotification = getNotificationBuilder( + contribution, + CommonsApplication.NOTIFICATION_CHANNEL_ID_ALL); this.startForeground(NOTIFICATION_UPLOAD_IN_PROGRESS, curProgressNotification.build()); String filename = null; @@ -249,7 +248,7 @@ private void uploadContribution(Contribution contribution) { getString(R.string.upload_progress_notification_title_finishing, contribution.getDisplayTitle()), contribution ); - UploadResult uploadResult = mwApi.uploadFile(filename, fileInputStream, contribution.getDataLength(), contribution.getPageContents(), contribution.getEditSummary(), notificationUpdater, contribution.getLocalUri(), contribution.getContentProviderUri()); + UploadResult uploadResult = mwApi.uploadFile(filename, fileInputStream, contribution.getDataLength(), contribution.getPageContents(), contribution.getEditSummary(), contribution.getLocalUri(), contribution.getContentProviderUri(), notificationUpdater); Timber.d("Response is %s", uploadResult.toString()); @@ -285,6 +284,7 @@ private void uploadContribution(Contribution contribution) { } @SuppressLint("StringFormatInvalid") + @SuppressWarnings("deprecation") private void showFailedNotification(Contribution contribution) { Notification failureNotification = new NotificationCompat.Builder(this).setAutoCancel(true) .setSmallIcon(R.drawable.ic_launcher) diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadThumbnailRenderer.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadThumbnailRenderer.java new file mode 100644 index 0000000000..64b873da01 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadThumbnailRenderer.java @@ -0,0 +1,49 @@ +package fr.free.nrw.commons.upload; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; + +import com.facebook.drawee.view.SimpleDraweeView; +import com.pedrogomez.renderers.Renderer; + +import fr.free.nrw.commons.R; + +class UploadThumbnailRenderer extends Renderer { + private ThumbnailClickedListener listener; + private SimpleDraweeView background; + private View space; + private ImageView error; + + public UploadThumbnailRenderer(ThumbnailClickedListener listener) { + this.listener = listener; + } + + @Override + protected View inflate(LayoutInflater inflater, ViewGroup parent) { + return inflater.inflate(R.layout.item_upload_thumbnail, parent, false); + } + + @Override + protected void setUpView(View rootView) { + error = rootView.findViewById(R.id.error); + space = rootView.findViewById(R.id.left_space); + background = rootView.findViewById(R.id.thumbnail); + } + + @Override + protected void hookListeners(View rootView) { + background.setOnClickListener(v -> listener.thumbnailClicked(getContent())); + } + + @Override + public void render() { + UploadModel.UploadItem content = getContent(); + background.setImageURI(content.mediaUri); + background.setAlpha(content.selected ? 1.0f : 0.5f); + space.setVisibility(content.first ? View.VISIBLE : View.GONE); + error.setVisibility(content.visited && content.error ? View.VISIBLE : View.GONE); + } + +} diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadThumbnailsAdapterFactory.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadThumbnailsAdapterFactory.java new file mode 100644 index 0000000000..bc0a79c80b --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadThumbnailsAdapterFactory.java @@ -0,0 +1,26 @@ +package fr.free.nrw.commons.upload; + +import com.pedrogomez.renderers.ListAdapteeCollection; +import com.pedrogomez.renderers.RVRendererAdapter; +import com.pedrogomez.renderers.RendererBuilder; + +import java.util.Collections; +import java.util.List; + +import javax.inject.Inject; + +public class UploadThumbnailsAdapterFactory { + private ThumbnailClickedListener listener; + + UploadThumbnailsAdapterFactory(ThumbnailClickedListener listener) { + this.listener = listener; + } + + public RVRendererAdapter create(List placeList) { + RendererBuilder builder = new RendererBuilder() + .bind(UploadModel.UploadItem.class, new UploadThumbnailRenderer(listener)); + ListAdapteeCollection collection = new ListAdapteeCollection<>( + placeList != null ? placeList : Collections.emptyList()); + return new RVRendererAdapter<>(builder, collection); + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadView.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadView.java new file mode 100644 index 0000000000..0256f64837 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadView.java @@ -0,0 +1,74 @@ +package fr.free.nrw.commons.upload; + +import android.net.Uri; +import android.support.annotation.IntDef; + +import java.lang.annotation.Retention; +import java.util.List; + +import fr.free.nrw.commons.utils.ImageUtils; + +import static java.lang.annotation.RetentionPolicy.SOURCE; + +public interface UploadView { + // Dummy implementation of the view interface to allow us to have a 'null object pattern' + // in the presenter and avoid constant NULL checking. +// UploadView DUMMY = (UploadView) Proxy.newProxyInstance(UploadView.class.getClassLoader(), +// new Class[]{UploadView.class}, (proxy, method, methodArgs) -> null); + + List getDescriptions(); + + @Retention(SOURCE) + @IntDef({PLEASE_WAIT, TITLE_CARD, CATEGORIES, LICENSE}) + @interface UploadPage {} + + int PLEASE_WAIT = 0; + + int TITLE_CARD = 1; + int CATEGORIES = 2; + int LICENSE = 3; + + boolean checkIfLoggedIn(); + + void updateThumbnails(List uploads); + + void setNextEnabled(boolean available); + + void setSubmitEnabled(boolean available); + + void setPreviousEnabled(boolean available); + + void setTopCardState(boolean state); + + void setRightCardVisibility(boolean visible); + + void setBottomCardState(boolean state); + + void setRightCardState(boolean bottomCardState); + + void setBackground(Uri mediaUri); + + void setTopCardVisibility(boolean visible); + + void setBottomCardVisibility(boolean visible); + + void setBottomCardVisibility(@UploadPage int page); + + void updateRightCardContent(boolean gpsPresent); + + void updateBottomCardContent(int currentStep, int stepCount, UploadModel.UploadItem uploadItem); + + void updateLicenses(List licenses, String selectedLicense); + + void updateLicenseSummary(String selectedLicense); + + void updateTopCardContent(); + + void dismissKeyboard(); + + void showBadPicturePopup(@ImageUtils.Result int errorMessage); + + void finish(); + + void launchMapActivity(String decCoords); +} diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UrlLicense.java b/app/src/main/java/fr/free/nrw/commons/upload/UrlLicense.java index 875dde26a5..ef7a3b8f08 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/UrlLicense.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/UrlLicense.java @@ -8,61 +8,62 @@ * info in the user language */ public class UrlLicense { - HashMap urlLicense = new HashMap(); - public void initialize(){ - urlLicense.put("en","https://commons.wikimedia.org/wiki/Commons:Licensing"); - urlLicense.put("ar","https://commons.wikimedia.org/wiki/Commons:Licensing/ar"); - urlLicense.put("ast","https://commons.wikimedia.org/wiki/Commons:Licensing/ast"); - urlLicense.put("az","https://commons.wikimedia.org/wiki/Commons:Licensing/az"); - urlLicense.put("be","https://commons.wikimedia.org/wiki/Commons:Licensing/be"); - urlLicense.put("bg","https://commons.wikimedia.org/wiki/Commons:Licensing/bg"); - urlLicense.put("bn","https://commons.wikimedia.org/wiki/Commons:Licensing/bn"); - urlLicense.put("ca","https://commons.wikimedia.org/wiki/Commons:Licensing/ca"); - urlLicense.put("cs","https://commons.wikimedia.org/wiki/Commons:Licensing/cs"); - urlLicense.put("da","https://commons.wikimedia.org/wiki/Commons:Licensing/da"); - urlLicense.put("de","https://commons.wikimedia.org/wiki/Commons:Licensing/de"); - urlLicense.put("el","https://commons.wikimedia.org/wiki/Commons:Licensing/el"); - urlLicense.put("eo","https://commons.wikimedia.org/wiki/Commons:Licensing/eo"); - urlLicense.put("es","https://commons.wikimedia.org/wiki/Commons:Licensing/es"); - urlLicense.put("eu","https://commons.wikimedia.org/wiki/Commons:Licensing/eu"); - urlLicense.put("fa","https://commons.wikimedia.org/wiki/Commons:Licensing/fa"); - urlLicense.put("fi","https://commons.wikimedia.org/wiki/Commons:Licensing/fi"); - urlLicense.put("fr","https://commons.wikimedia.org/wiki/Commons:Licensing/fr"); - urlLicense.put("gl","https://commons.wikimedia.org/wiki/Commons:Licensing/gl"); - urlLicense.put("gsw","https://commons.wikimedia.org/wiki/Commons:Licensing/gsw"); - urlLicense.put("he","https://commons.wikimedia.org/wiki/Commons:Licensing/he"); - urlLicense.put("hi","https://commons.wikimedia.org/wiki/Commons:Licensing/hi"); - urlLicense.put("hu","https://commons.wikimedia.org/wiki/Commons:Licensing/hu"); - urlLicense.put("id","https://commons.wikimedia.org/wiki/Commons:Licensing/id"); - urlLicense.put("is","https://commons.wikimedia.org/wiki/Commons:Licensing/is"); - urlLicense.put("it","https://commons.wikimedia.org/wiki/Commons:Licensing/it"); - urlLicense.put("ja","https://commons.wikimedia.org/wiki/Commons:Licensing/ja"); - urlLicense.put("ka","https://commons.wikimedia.org/wiki/Commons:Licensing/ka"); - urlLicense.put("km","https://commons.wikimedia.org/wiki/Commons:Licensing/km"); - urlLicense.put("ko","https://commons.wikimedia.org/wiki/Commons:Licensing/ko"); - urlLicense.put("ku","https://commons.wikimedia.org/wiki/Commons:Licensing/ku"); - urlLicense.put("mk","https://commons.wikimedia.org/wiki/Commons:Licensing/mk"); - urlLicense.put("mr","https://commons.wikimedia.org/wiki/Commons:Licensing/mr"); - urlLicense.put("ms","https://commons.wikimedia.org/wiki/Commons:Licensing/ms"); - urlLicense.put("my","https://commons.wikimedia.org/wiki/Commons:Licensing/my"); - urlLicense.put("nl","https://commons.wikimedia.org/wiki/Commons:Licensing/nl"); - urlLicense.put("oc","https://commons.wikimedia.org/wiki/Commons:Licensing/oc"); - urlLicense.put("pl","https://commons.wikimedia.org/wiki/Commons:Licensing/pl"); - urlLicense.put("pt","https://commons.wikimedia.org/wiki/Commons:Licensing/pt"); - urlLicense.put("pt-br","https://commons.wikimedia.org/wiki/Commons:Licensing/pt-br"); - urlLicense.put("ro","https://commons.wikimedia.org/wiki/Commons:Licensing/ro"); - urlLicense.put("ru","https://commons.wikimedia.org/wiki/Commons:Licensing/ru"); - urlLicense.put("scn","https://commons.wikimedia.org/wiki/Commons:Licensing/scn"); - urlLicense.put("sk","https://commons.wikimedia.org/wiki/Commons:Licensing/sk"); - urlLicense.put("sl","https://commons.wikimedia.org/wiki/Commons:Licensing/sl"); - urlLicense.put("sv","https://commons.wikimedia.org/wiki/Commons:Licensing/sv"); - urlLicense.put("tr","https://commons.wikimedia.org/wiki/Commons:Licensing/tr"); - urlLicense.put("uk","https://commons.wikimedia.org/wiki/Commons:Licensing/uk"); - urlLicense.put("ur","https://commons.wikimedia.org/wiki/Commons:Licensing/ur"); - urlLicense.put("vi","https://commons.wikimedia.org/wiki/Commons:Licensing/vi"); - urlLicense.put("zh","https://commons.wikimedia.org/wiki/Commons:Licensing/zh"); + public static HashMap urlLicense = new HashMap<>(); + static { + urlLicense.put("en", "https://commons.wikimedia.org/wiki/Commons:Licensing"); + urlLicense.put("ar", "https://commons.wikimedia.org/wiki/Commons:Licensing/ar"); + urlLicense.put("ast", "https://commons.wikimedia.org/wiki/Commons:Licensing/ast"); + urlLicense.put("az", "https://commons.wikimedia.org/wiki/Commons:Licensing/az"); + urlLicense.put("be", "https://commons.wikimedia.org/wiki/Commons:Licensing/be"); + urlLicense.put("bg", "https://commons.wikimedia.org/wiki/Commons:Licensing/bg"); + urlLicense.put("bn", "https://commons.wikimedia.org/wiki/Commons:Licensing/bn"); + urlLicense.put("ca", "https://commons.wikimedia.org/wiki/Commons:Licensing/ca"); + urlLicense.put("cs", "https://commons.wikimedia.org/wiki/Commons:Licensing/cs"); + urlLicense.put("da", "https://commons.wikimedia.org/wiki/Commons:Licensing/da"); + urlLicense.put("de", "https://commons.wikimedia.org/wiki/Commons:Licensing/de"); + urlLicense.put("el", "https://commons.wikimedia.org/wiki/Commons:Licensing/el"); + urlLicense.put("eo", "https://commons.wikimedia.org/wiki/Commons:Licensing/eo"); + urlLicense.put("es", "https://commons.wikimedia.org/wiki/Commons:Licensing/es"); + urlLicense.put("eu", "https://commons.wikimedia.org/wiki/Commons:Licensing/eu"); + urlLicense.put("fa", "https://commons.wikimedia.org/wiki/Commons:Licensing/fa"); + urlLicense.put("fi", "https://commons.wikimedia.org/wiki/Commons:Licensing/fi"); + urlLicense.put("fr", "https://commons.wikimedia.org/wiki/Commons:Licensing/fr"); + urlLicense.put("gl", "https://commons.wikimedia.org/wiki/Commons:Licensing/gl"); + urlLicense.put("gsw", "https://commons.wikimedia.org/wiki/Commons:Licensing/gsw"); + urlLicense.put("he", "https://commons.wikimedia.org/wiki/Commons:Licensing/he"); + urlLicense.put("hi", "https://commons.wikimedia.org/wiki/Commons:Licensing/hi"); + urlLicense.put("hu", "https://commons.wikimedia.org/wiki/Commons:Licensing/hu"); + urlLicense.put("id", "https://commons.wikimedia.org/wiki/Commons:Licensing/id"); + urlLicense.put("is", "https://commons.wikimedia.org/wiki/Commons:Licensing/is"); + urlLicense.put("it", "https://commons.wikimedia.org/wiki/Commons:Licensing/it"); + urlLicense.put("ja", "https://commons.wikimedia.org/wiki/Commons:Licensing/ja"); + urlLicense.put("ka", "https://commons.wikimedia.org/wiki/Commons:Licensing/ka"); + urlLicense.put("km", "https://commons.wikimedia.org/wiki/Commons:Licensing/km"); + urlLicense.put("ko", "https://commons.wikimedia.org/wiki/Commons:Licensing/ko"); + urlLicense.put("ku", "https://commons.wikimedia.org/wiki/Commons:Licensing/ku"); + urlLicense.put("mk", "https://commons.wikimedia.org/wiki/Commons:Licensing/mk"); + urlLicense.put("mr", "https://commons.wikimedia.org/wiki/Commons:Licensing/mr"); + urlLicense.put("ms", "https://commons.wikimedia.org/wiki/Commons:Licensing/ms"); + urlLicense.put("my", "https://commons.wikimedia.org/wiki/Commons:Licensing/my"); + urlLicense.put("nl", "https://commons.wikimedia.org/wiki/Commons:Licensing/nl"); + urlLicense.put("oc", "https://commons.wikimedia.org/wiki/Commons:Licensing/oc"); + urlLicense.put("pl", "https://commons.wikimedia.org/wiki/Commons:Licensing/pl"); + urlLicense.put("pt", "https://commons.wikimedia.org/wiki/Commons:Licensing/pt"); + urlLicense.put("pt-br", "https://commons.wikimedia.org/wiki/Commons:Licensing/pt-br"); + urlLicense.put("ro", "https://commons.wikimedia.org/wiki/Commons:Licensing/ro"); + urlLicense.put("ru", "https://commons.wikimedia.org/wiki/Commons:Licensing/ru"); + urlLicense.put("scn", "https://commons.wikimedia.org/wiki/Commons:Licensing/scn"); + urlLicense.put("sk", "https://commons.wikimedia.org/wiki/Commons:Licensing/sk"); + urlLicense.put("sl", "https://commons.wikimedia.org/wiki/Commons:Licensing/sl"); + urlLicense.put("sv", "https://commons.wikimedia.org/wiki/Commons:Licensing/sv"); + urlLicense.put("tr", "https://commons.wikimedia.org/wiki/Commons:Licensing/tr"); + urlLicense.put("uk", "https://commons.wikimedia.org/wiki/Commons:Licensing/uk"); + urlLicense.put("ur", "https://commons.wikimedia.org/wiki/Commons:Licensing/ur"); + urlLicense.put("vi", "https://commons.wikimedia.org/wiki/Commons:Licensing/vi"); + urlLicense.put("zh", "https://commons.wikimedia.org/wiki/Commons:Licensing/zh"); } - public String getLicenseUrl ( String language){ + + public static String getLicenseUrl ( String language){ if(urlLicense.containsKey(language)) { return urlLicense.get(language); } else { diff --git a/app/src/main/java/fr/free/nrw/commons/utils/AbstractTextWatcher.java b/app/src/main/java/fr/free/nrw/commons/utils/AbstractTextWatcher.java new file mode 100644 index 0000000000..92f9f19355 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/utils/AbstractTextWatcher.java @@ -0,0 +1,30 @@ +package fr.free.nrw.commons.utils; + +import android.support.annotation.NonNull; +import android.text.Editable; +import android.text.TextWatcher; + +public class AbstractTextWatcher implements TextWatcher { + private final TextChange textChange; + + public AbstractTextWatcher(@NonNull TextChange textChange) { + this.textChange = textChange; + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + textChange.onTextChanged(s.toString()); + } + + @Override + public void afterTextChanged(Editable s) { + } + + public interface TextChange { + void onTextChanged(String value); + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/BiMap.java b/app/src/main/java/fr/free/nrw/commons/utils/BiMap.java new file mode 100644 index 0000000000..227f5f024d --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/utils/BiMap.java @@ -0,0 +1,41 @@ +package fr.free.nrw.commons.utils; + +import java.util.HashMap; +import java.util.Set; + +/** + * HashMap that can be searched in both the forward and reverse directions. + */ +public class BiMap { + + private HashMap map = new HashMap(); + private HashMap inversedMap = new HashMap(); + + public void put(K k, V v) { + map.put(k, v); + inversedMap.put(v, k); + } + + public V get(K k) { + return map.get(k); + } + + public K getKey(V v) { + return inversedMap.get(v); + } + + public Set getEntrySet(){ + return inversedMap.keySet(); + } + + public void remove(K k){ + inversedMap.remove(map.remove(k)); + } + + + public boolean containsKey(V v){ + return inversedMap.containsKey(v); + } + +} + diff --git a/app/src/main/java/fr/free/nrw/commons/utils/DialogUtil.java b/app/src/main/java/fr/free/nrw/commons/utils/DialogUtil.java index 78c1ca155e..2e4592e400 100644 --- a/app/src/main/java/fr/free/nrw/commons/utils/DialogUtil.java +++ b/app/src/main/java/fr/free/nrw/commons/utils/DialogUtil.java @@ -1,12 +1,16 @@ package fr.free.nrw.commons.utils; import android.app.Activity; +import android.app.AlertDialog; +import android.app.AlertDialog.Builder; import android.app.Dialog; +import android.content.Context; import android.os.Build; import android.support.annotation.Nullable; import android.support.v4.app.DialogFragment; import android.support.v4.app.FragmentActivity; +import fr.free.nrw.commons.R; import timber.log.Timber; public class DialogUtil { @@ -92,4 +96,31 @@ public static void showSafely(FragmentActivity activity, DialogFragment dialog) Timber.e(e, "Could not show dialog."); } } + + public static AlertDialog getAlertDialogWithPositiveAndNegativeCallbacks( + Context context, String title, String message, int iconResourceId, Callback callback) { + + AlertDialog alertDialog = new Builder(context) + .setTitle(title) + .setMessage(message) + .setPositiveButton(context.getString(R.string.ok), (dialog, which) -> { + callback.onPositiveButtonClicked(); + dialog.dismiss(); + }) + .setNegativeButton(context.getString(R.string.cancel), (dialog, which) -> { + callback.onNegativeButtonClicked(); + dialog.dismiss(); + }) + .setIcon(iconResourceId).create(); + + return alertDialog; + + } + + public interface Callback { + + void onPositiveButtonClicked(); + + void onNegativeButtonClicked(); + } } diff --git a/app/src/main/java/fr/free/nrw/commons/utils/ImageUtils.java b/app/src/main/java/fr/free/nrw/commons/utils/ImageUtils.java index 79dad33e52..c98e5c56af 100644 --- a/app/src/main/java/fr/free/nrw/commons/utils/ImageUtils.java +++ b/app/src/main/java/fr/free/nrw/commons/utils/ImageUtils.java @@ -7,6 +7,7 @@ import android.graphics.Color; import android.graphics.Rect; import android.net.Uri; +import android.support.annotation.IntDef; import android.support.annotation.Nullable; import com.facebook.common.executors.CallerThreadExecutor; @@ -20,6 +21,8 @@ import com.facebook.imagepipeline.request.ImageRequestBuilder; import java.io.IOException; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import fr.free.nrw.commons.R; import timber.log.Timber; @@ -35,9 +38,26 @@ public class ImageUtils { private static final double MINIMUM_BLURRYNESS_FACTOR = 0.50; private static final int LAPLACIAN_VARIANCE_THRESHOLD = 70; - public enum Result { - IMAGE_DARK, - IMAGE_OK + public static final int IMAGE_DARK = 1; + public static final int IMAGE_BLURRY = 1 << 1; + public static final int IMAGE_DUPLICATE = 1 << 2; + public static final int IMAGE_OK = 0; + public static final int IMAGE_KEEP = -1; + public static final int IMAGE_WAIT = -2; + + @IntDef( + flag = true, + value = { + IMAGE_DARK, + IMAGE_BLURRY, + IMAGE_DUPLICATE, + IMAGE_OK, + IMAGE_KEEP, + IMAGE_WAIT + } + ) + @Retention(RetentionPolicy.SOURCE) + public @interface Result { } /** @@ -49,13 +69,14 @@ public enum Result { * was divided, we will declare the image as wanted/unwanted * * @param bitmapRegionDecoder BitmapRegionDecoder for the image we wish to process - * @return Result.IMAGE_OK if image is neither dark nor blurry or if the input bitmapRegionDecoder provided is null - * Result.IMAGE_DARK if image is too dark + * @return IMAGE_OK if image is neither dark nor blurry or if the input bitmapRegionDecoder provided is null + * IMAGE_DARK if image is too dark */ - public static Result checkIfImageIsTooDark(BitmapRegionDecoder bitmapRegionDecoder) { + public static @Result + int checkIfImageIsTooDark(BitmapRegionDecoder bitmapRegionDecoder) { if (bitmapRegionDecoder == null) { Timber.e("Expected bitmapRegionDecoder was null"); - return Result.IMAGE_OK; + return IMAGE_OK; } int loadImageHeight = bitmapRegionDecoder.getHeight(); @@ -73,10 +94,10 @@ public static Result checkIfImageIsTooDark(BitmapRegionDecoder bitmapRegionDecod while ((checkImageBottomPosition <= loadImageHeight) && (checkImageTopPosition < checkImageBottomPosition)) { Timber.v("left: " + checkImageLeftPosition + " right: " + checkImageRightPosition + " top: " + checkImageTopPosition + " bottom: " + checkImageBottomPosition); - Rect rect = new Rect(checkImageLeftPosition,checkImageTopPosition,checkImageRightPosition,checkImageBottomPosition); + Rect rect = new Rect(checkImageLeftPosition, checkImageTopPosition, checkImageRightPosition, checkImageBottomPosition); totalDividedRectangles++; - Bitmap processBitmap = bitmapRegionDecoder.decodeRegion(rect,null); + Bitmap processBitmap = bitmapRegionDecoder.decodeRegion(rect, null); if (checkIfImageIsDark(processBitmap)) { numberOfDarkRectangles++; @@ -95,10 +116,10 @@ public static Result checkIfImageIsTooDark(BitmapRegionDecoder bitmapRegionDecod Timber.d("dark rectangles count = " + numberOfDarkRectangles + ", total rectangles count = " + totalDividedRectangles); if (numberOfDarkRectangles > totalDividedRectangles * MINIMUM_DARKNESS_FACTOR) { - return Result.IMAGE_DARK; + return IMAGE_DARK; } - return Result.IMAGE_OK; + return IMAGE_OK; } /** @@ -107,7 +128,7 @@ public static Result checkIfImageIsTooDark(BitmapRegionDecoder bitmapRegionDecod * and then applying the formula to calculate its "Luminance". If this brightness value is less than * 50 then the pixel is considered to be dark. Based on the MINIMUM_DARKNESS_FACTOR if enough pixels * are dark then the entire bitmap is considered to be dark. - * + *

*

For more information on this brightness/darkness calculation technique refer the accepted answer * on this -> https://stackoverflow.com/questions/35914461/how-to-detect-dark-photos-in-android/35914745 * SO question and follow the trail. @@ -127,7 +148,7 @@ private static boolean checkIfImageIsDark(Bitmap bitmap) { int allPixelsCount = bitmapWidth * bitmapHeight; int[] bitmapPixels = new int[allPixelsCount]; - bitmap.getPixels(bitmapPixels,0,bitmapWidth,0,0,bitmapWidth,bitmapHeight); + bitmap.getPixels(bitmapPixels, 0, bitmapWidth, 0, 0, bitmapWidth, bitmapHeight); boolean isImageDark = false; int darkPixelsCount = 0; @@ -153,8 +174,9 @@ private static boolean checkIfImageIsDark(Bitmap bitmap) { /** * 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 + * + * @param context context + * @param imageUrl Url of the image */ public static void setWallpaperFromImageUrl(Context context, Uri imageUrl) { Timber.d("Trying to set wallpaper from url %s", imageUrl.toString()); @@ -171,7 +193,7 @@ public static void setWallpaperFromImageUrl(Context context, Uri imageUrl) { @Override public void onNewResultImpl(@Nullable Bitmap bitmap) { - if (dataSource.isFinished() && bitmap != null){ + if (dataSource.isFinished() && bitmap != null) { Timber.d("Bitmap loaded from url %s", imageUrl.toString()); setWallpaper(context, Bitmap.createBitmap(bitmap)); dataSource.close(); @@ -194,7 +216,7 @@ private static void setWallpaper(Context context, Bitmap bitmap) { wallpaperManager.setBitmap(bitmap); ViewUtil.showLongToast(context, context.getString(R.string.wallpaper_set_successfully)); } catch (IOException e) { - Timber.e(e,"Error setting wallpaper"); + Timber.e(e, "Error setting wallpaper"); } } } diff --git a/app/src/main/java/fr/free/nrw/commons/utils/PermissionUtils.java b/app/src/main/java/fr/free/nrw/commons/utils/PermissionUtils.java new file mode 100644 index 0000000000..ecdc015119 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/utils/PermissionUtils.java @@ -0,0 +1,23 @@ +package fr.free.nrw.commons.utils; + +import android.app.Activity; +import android.content.Intent; +import android.net.Uri; +import android.provider.Settings; +import fr.free.nrw.commons.CommonsApplication; + +public class PermissionUtils { + + /** + * This method can be used by any activity which requires a permission which has been blocked(marked never ask again by the user) + It open the app settings from where the user can manually give us the required permission. + * @param activity + */ + public static void askUserToManuallyEnablePermissionFromSettings( + Activity activity) { + Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); + Uri uri = Uri.fromParts("package", activity.getPackageName(), null); + intent.setData(uri); + activity.startActivityForResult(intent,CommonsApplication.OPEN_APPLICATION_DETAIL_SETTINGS); + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/widget/PicOfDayAppWidget.java b/app/src/main/java/fr/free/nrw/commons/widget/PicOfDayAppWidget.java index 82bad3f097..c7c0cfb814 100644 --- a/app/src/main/java/fr/free/nrw/commons/widget/PicOfDayAppWidget.java +++ b/app/src/main/java/fr/free/nrw/commons/widget/PicOfDayAppWidget.java @@ -3,18 +3,27 @@ import android.appwidget.AppWidgetManager; import android.appwidget.AppWidgetProvider; import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.net.Uri; +import android.support.annotation.Nullable; import android.widget.RemoteViews; +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 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; @@ -27,10 +36,7 @@ */ public class PicOfDayAppWidget extends AppWidgetProvider { - static void updateAppWidget(Context context, AppWidgetManager appWidgetManager, - int appWidgetId) { - - // Construct the RemoteViews object + static void updateAppWidget(Context context, AppWidgetManager appWidgetManager, int appWidgetId) { RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.pic_of_day_app_widget); String urlString = BuildConfig.WIKIMEDIA_API_POTD; @@ -45,19 +51,37 @@ public void onTaskCompleted(ArrayList

list) { 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}); + + ImageRequest request = ImageRequestBuilder.newBuilderWithSource(Uri.parse(imageUrl)).build(); + ImagePipeline imagePipeline = Fresco.getImagePipeline(); + DataSource> dataSource + = imagePipeline.fetchDecodedImage(request, context); + dataSource.subscribe(new BaseBitmapDataSubscriber() { + @Override + protected void onNewResultImpl(@Nullable Bitmap tempBitmap) { + Bitmap bitmap = null; + if (tempBitmap != null) { + bitmap = Bitmap.createBitmap(tempBitmap.getWidth(), tempBitmap.getHeight(), Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bitmap); + canvas.drawBitmap(tempBitmap, 0f, 0f, new Paint()); + } + views.setImageViewBitmap(R.id.appwidget_image, bitmap); + appWidgetManager.updateAppWidget(appWidgetId, views); + } + + @Override + protected void onFailureImpl(DataSource> dataSource) { + // Ignore failure for now. + } + }, CallerThreadExecutor.getInstance()); } } - } @Override public void onError() { } }); - - // Instruct the widget manager to update the widget - appWidgetManager.updateAppWidget(appWidgetId, views); } @Override diff --git a/app/src/main/res/drawable/ic_error_red_24dp.xml b/app/src/main/res/drawable/ic_error_red_24dp.xml new file mode 100644 index 0000000000..e1569395b1 --- /dev/null +++ b/app/src/main/res/drawable/ic_error_red_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_expand_less_black_24dp.xml b/app/src/main/res/drawable/ic_expand_less_black_24dp.xml new file mode 100644 index 0000000000..3afdf96826 --- /dev/null +++ b/app/src/main/res/drawable/ic_expand_less_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_expand_less_white_24dp.xml b/app/src/main/res/drawable/ic_expand_less_white_24dp.xml new file mode 100644 index 0000000000..d58421a2f4 --- /dev/null +++ b/app/src/main/res/drawable/ic_expand_less_white_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_expand_more_black_24dp.xml b/app/src/main/res/drawable/ic_expand_more_black_24dp.xml new file mode 100644 index 0000000000..8d57dbc10f --- /dev/null +++ b/app/src/main/res/drawable/ic_expand_more_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_expand_more_white_24dp.xml b/app/src/main/res/drawable/ic_expand_more_white_24dp.xml new file mode 100644 index 0000000000..fd3ce4a468 --- /dev/null +++ b/app/src/main/res/drawable/ic_expand_more_white_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/activity_login.xml b/app/src/main/res/layout/activity_login.xml index 5d1345bd34..0470cf720c 100644 --- a/app/src/main/res/layout/activity_login.xml +++ b/app/src/main/res/layout/activity_login.xml @@ -207,6 +207,16 @@ android:layout_marginBottom="@dimen/standard_gap" android:text="@string/forgot_password" /> + + diff --git a/app/src/main/res/layout/activity_upload.xml b/app/src/main/res/layout/activity_upload.xml new file mode 100644 index 0000000000..01414ff9fe --- /dev/null +++ b/app/src/main/res/layout/activity_upload.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_upload_bottom_card.xml b/app/src/main/res/layout/activity_upload_bottom_card.xml new file mode 100644 index 0000000000..5d1d7d7567 --- /dev/null +++ b/app/src/main/res/layout/activity_upload_bottom_card.xml @@ -0,0 +1,89 @@ + + + + + + + + + + + +