diff --git a/CREDITS b/CREDITS index 6847ac9b6..a620e5a88 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 696201029..70a5128aa 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,31 @@ We try to have an extensive documentation at [our wiki here at Github][5]: * [Volunteers Welcome!][9] * [Developer Documentation][8] +## Libraries Used ## + +* [Picasso][11] +* [RSS-Parser][12] +* [ViewPagerIndicator][13] +* [PhotoView][14] +* [Acra][15] +* [Renderers][16] +* [Gson][17] +* [Timber][18] +* [Java-String-Similarity][19] +* [ReadMoreTextView][20] +* [MaterialShowcaseView][21] +* [Butterknife][22] +* [OKHttp][23] +* [Okio][24] +* [RxJava][25] +* [JSoup][26] +* [Fresco][27] +* [Stetho][28] +* [Dagger][29] +* [Java-HTTP-Fluent][30] +* [CircleProgressBar][31] +* [Leak Canary][32] + ## License ## This software is open source, licensed under the [Apache License 2.0][4]. @@ -34,3 +59,25 @@ This software is open source, licensed under the [Apache License 2.0][4]. [8]: https://github.com/commons-app/apps-android-commons/wiki#developer-documentation [9]: https://github.com/commons-app/apps-android-commons/wiki/Volunteers-welcome%21 [10]: https://meta.wikimedia.org/wiki/Grants:Project/Improve_%27Upload_to_Commons%27_Android_App/Renewal +[11]: https://github.com/square/picasso +[12]: https://github.com/prof18/RSS-Parser +[13]: https://github.com/avianey/Android-ViewPagerIndicator +[14]: https://github.com/chrisbanes/PhotoView +[15]: https://github.com/ACRA/acra +[16]: https://github.com/pedrovgs/Renderers +[17]: https://github.com/google/gson +[18]: https://github.com/JakeWharton/timber +[19]: https://github.com/tdebatty/java-string-similarity +[20]: https://github.com/bravoborja/ReadMoreTextView +[21]: https://github.com/deano2390/MaterialShowcaseView +[22]: https://github.com/JakeWharton/butterknife +[23]: https://github.com/square/okhttp +[24]: https://github.com/square/okio +[25]: https://github.com/ReactiveX/RxJava +[26]: https://github.com/jhy/jsoup +[27]: https://github.com/facebook/fresco +[28]: https://github.com/facebook/stetho +[29]: https://github.com/google/dagger +[30]: https://github.com/yuvipanda/java-http-fluent/blob/master/src/main/java/in/yuvi/http/fluent/Http.java +[31]: https://github.com/dinuscxj/CircleProgressBar +[32]: https://github.com/square/leakcanary diff --git a/app/build.gradle b/app/build.gradle index 9f9ff00ed..994710fda 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' @@ -40,7 +39,6 @@ dependencies { implementation 'io.reactivex.rxjava2:rxandroid:2.0.1' // Because RxAndroid releases are few and far between, it is recommended you also // explicitly depend on RxJava's latest version for bug fixes and new features. - implementation 'com.android.support:multidex:1.0.3' 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 +51,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' @@ -63,9 +60,6 @@ dependencies { implementation 'com.dinuscxj:circleprogressbar:1.1.1' implementation 'com.tspoon.traceur:traceur:1.0.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' @@ -78,8 +72,9 @@ dependencies { releaseImplementation "com.squareup.leakcanary:leakcanary-android-no-op:$LEAK_CANARY" testImplementation "com.squareup.leakcanary:leakcanary-android-no-op:$LEAK_CANARY" - implementation 'com.borjabravo:readmoretextview:2.1.0' - implementation 'com.dinuscxj:circleprogressbar:1.1.1' + //For handling runtime permissions + implementation 'com.karumi:dexter:5.0.0' + } android { @@ -98,8 +93,6 @@ android { targetSdkVersion project.targetSdkVersion testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" vectorDrawables.useSupportLibrary = true - - multiDexEnabled true } testOptions { @@ -120,7 +113,7 @@ android { buildTypes { release { minifyEnabled false // See https://stackoverflow.com/questions/40232404/google-play-apk-and-android-studio-apk-usb-debug-behaving-differently - proguard.cfg modification alone insufficient. - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt', 'proguard-glide.txt' + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt' } debug { testCoverageEnabled true @@ -152,6 +145,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' } @@ -178,6 +172,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 ef3437660..000000000 --- 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 e80e2a84f..12cf53f79 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -190,7 +190,7 @@ diff --git a/app/src/main/java/fr/free/nrw/commons/AboutActivity.java b/app/src/main/java/fr/free/nrw/commons/AboutActivity.java index c8941dcd8..a8d2d74fc 100644 --- a/app/src/main/java/fr/free/nrw/commons/AboutActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/AboutActivity.java @@ -144,11 +144,12 @@ public class AboutActivity extends NavigationBaseActivity { @OnClick(R.id.about_translate) public void launchTranslate(View view) { final ArrayAdapter languageAdapter = new ArrayAdapter(AboutActivity.this, - android.R.layout.simple_spinner_item, language); + android.R.layout.simple_spinner_dropdown_item, language); final Spinner spinner = new Spinner(AboutActivity.this); spinner.setLayoutParams(new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT)); spinner.setAdapter(languageAdapter); spinner.setGravity(17); + spinner.setPadding(50,0,0,0); AlertDialog.Builder builder = new AlertDialog.Builder(AboutActivity.this); builder.setView(spinner); 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 5fcab1d0b..e2c7f367b 100644 --- a/app/src/main/java/fr/free/nrw/commons/CommonsApplication.java +++ b/app/src/main/java/fr/free/nrw/commons/CommonsApplication.java @@ -1,10 +1,14 @@ package fr.free.nrw.commons; import android.annotation.SuppressLint; +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; @@ -44,7 +48,7 @@ import timber.log.Timber; 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; @@ -53,6 +57,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"; @@ -63,6 +72,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; @@ -101,10 +116,23 @@ public class CommonsApplication extends MultiDexApplication { 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 affb57528..16941b35a 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 @@ package fr.free.nrw.commons; 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 @@ public class MediaDataExtractor { 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/WelcomeActivity.java b/app/src/main/java/fr/free/nrw/commons/WelcomeActivity.java index c1bc37f46..74247a02f 100644 --- a/app/src/main/java/fr/free/nrw/commons/WelcomeActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/WelcomeActivity.java @@ -19,6 +19,7 @@ public class WelcomeActivity extends BaseActivity { private WelcomePagerAdapter adapter = new WelcomePagerAdapter(); private boolean isQuiz; + static String moreInformation; /** * Initialises exiting fields and dependencies @@ -30,6 +31,8 @@ public class WelcomeActivity extends BaseActivity { super.onCreate(savedInstanceState); setContentView(R.layout.activity_welcome); + moreInformation = this.getString(R.string.welcome_help_button_text); + if(getIntent() != null) { Bundle bundle = getIntent().getExtras(); if (bundle != null) { diff --git a/app/src/main/java/fr/free/nrw/commons/WelcomePagerAdapter.java b/app/src/main/java/fr/free/nrw/commons/WelcomePagerAdapter.java index bca548632..8bc82be98 100644 --- a/app/src/main/java/fr/free/nrw/commons/WelcomePagerAdapter.java +++ b/app/src/main/java/fr/free/nrw/commons/WelcomePagerAdapter.java @@ -3,6 +3,7 @@ package fr.free.nrw.commons; import android.net.Uri; import android.support.annotation.Nullable; import android.support.v4.view.PagerAdapter; +import android.text.Html; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -66,6 +67,13 @@ public class WelcomePagerAdapter extends PagerAdapter { } ViewHolder holder = new ViewHolder(layout); layout.setTag(holder); + + if(position == PAGE_FINAL){ + TextView moreInfo = layout.findViewById(R.id.welcomeInfo); + moreInfo.setText(Html.fromHtml(WelcomeActivity.moreInformation)); + ViewHolder holder1 = new ViewHolder(layout); + layout.setTag(holder1); + } } else { if (position == PAGE_FINAL) { ViewHolder holder = new ViewHolder(layout); diff --git a/app/src/main/java/fr/free/nrw/commons/achievements/AchievementsActivity.java b/app/src/main/java/fr/free/nrw/commons/achievements/AchievementsActivity.java index 1bc1cc27d..73143fb80 100644 --- a/app/src/main/java/fr/free/nrw/commons/achievements/AchievementsActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/achievements/AchievementsActivity.java @@ -343,8 +343,8 @@ public class AchievementsActivity extends NavigationBaseActivity { TextView shareMessage = (TextView) view.findViewById(R.id.alert_text); shareMessage.setText(R.string.achievements_share_message); alertadd.setView(view); - alertadd.setPositiveButton("Proceed", (dialog, which) -> shareScreen(screenshot)); - alertadd.setNegativeButton("Cancel", (dialog, which) -> dialog.cancel()); + alertadd.setPositiveButton(R.string.about_translate_proceed, (dialog, which) -> shareScreen(screenshot)); + alertadd.setNegativeButton(android.R.string.cancel, (dialog, which) -> dialog.cancel()); alertadd.show(); } diff --git a/app/src/main/java/fr/free/nrw/commons/achievements/LevelController.java b/app/src/main/java/fr/free/nrw/commons/achievements/LevelController.java index e0f84bbee..d1d203aca 100644 --- a/app/src/main/java/fr/free/nrw/commons/achievements/LevelController.java +++ b/app/src/main/java/fr/free/nrw/commons/achievements/LevelController.java @@ -25,7 +25,19 @@ public class LevelController { LEVEL_12(12,R.style.LevelTwo,65 , 130, 90), LEVEL_13(13,R.style.LevelThree, 70, 140, 90), LEVEL_14(14,R.style.LevelFour, 75 , 150, 90), - LEVEL_15(15,R.style.LevelFive, 80, 160, 90); + LEVEL_15(15,R.style.LevelFive, 80, 160, 90), + LEVEL_16(16,R.style.LevelOne, 160, 320, 91), + LEVEL_17(17,R.style.LevelTwo, 320, 640, 92), + LEVEL_18(18,R.style.LevelThree, 640, 1280, 93), + LEVEL_19(19,R.style.LevelFour, 1280, 2560, 94), + LEVEL_20(20,R.style.LevelFive, 2560, 5120, 95), + LEVEL_21(21,R.style.LevelOne, 5120, 10240, 96), + LEVEL_22(22,R.style.LevelTwo, 10240, 20480, 97), + LEVEL_23(23,R.style.LevelThree, 20480, 40960, 98), + LEVEL_24(24,R.style.LevelFour, 40960, 81920, 98), + LEVEL_25(25,R.style.LevelFive, 81920, 163840, 98), + LEVEL_26(26,R.style.LevelOne, 163840, 327680, 98), + LEVEL_27(27,R.style.LevelTwo, 327680, 655360, 98); private int levelNumber; private int levelStyle; 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 9cdd93352..1f2ebf047 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.annotation.StringRes; 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.view.ViewGroup; 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.PageTitle; 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; @@ -60,6 +63,7 @@ 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; @@ -76,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; @@ -125,6 +130,15 @@ public class LoginActivity extends AccountAuthenticatorActivity { 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)); @@ -133,6 +147,17 @@ public class LoginActivity extends AccountAuthenticatorActivity { } } + /** + * 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)); } @@ -159,9 +184,15 @@ public class LoginActivity extends AccountAuthenticatorActivity { 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 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 93ddb60d5..c53c29379 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 @@ -377,10 +377,10 @@ public class CategorizationFragment extends CommonsDaggerSupportFragment { .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,10 +391,10 @@ public class CategorizationFragment extends CommonsDaggerSupportFragment { + "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)); }) 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 efaf5f221..9f5084e6a 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 @@ -13,7 +13,7 @@ import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.Media; import fr.free.nrw.commons.settings.Prefs; -public class Contribution extends Media { +public class Contribution extends Media { public static Creator CREATOR = new Creator() { @Override 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 6abb3ce43..e366a1468 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 @@ package fr.free.nrw.commons.contributions; 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.BuildConfig; 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 class ContributionsListFragment extends CommonsDaggerSupportFragment { 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 class ContributionsListFragment extends CommonsDaggerSupportFragment { // 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 37b9a7a82..f35a6c38e 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 java.util.Locale; 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 class DeleteTask extends AsyncTask { } @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/CommonsApplicationModule.java b/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.java index a6bc4f59d..d59cf5f64 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 @@ -24,7 +24,6 @@ import fr.free.nrw.commons.wikidata.WikidataEditListenerImpl; import okhttp3.OkHttpClient; import static android.content.Context.MODE_PRIVATE; -import static fr.free.nrw.commons.explore.recentsearches.RecentSearchesContentProvider.RECENT_SEARCH_AUTHORITY; @Module @SuppressWarnings({"WeakerAccess", "unused"}) @@ -60,7 +59,7 @@ public class CommonsApplicationModule { @Provides @Named("recentsearch") public ContentProviderClient provideRecentSearchContentProviderClient(Context context) { - return context.getContentResolver().acquireContentProviderClient(RECENT_SEARCH_AUTHORITY); + return context.getContentResolver().acquireContentProviderClient(BuildConfig.RECENT_SEARCH_AUTHORITY); } @Provides 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 bf3cf959a..ed7821885 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 android.text.TextUtils; 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 @@ import static fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao.Table **/ 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 5c109fbb4..7c0a6fcca 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 class RecentSearchesFragment extends CommonsDaggerSupportFragment { 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 class RecentSearchesFragment extends CommonsDaggerSupportFragment { 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 9087f9501..000000000 --- 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 89910c8fb..000000000 --- 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 66a3bd6bf..000000000 --- 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 cd1082ba5..4a137beed 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 class LocationServiceManager implements LocationListener { 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 e7a21ed09..b46789ba0 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.AsyncTask; 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.mwapi.MediaWikiApi; 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; @@ -60,6 +64,7 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment { private boolean isCategoryImage; private MediaDetailPagerFragment.MediaDetailProvider detailProvider; private int index; + private Locale locale; public static MediaDetailFragment forMedia(int index, boolean editable, boolean isCategoryImage) { MediaDetailFragment mf = new MediaDetailFragment(); @@ -161,6 +166,7 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment { 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); @@ -198,6 +204,7 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment { } }; view.getViewTreeObserver().addOnGlobalLayoutListener(layoutListener); + locale = getResources().getConfiguration().locale; return view; } @@ -349,6 +356,17 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment { } } + @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 @@ -455,7 +473,7 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment { private String prettyDescription(Media media) { // @todo use UI language when multilingual descs are available - String desc = media.getDescription("en").trim(); + String desc = media.getDescription(locale.getLanguage()).trim(); if (desc.equals("")) { return getString(R.string.detail_description_empty); } else { 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 d5e4fac2f..b51e85903 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 @@ -94,6 +94,12 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple 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); @@ -123,6 +129,10 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple @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()) { @@ -189,10 +199,13 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple * @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; } @@ -234,6 +247,10 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple 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 @@ -285,6 +302,10 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple @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(); @@ -336,6 +357,10 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple public Fragment getItem(int i) { if (i == 0) { // See bug https://code.google.com/p/android/issues/detail?id=27526 + if(getActivity() == null) { + Timber.d("Skipping getItem. Returning as activity is destroyed!"); + return null; + } pager.postDelayed(() -> getActivity().supportInvalidateOptionsMenu(), 5); } return MediaDetailFragment.forMedia(i, editable, isFeaturedImage); @@ -343,6 +368,10 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple @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/nearby/DirectUpload.java b/app/src/main/java/fr/free/nrw/commons/nearby/DirectUpload.java index 9a12c6d39..ca25fccf3 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 @@ class DirectUpload { 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 @@ class DirectUpload { 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 cb28df947..2143496f0 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.annotation.NonNull; 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 timber.log.Timber; import uk.co.deanwild.materialshowcaseview.IShowcaseListener; import uk.co.deanwild.materialshowcaseview.MaterialShowcaseView; -import static fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType.*; +import static fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType.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 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp // 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 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp 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 bd042b4d7..4f777aeb4 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 class NearbyController { } 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/NearbyMapFragment.java b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyMapFragment.java index ba23b8e1a..ff6d8f523 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.annotation.Nullable; 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.Inject; 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 @@ import static fr.free.nrw.commons.wikidata.WikidataConstants.WIKIDATA_ENTITY_ID_ 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 @@ public class NearbyMapFragment extends DaggerFragment { } 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 class NearbyMapFragment extends DaggerFragment { 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 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 c8d20f753..f9d35d63e 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 android.net.Uri; 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 @@ public class NearbyPlaces { // 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 7170e4c02..ec4dbf8ba 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.net.Uri; 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,14 +28,18 @@ import butterknife.BindView; 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; @@ -90,9 +95,9 @@ public class PlaceRenderer extends Renderer { Log.d("Renderer", "clicked"); TransitionManager.beginDelayedTransition(buttonLayout); - if(buttonLayout.isShown()){ + if (buttonLayout.isShown()) { closeLayout(buttonLayout); - }else { + } else { openLayout(buttonLayout); } @@ -108,18 +113,46 @@ public class PlaceRenderer extends Renderer { }); 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() { @@ -210,4 +243,4 @@ public class PlaceRenderer extends Renderer { 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 6dcfca35d..1164d9d57 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.ImageView; import android.widget.TextView; import com.borjabravo.readmoretextview.ReadMoreTextView; -import com.bumptech.glide.RequestBuilder; import com.pedrogomez.renderers.Renderer; import butterknife.BindView; import butterknife.ButterKnife; import fr.free.nrw.commons.R; -import fr.free.nrw.commons.glide.SvgSoftwareLayerSetter; - -import static com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions.withCrossFade; /** * Created by root on 19.12.2017. */ public class NotificationRenderer extends Renderer { - 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 @@ public class NotificationRenderer extends Renderer { 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 class NotificationRenderer extends Renderer { 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 5a1e8ae63..000000000 --- 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 f34ef81ba..654c08fa9 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 @@ -136,7 +136,7 @@ public class QuizChecker { 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", (dialog, which) -> { + alert.setPositiveButton(R.string.about_translate_proceed, (dialog, which) -> { int newRevetSharedPrefs = revertCount + revertPref.getInt(REVERT_SHARED_PREFERENCE, 0); revertPref.edit().putInt(REVERT_SHARED_PREFERENCE, newRevetSharedPrefs).apply(); int newUploadCount = totalUploadCount + countPref.getInt(UPLOAD_SHARED_PREFERENCE, 0); @@ -146,7 +146,7 @@ public class QuizChecker { dialog.dismiss(); context.startActivity(i); }); - alert.setNegativeButton("Cancel", (dialogInterface, i) -> dialogInterface.cancel()); + alert.setNegativeButton(android.R.string.cancel, (dialogInterface, i) -> dialogInterface.cancel()); android.support.v7.app.AlertDialog dialog = alert.create(); dialog.show(); } diff --git a/app/src/main/java/fr/free/nrw/commons/quiz/QuizController.java b/app/src/main/java/fr/free/nrw/commons/quiz/QuizController.java index fc6b4c15f..5035d32ec 100644 --- a/app/src/main/java/fr/free/nrw/commons/quiz/QuizController.java +++ b/app/src/main/java/fr/free/nrw/commons/quiz/QuizController.java @@ -43,7 +43,7 @@ public class QuizController { quiz.add(q3); QuizQuestion q4 = new QuizQuestion(4, - context.getResources().getString(R.string.quiz_question_string), + context.getResources().getString(R.string.quiz_screenshot_question), URL_FOR_SCREENSHOT, false, context.getResources().getString(R.string.screenshot_answer)); 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 e7c40f615..93a113338 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 class QuizResultActivity extends AppCompatActivity { 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/NavigationBaseActivity.java b/app/src/main/java/fr/free/nrw/commons/theme/NavigationBaseActivity.java index 2933d4c19..49fee01c1 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.accounts.AccountManager; 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.v4.widget.DrawerLayout; 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.ImageView; 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 abstract class NavigationBaseActivity extends BaseActivity 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); + } } /** @@ -120,6 +145,14 @@ public abstract class NavigationBaseActivity extends BaseActivity 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 new file mode 100644 index 000000000..8e41c422b --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/upload/Description.java @@ -0,0 +1,57 @@ +package fr.free.nrw.commons.upload; + +import android.text.TextUtils; +import android.util.TimeUtils; + +class Description { + + private String languageId; + private String languageDisplayText; + 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; + } + + public void setLanguageDisplayText(String languageDisplayText) { + this.languageDisplayText = languageDisplayText; + } + + public String getDescriptionText() { + return descriptionText; + } + + 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; + } + + public int getSelectedLanguageIndex() { + return selectedLanguageIndex; + } + + public void setSelectedLanguageIndex(int selectedLanguageIndex) { + this.selectedLanguageIndex = selectedLanguageIndex; + } +} 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 new file mode 100644 index 000000000..8642e1210 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/upload/DescriptionsAdapter.java @@ -0,0 +1,231 @@ +package fr.free.nrw.commons.upload; + +import static android.view.MotionEvent.ACTION_UP; + +import android.content.Context; +import android.graphics.drawable.Drawable; +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; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemSelectedListener; +import android.widget.EditText; +import butterknife.BindView; +import butterknife.ButterKnife; +import butterknife.OnTouch; +import fr.free.nrw.commons.R; +import fr.free.nrw.commons.utils.ViewUtil; +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 { + + List descriptions; + List languages; + private Context context; + private Callback callback; + + public DescriptionsAdapter() { + descriptions = new ArrayList<>(); + descriptions.add(new Description()); + languages = new ArrayList<>(); + } + + public void setCallback(Callback callback) { + this.callback = callback; + } + + public void setDescriptions(List descriptions) { + this.descriptions = descriptions; + notifyDataSetChanged(); + } + + public void setLanguages(List languages) { + this.languages = languages; + } + + @Override + public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + View view = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.row_item_description, parent, false); + context = parent.getContext(); + ViewHolder viewHolder = new ViewHolder(view); + return viewHolder; + } + + @Override + public void onBindViewHolder(ViewHolder holder, int position) { + holder.init(position); + } + + @Override + public int getItemCount() { + return descriptions.size(); + } + + public List getDescriptions() { + return descriptions; + } + + public void addDescription(Description description) { + this.descriptions.add(description); + notifyItemInserted(descriptions.size() - 1); + } + + + public class ViewHolder extends RecyclerView.ViewHolder { + + @BindView(R.id.spinner_description_languages) + AppCompatSpinner spinnerDescriptionLanguages; + @BindView(R.id.et_description_text) + EditText etDescriptionText; + private View view; + + + public ViewHolder(View itemView) { + super(itemView); + ButterKnife.bind(this, itemView); + this.view = itemView; + } + + 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()); + } + }); + + etDescriptionText.setOnFocusChangeListener((v, hasFocus) -> { + if (!hasFocus) { + ViewUtil.hideKeyboard(v); + } + }); + + 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); + } else { + spinnerDescriptionLanguages.setSelection(0); + } + } 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); + } + + @Override + public void onNothingSelected(AdapterView adapterView) { + + } + }); + + + } + + @OnTouch(R.id.et_description_text) + boolean descriptionInfo(View view, MotionEvent motionEvent) { + + 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 + .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 + .getCompoundDrawables()[0] + .getBounds().width(); + if (motionEvent.getAction() == ACTION_UP && motionEvent.getRawX() <= value) { + callback.showAlert(R.string.media_detail_description, + R.string.description_info); + return true; + } + } + } + return false; + } + } + + 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/Language.java b/app/src/main/java/fr/free/nrw/commons/upload/Language.java new file mode 100644 index 000000000..8d4b27239 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/upload/Language.java @@ -0,0 +1,29 @@ +package fr.free.nrw.commons.upload; + +import java.util.Locale; + +class Language { + + private Locale locale; + private boolean isSet = false; + + public Language(Locale locale) { + this.locale = locale; + } + + public Locale getLocale() { + return locale; + } + + public void setLocale(Locale locale) { + this.locale = locale; + } + + public boolean isSet() { + return isSet; + } + + public void setSet(boolean set) { + isSet = set; + } +} 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 index e429c3ee8..a35eb46ee 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/MultipleShareActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/MultipleShareActivity.java @@ -1,6 +1,8 @@ package fr.free.nrw.commons.upload; import android.Manifest; +import android.Manifest.permission; +import android.app.AlertDialog; import android.app.ProgressDialog; import android.content.ContentResolver; import android.content.Context; @@ -12,7 +14,6 @@ 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; @@ -23,6 +24,15 @@ import android.view.inputmethod.InputMethodManager; import android.widget.AdapterView; import android.widget.Toast; +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.CommonsApplication; +import fr.free.nrw.commons.utils.DialogUtil; +import fr.free.nrw.commons.utils.DialogUtil.Callback; +import fr.free.nrw.commons.utils.PermissionUtils; import java.io.FileNotFoundException; import java.util.ArrayList; import java.util.List; @@ -41,7 +51,6 @@ 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; @@ -81,6 +90,11 @@ public class MultipleShareActivity extends AuthenticatedActivity private boolean locationPermitted = false; private boolean isMultipleUploadsPrepared = false; private boolean isMultipleUploadsFinalised = false; // Checks is user clicked to upload button or regret before this phase + private final String TAG="#MultipleShareActivity#"; + private AlertDialog storagePermissionInfoDialog; + private DexterBuilder dexterStoragePermissionBuilder; + + private PermissionDeniedResponse permissionDeniedResponse; @Override public Media getMediaAtPosition(int i) { @@ -124,17 +138,6 @@ public class MultipleShareActivity extends AuthenticatedActivity 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"); @@ -216,6 +219,7 @@ public class MultipleShareActivity extends AuthenticatedActivity setContentView(R.layout.activity_multiple_uploads); ButterKnife.bind(this); initDrawer(); + initPermissionsRationaleDialog(); if (savedInstanceState != null) { photosList = savedInstanceState.getParcelableArrayList("uploadsList"); @@ -233,6 +237,47 @@ public class MultipleShareActivity extends AuthenticatedActivity } } + + /** + * 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( + MultipleShareActivity.this, + getString(R.string.storage_permission), getString( + R.string.write_storage_permission_rationale_for_image_share), + R.drawable.ic_launcher, new 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 + return; + } + else if (permissionDeniedResponse.isPermanentlyDenied()) { + PermissionUtils.askUserToManuallyEnablePermissionFromSettings(MultipleShareActivity.this); + } else { + //or if we still have chance to show runtime permission dialog, we show him that. + askDexterToHandleExternalStoragePermission(); + } + } + + @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 anymore + finish(); + } + }); + } + } + @Override protected void onDestroy() { super.onDestroy(); @@ -275,12 +320,55 @@ public class MultipleShareActivity extends AuthenticatedActivity isMultipleUploadsPrepared = false; mwApi.setAuthCookie(authCookie); if (!ExternalStorageUtils.isStoragePermissionGranted(this)) { - ExternalStorageUtils.requestExternalStoragePermission(this); + //If permission is not there, handle the negative cases + askDexterToHandleExternalStoragePermission(); isMultipleUploadsPrepared = false; return; // Postpone operation to do after gettion permission } else { isMultipleUploadsPrepared = true; - prepareMultipleUpoadList(); + prepareMultipleUploadList(); + } + } + + /** + * 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(this) + .withPermission(permission.WRITE_EXTERNAL_STORAGE) + .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 + prepareMultipleUploadList(); + } + + @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(); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if (requestCode == CommonsApplication.OPEN_APPLICATION_DETAIL_SETTINGS) { + //OnActivity result, no matter what the result is, our function can handle that. + askDexterToHandleExternalStoragePermission(); } } @@ -288,7 +376,7 @@ public class MultipleShareActivity extends AuthenticatedActivity * Prepares a list from files will be uploaded. Saves these files temporarily to external * storage. Adds them to uploads list */ - private void prepareMultipleUpoadList() { + private void prepareMultipleUploadList() { Intent intent = getIntent(); if (Intent.ACTION_SEND_MULTIPLE.equals(intent.getAction())) { 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 index 028456bb6..f38862da2 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/MultipleUploadListFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/MultipleUploadListFragment.java @@ -23,6 +23,7 @@ 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; @@ -205,6 +206,10 @@ public class MultipleUploadListFragment extends Fragment { 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; } 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 index e06aeae6d..3a8e1b413 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/ShareActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/ShareActivity.java @@ -18,6 +18,7 @@ import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.Environment; +import android.os.Handler; import android.support.annotation.NonNull; import android.support.annotation.RequiresApi; import android.support.design.widget.FloatingActionButton; @@ -103,6 +104,7 @@ public class ShareActivity ModifierSequenceDao modifierSequenceDao; @Inject CategoryApi apiCall; + @Inject @Named("application_preferences") SharedPreferences applicationPrefs; @Inject @Named("default_preferences") SharedPreferences prefs; @@ -341,6 +343,12 @@ public class ShareActivity checkIfFileExists(); gpsObj = fileObj.processFileCoordinates(locationPermitted); decimalCoords = fileObj.getDecimalCoords(); + if (sessionManager.getCurrentAccount() == null) { + Toast.makeText(this, getString(R.string.login_alert_message), Toast.LENGTH_SHORT).show(); + applicationPrefs.edit().putBoolean("login_skipped", false).apply(); + Intent loginIntent = new Intent(ShareActivity.this, LoginActivity.class); + startActivity(loginIntent); + } } /** @@ -523,7 +531,7 @@ public class ShareActivity CurrentAnimator.cancel(); } isZoom = true; - ViewUtil.hideKeyboard(ShareActivity.this.findViewById(R.id.titleEdit | R.id.descEdit)); + ViewUtil.hideKeyboard(ShareActivity.this.findViewById(R.id.titleEdit)); closeFABMenu(); mainFab.setVisibility(View.GONE); 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 index f2fef1ddf..bacefd524 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/SingleUploadFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/SingleUploadFragment.java @@ -10,6 +10,8 @@ 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; @@ -30,9 +32,16 @@ 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; @@ -53,7 +62,7 @@ import static android.view.MotionEvent.ACTION_UP; public class SingleUploadFragment extends CommonsDaggerSupportFragment { @BindView(R.id.titleEdit) EditText titleEdit; - @BindView(R.id.descEdit) EditText descEdit; + @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; @@ -65,6 +74,7 @@ public class SingleUploadFragment extends CommonsDaggerSupportFragment { private String license; private OnUploadActionInitiated uploadActionInitiatedHandler; private TitleTextWatcher textWatcher = new TitleTextWatcher(); + private DescriptionsAdapter descriptionsAdapter; @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { @@ -82,35 +92,60 @@ public class SingleUploadFragment extends CommonsDaggerSupportFragment { return false; } - String title = titleEdit.getText().toString().trim(); - String desc = descEdit.getText().toString().trim(); + 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", desc) + .putString("Desc", new Gson().toJson(descriptionsAdapter + .getDescriptions()))//Description, now is not just a string, its a list of description objects .apply(); - uploadActionInitiatedHandler.uploadActionInitiated(title, desc); + 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")) { - descEdit.setText(activityIntent.getStringExtra("description")); + 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)); @@ -129,7 +164,11 @@ public class SingleUploadFragment extends CommonsDaggerSupportFragment { String imageCats = directPrefs.getString("Category", ""); Timber.d("Image title: " + imageTitle + ", image desc: " + imageDesc + ", image categories: " + imageCats); titleEdit.setText(imageTitle); - descEdit.setText(imageDesc); + 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 @@ -170,17 +209,29 @@ public class SingleUploadFragment extends CommonsDaggerSupportFragment { } }); - descEdit.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); @@ -224,11 +275,16 @@ public class SingleUploadFragment extends CommonsDaggerSupportFragment { void setTitleDescButton() { //Retrieve last title and desc entered String title = prefs.getString("Title", ""); - String desc = prefs.getString("Desc", ""); - Timber.d("Title: %s, Desc: %s", title, desc); + String descriptionJson = prefs.getString("Desc", ""); + Timber.d("Title: %s, Desc: %s", title, descriptionJson); titleEdit.setText(title); - descEdit.setText(desc); + Type typeOfDest = new TypeToken>() { + }.getType(); + + List descriptions = new Gson().fromJson(descriptionJson, typeOfDest); + descriptionsAdapter.setDescriptions(descriptions); + } /** @@ -254,26 +310,6 @@ public class SingleUploadFragment extends CommonsDaggerSupportFragment { return false; } - @OnTouch(R.id.descEdit) - boolean descriptionInfo(View view, MotionEvent motionEvent) { - final int value; - if (ViewCompat.getLayoutDirection(getView()) == ViewCompat.LAYOUT_DIRECTION_LTR) { - value = descEdit.getRight() - descEdit.getCompoundDrawables()[2].getBounds().width(); - if (motionEvent.getAction() == ACTION_UP && motionEvent.getRawX() >= value) { - showInfoAlert(R.string.media_detail_description,R.string.description_info); - return true; - } - } - else{ - value = descEdit.getLeft() + descEdit.getCompoundDrawables()[0].getBounds().width(); - if (motionEvent.getAction() == ACTION_UP && motionEvent.getRawX() <= value) { - showInfoAlert(R.string.media_detail_description,R.string.description_info); - return true; - } - } - return false; - } - @SuppressLint("StringFormatInvalid") private void setLicenseSummary(String license) { String licenseHyperLink = ""+ getString(Utils.licenseNameFor(license)) + "
"; @@ -315,10 +351,12 @@ public class SingleUploadFragment extends CommonsDaggerSupportFragment { } 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) { } @@ -346,16 +384,9 @@ public class SingleUploadFragment extends CommonsDaggerSupportFragment { .show(); } - /** - * To launch the Commons:Licensing - * @param view - */ - @OnClick(R.id.licenseInfo) - public void launchLicenseInfo(View view){ - Log.i("Language", Locale.getDefault().getLanguage()); - UrlLicense urlLicense = new UrlLicense(); - urlLicense.initialize(); - String url = urlLicense.getLicenseUrl(Locale.getDefault().getLanguage()); - Utils.handleWebUrl(getActivity() , Uri.parse(url)); + @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 new file mode 100644 index 000000000..5667aa55c --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/upload/SpinnerLanguagesAdapter.java @@ -0,0 +1,92 @@ +package fr.free.nrw.commons.upload; + +import android.content.Context; +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.ArrayAdapter; +import android.widget.LinearLayout; +import android.widget.TextView; +import butterknife.BindView; +import butterknife.ButterKnife; +import fr.free.nrw.commons.R; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +public class SpinnerLanguagesAdapter extends ArrayAdapter { + + private final int resource; + private final LayoutInflater layoutInflater; + List languages; + + public SpinnerLanguagesAdapter(@NonNull Context context, + int resource) { + super(context, resource); + this.resource = resource; + this.layoutInflater = LayoutInflater.from(context); + languages = new ArrayList<>(); + } + + public void setLanguages(List languages) { + this.languages = languages; + } + + @Override + public int getCount() { + return languages.size(); + } + + @Override + public View getDropDownView(int position, @Nullable View convertView, + @NonNull ViewGroup parent) { + View view = layoutInflater.inflate(resource, parent, false); + ViewHolder holder = new ViewHolder(view); + holder.init(position, true); + return view; + } + + @Override + public @NonNull + View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) { + View view = layoutInflater.inflate(resource, parent, false); + ViewHolder holder = new ViewHolder(view); + holder.init(position, false); + return view; + } + + + public class ViewHolder { + + @BindView(R.id.ll_container_description_language) + LinearLayout llContainerDescriptionLanguage; + + @BindView(R.id.tv_language) + TextView tvLanguage; + + @BindView(R.id.view) + View view; + + public ViewHolder(View itemView) { + ButterKnife.bind(this, itemView); + } + + public void init(int position, boolean isDropDownView) { + Language language = languages.get(position); + if (!isDropDownView) { + view.setVisibility(View.GONE); + tvLanguage.setText( + language.getLocale().getLanguage()); + } else { + view.setVisibility(View.VISIBLE); + tvLanguage.setText( + String.format("%s [%s]", language.getLocale().getDisplayName(), + language.getLocale().getLanguage())); + } + + } + } + +} 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 306c7272f..c2fdfbe8d 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.content.Intent; 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 java.util.regex.Pattern; 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.Contribution; 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; @@ -182,6 +181,19 @@ public class UploadService extends HandlerService { } @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; @@ -207,17 +219,9 @@ public class UploadService extends HandlerService { } 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; 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 78c1ca155..2e4592e40 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 class DialogUtil { 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 79dad33e5..dce29402c 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 @@ -8,6 +8,7 @@ import android.graphics.Color; import android.graphics.Rect; import android.net.Uri; import android.support.annotation.Nullable; +import android.util.Log; import com.facebook.common.executors.CallerThreadExecutor; import com.facebook.common.references.CloseableReference; @@ -29,11 +30,6 @@ import timber.log.Timber; */ public class ImageUtils { - //atleast 50% of the image in question should be considered dark for the entire image to be dark - private static final double MINIMUM_DARKNESS_FACTOR = 0.50; - //atleast 50% of the image in question should be considered blurry for the entire image to be blurry - private static final double MINIMUM_BLURRYNESS_FACTOR = 0.50; - private static final int LAPLACIAN_VARIANCE_THRESHOLD = 70; public enum Result { IMAGE_DARK, @@ -41,13 +37,6 @@ public class ImageUtils { } /** - * BitmapRegionDecoder allows us to process a large bitmap by breaking it down into smaller rectangles. The rectangles - * are obtained by setting an initial width, height and start position of the rectangle as a factor of the width and - * height of the original bitmap and then manipulating the width, height and position to loop over the entire original - * bitmap. Each individual rectangle is independently processed to check if its too dark. Based on - * the factor of "bright enough" individual rectangles amongst the total rectangles into which the image - * 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 @@ -62,39 +51,15 @@ public class ImageUtils { int loadImageWidth = bitmapRegionDecoder.getWidth(); int checkImageTopPosition = 0; - int checkImageBottomPosition = loadImageHeight / 10; int checkImageLeftPosition = 0; - int checkImageRightPosition = loadImageWidth / 10; - int totalDividedRectangles = 0; - int numberOfDarkRectangles = 0; + Timber.v("left: " + checkImageLeftPosition + " right: " + loadImageWidth + " top: " + checkImageTopPosition + " bottom: " + loadImageHeight); - while ((checkImageRightPosition <= loadImageWidth) && (checkImageLeftPosition < checkImageRightPosition)) { - while ((checkImageBottomPosition <= loadImageHeight) && (checkImageTopPosition < checkImageBottomPosition)) { - Timber.v("left: " + checkImageLeftPosition + " right: " + checkImageRightPosition + " top: " + checkImageTopPosition + " bottom: " + checkImageBottomPosition); + Rect rect = new Rect(checkImageLeftPosition,checkImageTopPosition, loadImageWidth, loadImageHeight); - 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++; - } - - checkImageTopPosition = checkImageBottomPosition; - checkImageBottomPosition += (checkImageBottomPosition < (loadImageHeight - checkImageBottomPosition)) ? checkImageBottomPosition : (loadImageHeight - checkImageBottomPosition); - } - - checkImageTopPosition = 0; //reset to start - checkImageBottomPosition = loadImageHeight / 10; //reset to start - checkImageLeftPosition = checkImageRightPosition; - checkImageRightPosition += (checkImageRightPosition < (loadImageWidth - checkImageRightPosition)) ? checkImageRightPosition : (loadImageWidth - checkImageRightPosition); - } - - Timber.d("dark rectangles count = " + numberOfDarkRectangles + ", total rectangles count = " + totalDividedRectangles); - - if (numberOfDarkRectangles > totalDividedRectangles * MINIMUM_DARKNESS_FACTOR) { + if (checkIfImageIsDark(processBitmap)) { return Result.IMAGE_DARK; } @@ -104,14 +69,12 @@ public class ImageUtils { /** * Pulls the pixels into an array and then runs through it while checking the brightness of each pixel. * The calculation of brightness of each pixel is done by extracting the RGB constituents of the pixel - * and then applying the formula to calculate its "Luminance". 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. - * + * and then applying the formula to calculate its "Luminance". + * Pixels with luminance greater than 40% are considered to be bright pixels while the ones with luminance + * greater than 26% but less than 40% are considered to be pixels with medium brightness. The rest are + * dark pixels. + * If the number of bright pixels is more than 2.5% or the number of pixels with medium brightness is + * more than 30% of the total number of pixels then the image is considered to be OK else dark. * @param bitmap The bitmap that needs to be checked. * @return true if bitmap is dark or null, false if bitmap is bright */ @@ -126,28 +89,45 @@ public class ImageUtils { int allPixelsCount = bitmapWidth * bitmapHeight; int[] bitmapPixels = new int[allPixelsCount]; + Log.e("total", Integer.toString(allPixelsCount)); bitmap.getPixels(bitmapPixels,0,bitmapWidth,0,0,bitmapWidth,bitmapHeight); - boolean isImageDark = false; - int darkPixelsCount = 0; + int numberOfBrightPixels = 0; + int numberOfMediumBrightnessPixels = 0; + double brightPixelThreshold = 0.025*allPixelsCount; + double mediumBrightPixelThreshold = 0.3*allPixelsCount; for (int pixel : bitmapPixels) { int r = Color.red(pixel); int g = Color.green(pixel); int b = Color.blue(pixel); - int brightness = (int) (0.2126 * r + 0.7152 * g + 0.0722 * b); - if (brightness < 50) { - //pixel is dark - darkPixelsCount++; - if (darkPixelsCount > allPixelsCount * MINIMUM_DARKNESS_FACTOR) { - isImageDark = true; - break; + int secondMax = r>g ? r:g; + double max = (secondMax>b ? secondMax:b)/255.0; + + int secondMin = rmediumBrightnessLuminance){ + numberOfMediumBrightnessPixels++; } } - } + else { + numberOfBrightPixels++; + } - return isImageDark; + if (numberOfBrightPixels>=brightPixelThreshold || numberOfMediumBrightnessPixels>=mediumBrightPixelThreshold){ + return false; + } + + } + return true; } /** 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 000000000..ecdc01511 --- /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 82bad3f09..c7c0cfb81 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 @@ package fr.free.nrw.commons.widget; 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 @@ import fr.free.nrw.commons.R; */ 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 class PicOfDayAppWidget extends AppWidgetProvider { 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/layout-land/activity_login.xml b/app/src/main/res/layout-land/activity_login.xml index deb295438..ed1c8a578 100644 --- a/app/src/main/res/layout-land/activity_login.xml +++ b/app/src/main/res/layout-land/activity_login.xml @@ -4,6 +4,10 @@ android:layout_width="match_parent" android:layout_height="match_parent"> + - + @@ -230,5 +233,15 @@ tools:ignore="UnusedAttribute" /> + + diff --git a/app/src/main/res/layout-xlarge/activity_login.xml b/app/src/main/res/layout-xlarge/activity_login.xml index 819260920..1fe7acf64 100644 --- a/app/src/main/res/layout-xlarge/activity_login.xml +++ b/app/src/main/res/layout-xlarge/activity_login.xml @@ -4,6 +4,11 @@ android:layout_width="match_parent" android:layout_height="match_parent"> + + - + @@ -230,5 +234,14 @@ tools:ignore="UnusedAttribute" /> + + diff --git a/app/src/main/res/layout/activity_login.xml b/app/src/main/res/layout/activity_login.xml index 5d1345bd3..0470cf720 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/drawer_header.xml b/app/src/main/res/layout/drawer_header.xml index a984eff90..51b0c711c 100644 --- a/app/src/main/res/layout/drawer_header.xml +++ b/app/src/main/res/layout/drawer_header.xml @@ -25,7 +25,7 @@ android:textColor="@color/item_white_background" android:textSize="@dimen/subheading_text_size" android:layout_below="@+id/pictureOfTheDay" - android:layout_marginLeft="@dimen/activity_margin_horizontal" + android:layout_centerHorizontal="true" android:paddingBottom="@dimen/small_gap"/> +