Skip to content

Commit e2aadc9

Browse files
ashishkumar468ilgazer
authored andcommitted
Feature/permissions library (commons-app#1855)
* Added permission for Dexter, the runtime permission handling library * [Preparing fir issue commons-app#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 commons-app#1773] * Improvements in the way External Storage Permission is handled in MultipleShareActivity[Bug fix commons-app#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
1 parent d455c35 commit e2aadc9

File tree

6 files changed

+172
-16
lines changed

6 files changed

+172
-16
lines changed

app/build.gradle

+3
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,9 @@ dependencies {
8787
debugImplementation "com.squareup.leakcanary:leakcanary-android:$LEAK_CANARY"
8888
releaseImplementation "com.squareup.leakcanary:leakcanary-android-no-op:$LEAK_CANARY"
8989
testImplementation "com.squareup.leakcanary:leakcanary-android-no-op:$LEAK_CANARY"
90+
//For handling runtime permissions
91+
implementation 'com.karumi:dexter:5.0.0'
92+
9093
}
9194

9295
android {

app/src/main/java/fr/free/nrw/commons/CommonsApplication.java

+9
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,11 @@ public class CommonsApplication extends Application {
5454
@Inject @Named("application_preferences") SharedPreferences applicationPrefs;
5555
@Inject @Named("prefs") SharedPreferences otherPrefs;
5656

57+
/**
58+
* Constants begin
59+
*/
60+
public static final int OPEN_APPLICATION_DETAIL_SETTINGS = 1001;
61+
5762
public static final String DEFAULT_EDIT_SUMMARY = "Uploaded using [[COM:MOA|Commons Mobile App]]";
5863

5964
public static final String FEEDBACK_EMAIL = "commons-app-android@googlegroups.com";
@@ -66,6 +71,10 @@ public class CommonsApplication extends Application {
6671

6772
public static final String NOTIFICATION_CHANNEL_ID_ALL = "CommonsNotificationAll";
6873

74+
/**
75+
* Constants End
76+
*/
77+
6978
private RefWatcher refWatcher;
7079

7180

app/src/main/java/fr/free/nrw/commons/upload/MultipleShareActivity.java

+104-16
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package fr.free.nrw.commons.upload;
22

33
import android.Manifest;
4+
import android.Manifest.permission;
5+
import android.app.AlertDialog;
46
import android.app.ProgressDialog;
57
import android.content.ContentResolver;
68
import android.content.Context;
@@ -12,7 +14,6 @@
1214
import android.os.Build;
1315
import android.os.Bundle;
1416
import android.os.ParcelFileDescriptor;
15-
import android.support.annotation.NonNull;
1617
import android.support.annotation.Nullable;
1718
import android.support.v4.app.ActivityCompat;
1819
import android.support.v4.app.FragmentManager;
@@ -23,6 +24,15 @@
2324
import android.widget.AdapterView;
2425
import android.widget.Toast;
2526

27+
import com.karumi.dexter.Dexter;
28+
import com.karumi.dexter.DexterBuilder;
29+
import com.karumi.dexter.listener.PermissionDeniedResponse;
30+
import com.karumi.dexter.listener.PermissionGrantedResponse;
31+
import com.karumi.dexter.listener.single.BasePermissionListener;
32+
import fr.free.nrw.commons.CommonsApplication;
33+
import fr.free.nrw.commons.utils.DialogUtil;
34+
import fr.free.nrw.commons.utils.DialogUtil.Callback;
35+
import fr.free.nrw.commons.utils.PermissionUtils;
2636
import java.io.FileNotFoundException;
2737
import java.util.ArrayList;
2838
import java.util.List;
@@ -41,7 +51,6 @@
4151
import fr.free.nrw.commons.contributions.Contribution;
4252
import fr.free.nrw.commons.media.MediaDetailPagerFragment;
4353
import fr.free.nrw.commons.modifications.CategoryModifier;
44-
import fr.free.nrw.commons.modifications.ModificationsContentProvider;
4554
import fr.free.nrw.commons.modifications.ModifierSequence;
4655
import fr.free.nrw.commons.modifications.ModifierSequenceDao;
4756
import fr.free.nrw.commons.modifications.TemplateRemoveModifier;
@@ -81,6 +90,11 @@ public class MultipleShareActivity extends AuthenticatedActivity
8190
private boolean locationPermitted = false;
8291
private boolean isMultipleUploadsPrepared = false;
8392
private boolean isMultipleUploadsFinalised = false; // Checks is user clicked to upload button or regret before this phase
93+
private final String TAG="#MultipleShareActivity#";
94+
private AlertDialog storagePermissionInfoDialog;
95+
private DexterBuilder dexterStoragePermissionBuilder;
96+
97+
private PermissionDeniedResponse permissionDeniedResponse;
8498

8599
@Override
86100
public Media getMediaAtPosition(int i) {
@@ -124,17 +138,6 @@ public void OnMultipleUploadInitiated() {
124138
multipleUploadBegins();
125139
}
126140

127-
@Override
128-
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
129-
if (requestCode == 1 && grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
130-
Timber.d("onRequestPermissionsResult external storage permission granted");
131-
prepareMultipleUpoadList();
132-
} else {
133-
// Permission is not granted, close activity
134-
finish();
135-
}
136-
}
137-
138141
private void multipleUploadBegins() {
139142

140143
Timber.d("Multiple upload begins");
@@ -216,6 +219,7 @@ protected void onCreate(Bundle savedInstanceState) {
216219
setContentView(R.layout.activity_multiple_uploads);
217220
ButterKnife.bind(this);
218221
initDrawer();
222+
initPermissionsRationaleDialog();
219223

220224
if (savedInstanceState != null) {
221225
photosList = savedInstanceState.getParcelableArrayList("uploadsList");
@@ -233,6 +237,47 @@ protected void onCreate(Bundle savedInstanceState) {
233237
}
234238
}
235239

240+
241+
/**
242+
* We have agreed to show a dialog showing why we need a particular permission.
243+
* This method is used to initialise the dialog which is going to show the permission's rationale.
244+
* The dialog is initialised along with a callback for positive and negative user actions.
245+
*/
246+
private void initPermissionsRationaleDialog() {
247+
if (storagePermissionInfoDialog == null) {
248+
storagePermissionInfoDialog = DialogUtil
249+
.getAlertDialogWithPositiveAndNegativeCallbacks(
250+
MultipleShareActivity.this,
251+
getString(R.string.storage_permission), getString(
252+
R.string.write_storage_permission_rationale_for_image_share),
253+
R.drawable.ic_launcher, new Callback() {
254+
@Override
255+
public void onPositiveButtonClicked() {
256+
//If the user is willing to give us the permission
257+
//But had somehow previously choose never ask again, we take him to app settings to manually enable permission
258+
if(null== permissionDeniedResponse){
259+
//Dexter returned null, lets see if this ever happens
260+
return;
261+
}
262+
else if (permissionDeniedResponse.isPermanentlyDenied()) {
263+
PermissionUtils.askUserToManuallyEnablePermissionFromSettings(MultipleShareActivity.this);
264+
} else {
265+
//or if we still have chance to show runtime permission dialog, we show him that.
266+
askDexterToHandleExternalStoragePermission();
267+
}
268+
}
269+
270+
@Override
271+
public void onNegativeButtonClicked() {
272+
//This was the behaviour as of now, I was planning to maybe snack him with some message
273+
//and then call finish after some time, or may be it could be associated with some action on the snack
274+
//If the user does not want us to give the permission, even after showing rationale dialog, lets not trouble him anymore
275+
finish();
276+
}
277+
});
278+
}
279+
}
280+
236281
@Override
237282
protected void onDestroy() {
238283
super.onDestroy();
@@ -275,20 +320,63 @@ protected void onAuthCookieAcquired(String authCookie) {
275320
isMultipleUploadsPrepared = false;
276321
mwApi.setAuthCookie(authCookie);
277322
if (!ExternalStorageUtils.isStoragePermissionGranted(this)) {
278-
ExternalStorageUtils.requestExternalStoragePermission(this);
323+
//If permission is not there, handle the negative cases
324+
askDexterToHandleExternalStoragePermission();
279325
isMultipleUploadsPrepared = false;
280326
return; // Postpone operation to do after gettion permission
281327
} else {
282328
isMultipleUploadsPrepared = true;
283-
prepareMultipleUpoadList();
329+
prepareMultipleUploadList();
330+
}
331+
}
332+
333+
/**
334+
* This method initialised the Dexter's permission builder (if not already initialised). Also makes sure that the builder is initialised
335+
* 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
336+
* permission and then handles the permission status, thanks to Dexter's appropriate callbacks.
337+
*/
338+
private void askDexterToHandleExternalStoragePermission() {
339+
Timber.d(TAG, "External storage permission is being requested");
340+
if (null == dexterStoragePermissionBuilder) {
341+
dexterStoragePermissionBuilder = Dexter.withActivity(this)
342+
.withPermission(permission.WRITE_EXTERNAL_STORAGE)
343+
.withListener(new BasePermissionListener() {
344+
@Override
345+
public void onPermissionGranted(PermissionGrantedResponse response) {
346+
Timber.d(TAG,"User has granted us the permission for writing the external storage");
347+
//If permission is granted, well and good
348+
prepareMultipleUploadList();
349+
}
350+
351+
@Override
352+
public void onPermissionDenied(PermissionDeniedResponse response) {
353+
Timber.d(TAG,"User has granted us the permission for writing the external storage");
354+
//If permission is not granted in whatsoever scenario, we show him a dialog stating why we need the permission
355+
permissionDeniedResponse=response;
356+
if (null != storagePermissionInfoDialog && !storagePermissionInfoDialog
357+
.isShowing()) {
358+
storagePermissionInfoDialog.show();
359+
}
360+
}
361+
});
362+
}
363+
dexterStoragePermissionBuilder.check();
364+
}
365+
366+
@Override
367+
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
368+
super.onActivityResult(requestCode, resultCode, data);
369+
if (requestCode == CommonsApplication.OPEN_APPLICATION_DETAIL_SETTINGS) {
370+
//OnActivity result, no matter what the result is, our function can handle that.
371+
askDexterToHandleExternalStoragePermission();
284372
}
285373
}
286374

287375
/**
288376
* Prepares a list from files will be uploaded. Saves these files temporarily to external
289377
* storage. Adds them to uploads list
290378
*/
291-
private void prepareMultipleUpoadList() {
379+
private void prepareMultipleUploadList() {
292380
Intent intent = getIntent();
293381

294382
if (Intent.ACTION_SEND_MULTIPLE.equals(intent.getAction())) {

app/src/main/java/fr/free/nrw/commons/utils/DialogUtil.java

+31
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
package fr.free.nrw.commons.utils;
22

33
import android.app.Activity;
4+
import android.app.AlertDialog;
5+
import android.app.AlertDialog.Builder;
46
import android.app.Dialog;
7+
import android.content.Context;
58
import android.os.Build;
69
import android.support.annotation.Nullable;
710
import android.support.v4.app.DialogFragment;
811
import android.support.v4.app.FragmentActivity;
912

13+
import fr.free.nrw.commons.R;
1014
import timber.log.Timber;
1115

1216
public class DialogUtil {
@@ -92,4 +96,31 @@ public static void showSafely(FragmentActivity activity, DialogFragment dialog)
9296
Timber.e(e, "Could not show dialog.");
9397
}
9498
}
99+
100+
public static AlertDialog getAlertDialogWithPositiveAndNegativeCallbacks(
101+
Context context, String title, String message, int iconResourceId, Callback callback) {
102+
103+
AlertDialog alertDialog = new Builder(context)
104+
.setTitle(title)
105+
.setMessage(message)
106+
.setPositiveButton(context.getString(R.string.ok), (dialog, which) -> {
107+
callback.onPositiveButtonClicked();
108+
dialog.dismiss();
109+
})
110+
.setNegativeButton(context.getString(R.string.cancel), (dialog, which) -> {
111+
callback.onNegativeButtonClicked();
112+
dialog.dismiss();
113+
})
114+
.setIcon(iconResourceId).create();
115+
116+
return alertDialog;
117+
118+
}
119+
120+
public interface Callback {
121+
122+
void onPositiveButtonClicked();
123+
124+
void onNegativeButtonClicked();
125+
}
95126
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package fr.free.nrw.commons.utils;
2+
3+
import android.app.Activity;
4+
import android.content.Intent;
5+
import android.net.Uri;
6+
import android.provider.Settings;
7+
import fr.free.nrw.commons.CommonsApplication;
8+
9+
public class PermissionUtils {
10+
11+
/**
12+
* This method can be used by any activity which requires a permission which has been blocked(marked never ask again by the user)
13+
It open the app settings from where the user can manually give us the required permission.
14+
* @param activity
15+
*/
16+
public static void askUserToManuallyEnablePermissionFromSettings(
17+
Activity activity) {
18+
Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
19+
Uri uri = Uri.fromParts("package", activity.getPackageName(), null);
20+
intent.setData(uri);
21+
activity.startActivityForResult(intent,CommonsApplication.OPEN_APPLICATION_DETAIL_SETTINGS);
22+
}
23+
}

app/src/main/res/values/strings.xml

+2
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,8 @@
354354
s <string name="notifications_channel_name_all">Commons Notification</string>
355355

356356
<string name="step_count">Step %1$d of %2$d</string>
357+
<string name="storage_permission">Storage Permission</string>
358+
<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>
357359
<string name="next">Next</string>
358360
<string name="previous">Previous</string>
359361
<string name="submit">Submit</string>

0 commit comments

Comments
 (0)