diff --git a/app/build.gradle b/app/build.gradle index 5ab6b479e..4a395787c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -70,6 +70,9 @@ dependencies { releaseImplementation "com.squareup.leakcanary:leakcanary-android-no-op:$LEAK_CANARY" testImplementation "com.squareup.leakcanary:leakcanary-android-no-op:$LEAK_CANARY" + //For handling runtime permissions + implementation 'com.karumi:dexter:5.0.0' + } android { 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 51b8b0b0f..2ee5f305f 100644 --- a/app/src/main/java/fr/free/nrw/commons/CommonsApplication.java +++ b/app/src/main/java/fr/free/nrw/commons/CommonsApplication.java @@ -54,6 +54,11 @@ public class CommonsApplication extends Application { @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"; @@ -66,6 +71,10 @@ public class CommonsApplication extends Application { public static final String NOTIFICATION_CHANNEL_ID_ALL = "CommonsNotificationAll"; + /** + * Constants End + */ + private RefWatcher refWatcher; 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/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/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/res/values/strings.xml b/app/src/main/res/values/strings.xml index 14dff9500..7f5f8f4a2 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -352,4 +352,6 @@ The percentage of images you have uploaded to Commons that were not deleted The number of images you have uploaded to Commons that were used in Wikimedia articles Commons Notification + Storage Permission + We need your permission to access the external storage of your device in order to upload images.