mirror of
https://github.com/commons-app/apps-android-commons.git
synced 2025-10-26 12:23:58 +01:00
Feature/permissions library (#1855)
* Added permission for Dexter, the runtime permission handling library * [Preparing fir issue #1773] Added a utility function which would take the user to app settings screen where he could manually give us the required permission * Added an alert dialog with positive and negative callback [Preparing fir issue #1773] * Improvements in the way External Storage Permission is handled in MultipleShareActivity[Bug fix #1697] 1. Used dexter to handle the external storage permission 2. Behaviour changes : When user tries to share(uppload) images to commons via MultipleShareActivity, following decision tree is followed a. If the app has permission for external storage, normal upload operation is followed b. If the app does not has the permission for external storage, dexter is used to ask for the same c. If the user gives us the required permission, normal upload flow is proceeded d. If the doesnot gives us the required permission a rationale dialog is shown with the appropriate message to let him know why we need the permission e. If he presses okay, steps a-c are followed and if he presses cancel, we close the app. f. If while asking for permission, the user chooses never ask again, then next time he tries to upload an image via MSA, the rational dialog follows the app setting screen where he could manually give us the required permission and the onActivityResult of same is handled * Added a Constants class to handle request and result codes from one place and other related constants common to the all app elements * replaced hardcoded strings ok and cancel in DialogUtil to string resources * init permission rationale dialog in activities onCreate * Code formatting, updated access modifiers wherever required, added javadocs for new methods created * *shifted constants to app class *Added JavaDocs in PermissionUtils * removed class REQUEST_CODES from CommonsApplication and instead put the enclosing constants in the App class itself
This commit is contained in:
parent
dd6144bbdb
commit
70099a9014
6 changed files with 172 additions and 16 deletions
|
|
@ -70,6 +70,9 @@ dependencies {
|
||||||
releaseImplementation "com.squareup.leakcanary:leakcanary-android-no-op:$LEAK_CANARY"
|
releaseImplementation "com.squareup.leakcanary:leakcanary-android-no-op:$LEAK_CANARY"
|
||||||
testImplementation "com.squareup.leakcanary:leakcanary-android-no-op:$LEAK_CANARY"
|
testImplementation "com.squareup.leakcanary:leakcanary-android-no-op:$LEAK_CANARY"
|
||||||
|
|
||||||
|
//For handling runtime permissions
|
||||||
|
implementation 'com.karumi:dexter:5.0.0'
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,11 @@ public class CommonsApplication extends Application {
|
||||||
@Inject @Named("application_preferences") SharedPreferences applicationPrefs;
|
@Inject @Named("application_preferences") SharedPreferences applicationPrefs;
|
||||||
@Inject @Named("prefs") SharedPreferences otherPrefs;
|
@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 DEFAULT_EDIT_SUMMARY = "Uploaded using [[COM:MOA|Commons Mobile App]]";
|
||||||
|
|
||||||
public static final String FEEDBACK_EMAIL = "commons-app-android@googlegroups.com";
|
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";
|
public static final String NOTIFICATION_CHANNEL_ID_ALL = "CommonsNotificationAll";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constants End
|
||||||
|
*/
|
||||||
|
|
||||||
private RefWatcher refWatcher;
|
private RefWatcher refWatcher;
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
package fr.free.nrw.commons.upload;
|
package fr.free.nrw.commons.upload;
|
||||||
|
|
||||||
import android.Manifest;
|
import android.Manifest;
|
||||||
|
import android.Manifest.permission;
|
||||||
|
import android.app.AlertDialog;
|
||||||
import android.app.ProgressDialog;
|
import android.app.ProgressDialog;
|
||||||
import android.content.ContentResolver;
|
import android.content.ContentResolver;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
|
@ -12,7 +14,6 @@ import android.net.Uri;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.ParcelFileDescriptor;
|
import android.os.ParcelFileDescriptor;
|
||||||
import android.support.annotation.NonNull;
|
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
import android.support.v4.app.ActivityCompat;
|
import android.support.v4.app.ActivityCompat;
|
||||||
import android.support.v4.app.FragmentManager;
|
import android.support.v4.app.FragmentManager;
|
||||||
|
|
@ -23,6 +24,15 @@ import android.view.inputmethod.InputMethodManager;
|
||||||
import android.widget.AdapterView;
|
import android.widget.AdapterView;
|
||||||
import android.widget.Toast;
|
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.io.FileNotFoundException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
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.contributions.Contribution;
|
||||||
import fr.free.nrw.commons.media.MediaDetailPagerFragment;
|
import fr.free.nrw.commons.media.MediaDetailPagerFragment;
|
||||||
import fr.free.nrw.commons.modifications.CategoryModifier;
|
import fr.free.nrw.commons.modifications.CategoryModifier;
|
||||||
import fr.free.nrw.commons.modifications.ModificationsContentProvider;
|
|
||||||
import fr.free.nrw.commons.modifications.ModifierSequence;
|
import fr.free.nrw.commons.modifications.ModifierSequence;
|
||||||
import fr.free.nrw.commons.modifications.ModifierSequenceDao;
|
import fr.free.nrw.commons.modifications.ModifierSequenceDao;
|
||||||
import fr.free.nrw.commons.modifications.TemplateRemoveModifier;
|
import fr.free.nrw.commons.modifications.TemplateRemoveModifier;
|
||||||
|
|
@ -81,6 +90,11 @@ public class MultipleShareActivity extends AuthenticatedActivity
|
||||||
private boolean locationPermitted = false;
|
private boolean locationPermitted = false;
|
||||||
private boolean isMultipleUploadsPrepared = false;
|
private boolean isMultipleUploadsPrepared = false;
|
||||||
private boolean isMultipleUploadsFinalised = false; // Checks is user clicked to upload button or regret before this phase
|
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
|
@Override
|
||||||
public Media getMediaAtPosition(int i) {
|
public Media getMediaAtPosition(int i) {
|
||||||
|
|
@ -124,17 +138,6 @@ public class MultipleShareActivity extends AuthenticatedActivity
|
||||||
multipleUploadBegins();
|
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() {
|
private void multipleUploadBegins() {
|
||||||
|
|
||||||
Timber.d("Multiple upload begins");
|
Timber.d("Multiple upload begins");
|
||||||
|
|
@ -216,6 +219,7 @@ public class MultipleShareActivity extends AuthenticatedActivity
|
||||||
setContentView(R.layout.activity_multiple_uploads);
|
setContentView(R.layout.activity_multiple_uploads);
|
||||||
ButterKnife.bind(this);
|
ButterKnife.bind(this);
|
||||||
initDrawer();
|
initDrawer();
|
||||||
|
initPermissionsRationaleDialog();
|
||||||
|
|
||||||
if (savedInstanceState != null) {
|
if (savedInstanceState != null) {
|
||||||
photosList = savedInstanceState.getParcelableArrayList("uploadsList");
|
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
|
@Override
|
||||||
protected void onDestroy() {
|
protected void onDestroy() {
|
||||||
super.onDestroy();
|
super.onDestroy();
|
||||||
|
|
@ -275,12 +320,55 @@ public class MultipleShareActivity extends AuthenticatedActivity
|
||||||
isMultipleUploadsPrepared = false;
|
isMultipleUploadsPrepared = false;
|
||||||
mwApi.setAuthCookie(authCookie);
|
mwApi.setAuthCookie(authCookie);
|
||||||
if (!ExternalStorageUtils.isStoragePermissionGranted(this)) {
|
if (!ExternalStorageUtils.isStoragePermissionGranted(this)) {
|
||||||
ExternalStorageUtils.requestExternalStoragePermission(this);
|
//If permission is not there, handle the negative cases
|
||||||
|
askDexterToHandleExternalStoragePermission();
|
||||||
isMultipleUploadsPrepared = false;
|
isMultipleUploadsPrepared = false;
|
||||||
return; // Postpone operation to do after gettion permission
|
return; // Postpone operation to do after gettion permission
|
||||||
} else {
|
} else {
|
||||||
isMultipleUploadsPrepared = true;
|
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
|
* Prepares a list from files will be uploaded. Saves these files temporarily to external
|
||||||
* storage. Adds them to uploads list
|
* storage. Adds them to uploads list
|
||||||
*/
|
*/
|
||||||
private void prepareMultipleUpoadList() {
|
private void prepareMultipleUploadList() {
|
||||||
Intent intent = getIntent();
|
Intent intent = getIntent();
|
||||||
|
|
||||||
if (Intent.ACTION_SEND_MULTIPLE.equals(intent.getAction())) {
|
if (Intent.ACTION_SEND_MULTIPLE.equals(intent.getAction())) {
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,16 @@
|
||||||
package fr.free.nrw.commons.utils;
|
package fr.free.nrw.commons.utils;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
|
import android.app.AlertDialog;
|
||||||
|
import android.app.AlertDialog.Builder;
|
||||||
import android.app.Dialog;
|
import android.app.Dialog;
|
||||||
|
import android.content.Context;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
import android.support.v4.app.DialogFragment;
|
import android.support.v4.app.DialogFragment;
|
||||||
import android.support.v4.app.FragmentActivity;
|
import android.support.v4.app.FragmentActivity;
|
||||||
|
|
||||||
|
import fr.free.nrw.commons.R;
|
||||||
import timber.log.Timber;
|
import timber.log.Timber;
|
||||||
|
|
||||||
public class DialogUtil {
|
public class DialogUtil {
|
||||||
|
|
@ -92,4 +96,31 @@ public class DialogUtil {
|
||||||
Timber.e(e, "Could not show dialog.");
|
Timber.e(e, "Could not show dialog.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static AlertDialog getAlertDialogWithPositiveAndNegativeCallbacks(
|
||||||
|
Context context, String title, String message, int iconResourceId, Callback callback) {
|
||||||
|
|
||||||
|
AlertDialog alertDialog = new Builder(context)
|
||||||
|
.setTitle(title)
|
||||||
|
.setMessage(message)
|
||||||
|
.setPositiveButton(context.getString(R.string.ok), (dialog, which) -> {
|
||||||
|
callback.onPositiveButtonClicked();
|
||||||
|
dialog.dismiss();
|
||||||
|
})
|
||||||
|
.setNegativeButton(context.getString(R.string.cancel), (dialog, which) -> {
|
||||||
|
callback.onNegativeButtonClicked();
|
||||||
|
dialog.dismiss();
|
||||||
|
})
|
||||||
|
.setIcon(iconResourceId).create();
|
||||||
|
|
||||||
|
return alertDialog;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface Callback {
|
||||||
|
|
||||||
|
void onPositiveButtonClicked();
|
||||||
|
|
||||||
|
void onNegativeButtonClicked();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -352,4 +352,6 @@
|
||||||
<string name="images_reverted_explanation">The percentage of images you have uploaded to Commons that were not deleted</string>
|
<string name="images_reverted_explanation">The percentage of images you have uploaded to Commons that were not deleted</string>
|
||||||
<string name="images_used_explanation">The number of images you have uploaded to Commons that were used in Wikimedia articles</string>
|
<string name="images_used_explanation">The number of images you have uploaded to Commons that were used in Wikimedia articles</string>
|
||||||
<string name="notifications_channel_name_all">Commons Notification</string>
|
<string name="notifications_channel_name_all">Commons Notification</string>
|
||||||
|
<string name="storage_permission">Storage Permission</string>
|
||||||
|
<string name="write_storage_permission_rationale_for_image_share">We need your permission to access the external storage of your device in order to upload images.</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue