diff --git a/.travis.yml b/.travis.yml index 03e43dc2..41183b52 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,9 +1,11 @@ language: android +jdk: oraclejdk8 android: components: - tools - build-tools-23.0.2 + - platform-tools - android-23 - extra-android-support - extra-google-m2repository diff --git a/README.md b/README.md index a8887de7..6aad2385 100644 --- a/README.md +++ b/README.md @@ -13,33 +13,35 @@ Dexter frees your permission code from your activities and lets you write that l Screenshots ----------- -![Demo screenshot][1] Usage ----- -To start using the library you just need to initialize Dexter with a ``Context``, preferably your ``Application`` as it won't be destroyed during your app lifetime: +To start using the library you just need to call `Dexter` with a valid `Activity`: ```java -public MyApplication extends Application { +public MyActivity extends Activity { @Override public void onCreate() { super.onCreate(); - Dexter.initialize(context); + Dexter.withActivity(activity) + .withPermission(permission) + .withListener(listener) + .check(); } } ``` -Once the library is initialized you can start checking permissions at will. You have two options, you can either check for a single permission or check for multiple permissions at once. - ###Single permission For each permission, register a ``PermissionListener`` implementation to receive the state of the request: ```java -Dexter.checkPermission(new PermissionListener() { - @Override public void onPermissionGranted(PermissionGrantedResponse response) {/* ... */} - @Override public void onPermissionDenied(PermissionDeniedResponse response) {/* ... */} - @Override public void onPermissionRationaleShouldBeShown(PermissionRequest permission, PermissionToken token) {/* ... */} -}, Manifest.permission.CAMERA); +Dexter.withActivity(this) + .withPermission(Manifest.permission.CAMERA) + .withListener(new PermissionListener() { + @Override public void onPermissionGranted(PermissionGrantedResponse response) {/* ... */} + @Override public void onPermissionDenied(PermissionDeniedResponse response) {/* ... */} + @Override public void onPermissionRationaleShouldBeShown(PermissionRequest permission, PermissionToken token) {/* ... */} + }).check(); ``` To make your life easier we offer some ``PermissionListener`` implementations to perform recurrent actions: @@ -56,7 +58,6 @@ PermissionListener dialogPermissionListener = .withButtonText(android.R.string.ok) .withIcon(R.mipmap.my_icon) .build(); -Dexter.checkPermission(dialogPermissionListener, Manifest.permission.CAMERA); ``` * ``SnackbarOnDeniedPermissionListener`` to show a snackbar message whenever the user rejects a permission request: @@ -75,9 +76,7 @@ PermissionListener snackbarPermissionListener = public void onDismissed(Snackbar snackbar, int event) { // Event handler for when the given Snackbar has been dismissed } - }) - .build(); -Dexter.checkPermission(snackbarPermissionListener, Manifest.permission.CAMERA); + }).build(); ``` * ``CompositePermissionListener`` to compound multiple listeners into one: @@ -85,17 +84,22 @@ Dexter.checkPermission(snackbarPermissionListener, Manifest.permission.CAMERA); ```java PermissionListener snackbarPermissionListener = /*...*/; PermissionListener dialogPermissionListener = /*...*/; -Dexter.checkPermission(new CompositePermissionListener(snackbarPermissionListener, dialogPermissionListener, /*...*/), Manifest.permission.CAMERA); +PermissionListener compositePermissionListener = new CompositePermissionListener(snackbarPermissionListener, dialogPermissionListener, /*...*/); ``` ###Multiple permissions -If you want to request multiple permissions you just need to do the same but registering an implementation of ``MultiplePermissionsListener``: +If you want to request multiple permissions you just need to call `withPermissions` and register an implementation of ``MultiplePermissionsListener``: ```java -Dexter.checkPermissions(new MultiplePermissionsListener() { - @Override public void onPermissionsChecked(MultiplePermissionsReport report) {/* ... */} - @Override public void onPermissionRationaleShouldBeShown(List permissions, PermissionToken token) {/* ... */} -}, Manifest.permission.CAMERA, Manifest.permission.READ_CONTACTS, Manifest.permission.RECORD_AUDIO); +Dexter.withActivity(this) + .withPermissions( + Manifest.permission.CAMERA, + Manifest.permission.READ_CONTACTS, + Manifest.permission.RECORD_AUDIO + ).withListener(new MultiplePermissionsListener() { + @Override public void onPermissionsChecked(MultiplePermissionsReport report) {/* ... */} + @Override public void onPermissionRationaleShouldBeShown(List permissions, PermissionToken token) {/* ... */} + }).check(); ``` The ``MultiplePermissionsReport`` contains all the details of the permission request like the list of denied/granted permissions or utility methods like ``areAllPermissionsGranted`` and ``isAnyPermissionPermanentlyDenied``. @@ -114,7 +118,6 @@ MultiplePermissionsListener dialogMultiplePermissionsListener = .withButtonText(android.R.string.ok) .withIcon(R.mipmap.my_icon) .build(); -Dexter.checkPermissions(dialogMultiplePermissionsListener, Manifest.permission.CAMERA, Manifest.permission.READ_CONTACTS, Manifest.permission.RECORD_AUDIO); ``` * ``SnackbarOnAnyDeniedMultiplePermissionsListener`` to show a snackbar message whenever the user rejects any of the requested permissions: @@ -135,7 +138,6 @@ MultiplePermissionsListener snackbarMultiplePermissionsListener = } }) .build(); -Dexter.checkPermissions(snackbarMultiplePermissionsListener, Manifest.permission.CAMERA, Manifest.permission.READ_CONTACTS, Manifest.permission.RECORD_AUDIO); ``` * ``CompositePermissionListener`` to compound multiple listeners into one: @@ -143,39 +145,60 @@ Dexter.checkPermissions(snackbarMultiplePermissionsListener, Manifest.permission ```java MultiplePermissionsListener snackbarMultiplePermissionsListener = /*...*/; MultiplePermissionsListener dialogMultiplePermissionsListener = /*...*/; -Dexter.checkPermissions(new CompositeMultiplePermissionsListener(snackbarMultiplePermissionsListener, dialogMultiplePermissionsListener, /*...*/), Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO); +MultiplePermissionsListener compositePermissionsListener = new CompositeMultiplePermissionsListener(snackbarMultiplePermissionsListener, dialogMultiplePermissionsListener, /*...*/); ``` ###Handling listener threads -If you want to receive permission listener callbacks on the same thread that fired the permission request, you just need to use the ``OnSameThread`` version of the single and multiple permissions methods. +If you want to receive permission listener callbacks on the same thread that fired the permission request, you just need to call ``onSameThread`` before checking for permissions: -* ``checkPermissionOnSameThread`` to request a single permission and receive callbacks in the thread that fired the request -* ``checkPermissionsOnSameThread`` to request multiple permissions and receive callbacks in the thread that fired the request +```java +Dexter.withActivity(activity) + .withPermission(permission) + .withListener(listener) + .onSameThread() + .check(); +``` ###Showing a rationale Android will notify you when you are requesting a permission that needs an additional explanation for its usage, either because it is considered dangerous, or because the user has already declined that permission once. -Dexter will call the method ``onPermissionRationaleShouldBeShown`` implemented in your listener with a ``PermissionToken``. It's important to keep in mind that the request process will pause until the token is used, therefore, you won't be able to call Dexter again or request any other permissions if the token has not been used. +Dexter will call the method ``onPermissionRationaleShouldBeShown`` implemented in your listener with a ``PermissionToken``. **It's important to keep in mind that the request process will pause until the token is used**, therefore, you won't be able to call Dexter again or request any other permissions if the token has not been used. The most simple implementation of your ``onPermissionRationaleShouldBeShown`` method could be: ```java @Override public void onPermissionRationaleShouldBeShown(PermissionRequest permission, PermissionToken token) { - token.continuePermissionRequest(); - } + token.continuePermissionRequest(); +} ``` ###Screen rotation If your application has to support configuration changes based on screen rotation remember to add a call to ``Dexter`` in your Activity ``onCreate`` method as follows: ```java - @Override protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.sample_activity); - Dexter.continuePendingRequestsIfPossible(permissionsListener); - } +@Override protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.sample_activity); + Dexter.withActivity(this).continueRequestingPendingPermissions(permissionListener); +} +``` + +###Error handling +If you think there is an error in your Dexter integration, just register a `PermissionRequestErrorListener` when calling Dexter: + +```java +Dexter.withActivity(activity) + .withPermission(permission) + .withListener(listener) + .withErrorListener(new PermissionRequestErrorListener() { + @Override public void onError(DexterError error) { + Log.e("Dexter", "There was an error: " + error.toString()); + } + }).check(); ``` +The library will notify you when something bad happens. In general, it is a good practice to, at least, log every error Dexter may throw but is up to you, the developer, to do that. + **IMPORTANT**: Remember to follow the [Google design guidelines] [2] to make your application as user-friendly as possible. Add it to your project @@ -204,13 +227,14 @@ Caveats ------- * For permissions that did not exist before API Level 16, you should check the OS version and use *ContextCompat.checkSelfPermission*. See [You Cannot Hold Non-Existent Permissions](https://commonsware.com/blog/2015/11/09/you-cannot-hold-nonexistent-permissions.html). * Don't call Dexter from an Activity with the flag `noHistory` enabled. When a permission is requested, Dexter creates its own Activity internally and pushes it into the stack causing the original Activity to be dismissed. +* Permissions `SYSTEM_ALERT_WINDOW` and `WRITE_SETTINGS` are considered special by Android. Dexter doesn't handle those and you'll need to request them in the old fashioned way. Do you want to contribute? -------------------------- Feel free to add any useful feature to the library, we will be glad to improve it with your help. -Keep in mind that your PRs **must** be validated by Travis-CI. Please, run a local build with ``./gradlew checkstyle build`` before submiting your code. +Keep in mind that your PRs **must** be validated by Travis-CI. Please, run a local build with ``./gradlew checkstyle build test`` before submiting your code. Libraries used in this project diff --git a/build.gradle b/build.gradle index 3531b4ea..b2e33cf9 100644 --- a/build.gradle +++ b/build.gradle @@ -3,7 +3,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:1.3.0' + classpath 'com.android.tools.build:gradle:2.2.3' } } diff --git a/dexter/src/main/AndroidManifest.xml b/dexter/src/main/AndroidManifest.xml index 5ce10cb3..5e950893 100644 --- a/dexter/src/main/AndroidManifest.xml +++ b/dexter/src/main/AndroidManifest.xml @@ -22,7 +22,7 @@ diff --git a/dexter/src/main/java/com/karumi/dexter/AndroidPermissionService.java b/dexter/src/main/java/com/karumi/dexter/AndroidPermissionService.java index c74a7a65..35102221 100644 --- a/dexter/src/main/java/com/karumi/dexter/AndroidPermissionService.java +++ b/dexter/src/main/java/com/karumi/dexter/AndroidPermissionService.java @@ -19,6 +19,7 @@ import android.app.Activity; import android.content.Context; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.support.v4.app.ActivityCompat; import android.support.v4.content.ContextCompat; @@ -37,16 +38,24 @@ int checkSelfPermission(@NonNull Context context, @NonNull String permission) { /** * @see ActivityCompat#requestPermissions */ - void requestPermissions(@NonNull Activity activity, @NonNull String[] permissions, + void requestPermissions(@Nullable Activity activity, @NonNull String[] permissions, int requestCode) { + if (activity == null) { + return; + } + ActivityCompat.requestPermissions(activity, permissions, requestCode); } /** * @see ActivityCompat#shouldShowRequestPermissionRationale */ - boolean shouldShowRequestPermissionRationale(@NonNull Activity activity, + boolean shouldShowRequestPermissionRationale(@Nullable Activity activity, @NonNull String permission) { + if (activity == null) { + return false; + } + return ActivityCompat.shouldShowRequestPermissionRationale(activity, permission); } } diff --git a/dexter/src/main/java/com/karumi/dexter/Dexter.java b/dexter/src/main/java/com/karumi/dexter/Dexter.java index 025f1013..aad8ca68 100644 --- a/dexter/src/main/java/com/karumi/dexter/Dexter.java +++ b/dexter/src/main/java/com/karumi/dexter/Dexter.java @@ -18,27 +18,128 @@ import android.app.Activity; import android.content.Context; +import com.karumi.dexter.listener.EmptyPermissionRequestErrorListener; +import com.karumi.dexter.listener.PermissionRequestErrorListener; +import com.karumi.dexter.listener.multi.EmptyMultiplePermissionsListener; import com.karumi.dexter.listener.multi.MultiplePermissionsListener; import com.karumi.dexter.listener.single.PermissionListener; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.Collections; /** * Class to simplify the management of Android runtime permissions - * Dexter needs to be initialized before checking for a permission using {@link - * #initialize(Context)} + * You can use this class directly using the provided fluent API like: + * + * Dexter.withActivity(activity) + * .withPermission(permission) + * .withListener(listener) + * .onSameThread() + * .check() */ -public final class Dexter { +public final class Dexter + implements DexterBuilder, DexterBuilder.Permission, DexterBuilder.SinglePermissionListener, + DexterBuilder.MultiPermissionListener { private static DexterInstance instance; + private Collection permissions; + private MultiplePermissionsListener listener = new EmptyMultiplePermissionsListener(); + private PermissionRequestErrorListener errorListener = new EmptyPermissionRequestErrorListener(); + private boolean shouldExecuteOnSameThread = false; + + private Dexter(Activity activity) { + newInitialize(activity); + } + + public static DexterBuilder.Permission withActivity(Activity activity) { + return new Dexter(activity); + } + + @Override public DexterBuilder.SinglePermissionListener withPermission(String permission) { + permissions = Collections.singletonList(permission); + return this; + } + + @Override public DexterBuilder.MultiPermissionListener withPermissions(String... permissions) { + this.permissions = Arrays.asList(permissions); + return this; + } + + @Override + public DexterBuilder.MultiPermissionListener withPermissions(Collection permissions) { + this.permissions = new ArrayList<>(permissions); + return this; + } + + @Override public void continueRequestingPendingPermissions(PermissionListener listener) { + instance.continuePendingRequestIfPossible(listener, ThreadFactory.makeMainThread()); + } + + @Override public void continueRequestingPendingPermissions(MultiplePermissionsListener listener) { + instance.continuePendingRequestsIfPossible(listener, ThreadFactory.makeMainThread()); + } + + @Override public DexterBuilder withListener(PermissionListener listener) { + this.listener = new MultiplePermissionsListenerToPermissionListenerAdapter(listener); + return this; + } + + @Override public DexterBuilder withListener(MultiplePermissionsListener listener) { + this.listener = listener; + return this; + } + + @Override public DexterBuilder onSameThread() { + shouldExecuteOnSameThread = true; + return this; + } + + @Override public DexterBuilder withErrorListener(PermissionRequestErrorListener errorListener) { + this.errorListener = errorListener; + return this; + } + + @Override public void check() { + try { + Thread thread = getThread(); + instance.checkPermissions(listener, permissions, thread); + } catch (DexterException e) { + errorListener.onError(e.error); + } + } + + private Thread getThread() { + Thread thread; + + if (shouldExecuteOnSameThread) { + thread = ThreadFactory.makeSameThread(); + } else { + thread = ThreadFactory.makeMainThread(); + } + + return thread; + } + /** - * Initializes the library + * Initializes the library. * * @param context Context used by Dexter. Use your {@link android.app.Application} to make sure - * the instance is not cleaned up during your app lifetime + * the instance is not cleaned up during your app lifetime. + * @deprecated There is no need to initialize Dexter anymore. Use `Dexter.withActivity()` to + * perform any permission check. */ - public static void initialize(Context context) { + @Deprecated public static void initialize(Context context) { + if (instance == null) { + AndroidPermissionService androidPermissionService = new AndroidPermissionService(); + IntentProvider intentProvider = new IntentProvider(); + instance = new DexterInstance(context.getApplicationContext(), androidPermissionService, + intentProvider); + } + } + + private static void newInitialize(Context context) { if (instance == null) { AndroidPermissionService androidPermissionService = new AndroidPermissionService(); IntentProvider intentProvider = new IntentProvider(); @@ -54,8 +155,15 @@ public static void initialize(Context context) { * * @param listener The class that will be reported when the state of the permission is ready * @param permission One of the values found in {@link android.Manifest.permission} + * @deprecated Use the builder instead: + * Dexter.withActivity(activity) + * .withPermission(permission) + * .withListener(listener) + * .onSameThread() + * .check() */ - public static void checkPermissionOnSameThread(PermissionListener listener, String permission) { + @Deprecated public static void checkPermissionOnSameThread(PermissionListener listener, + String permission) { checkInstanceNotNull(); instance.checkPermission(listener, permission, ThreadFactory.makeSameThread()); } @@ -68,8 +176,13 @@ public static void checkPermissionOnSameThread(PermissionListener listener, Stri * * @param listener The class that will be reported when the state of the permission is ready * @param permission One of the values found in {@link android.Manifest.permission} + * @deprecated Use the builder instead: + * Dexter.withActivity(activity) + * .withPermission(permission) + * .withListener(listener) + * .check() */ - public static void checkPermission(PermissionListener listener, String permission) { + @Deprecated public static void checkPermission(PermissionListener listener, String permission) { checkInstanceNotNull(); instance.checkPermission(listener, permission, ThreadFactory.makeMainThread()); } @@ -82,12 +195,18 @@ public static void checkPermission(PermissionListener listener, String permissio * * @param listener The class that will be reported when the state of the permissions are ready * @param permissions Array of values found in {@link android.Manifest.permission} + * @deprecated Use the builder instead: + * + * Dexter.withActivity(activity) + * .withPermissions(permissionA, permissionB) + * .withListener(listener) + * .onSameThread() + * .check() */ - public static void checkPermissionsOnSameThread(MultiplePermissionsListener listener, + @Deprecated public static void checkPermissionsOnSameThread(MultiplePermissionsListener listener, String... permissions) { checkInstanceNotNull(); - instance.checkPermissions(listener, Arrays.asList(permissions), - ThreadFactory.makeSameThread()); + instance.checkPermissions(listener, Arrays.asList(permissions), ThreadFactory.makeSameThread()); } /** @@ -98,11 +217,17 @@ public static void checkPermissionsOnSameThread(MultiplePermissionsListener list * * @param listener The class that will be reported when the state of the permissions are ready * @param permissions Array of values found in {@link android.Manifest.permission} + * @deprecated Use the builder instead: + * + * Dexter.withActivity(activity) + * .withPermissions(permissionA, permissionB) + * .withListener(listener) + * .check() */ - public static void checkPermissions(MultiplePermissionsListener listener, String... permissions) { + @Deprecated public static void checkPermissions(MultiplePermissionsListener listener, + String... permissions) { checkInstanceNotNull(); - instance.checkPermissions(listener, Arrays.asList(permissions), - ThreadFactory.makeMainThread()); + instance.checkPermissions(listener, Arrays.asList(permissions), ThreadFactory.makeMainThread()); } /** @@ -111,8 +236,15 @@ public static void checkPermissions(MultiplePermissionsListener listener, String * * @param listener The class that will be reported when the state of the permissions are ready * @param permissions Collection of values found in {@link android.Manifest.permission} + * @deprecated Use the builder instead: + * + * Dexter.withActivity(activity) + * .withPermissions(permissions) + * .withListener(listener) + * .onSameThread() + * .check() */ - public static void checkPermissions(MultiplePermissionsListener listener, + @Deprecated public static void checkPermissions(MultiplePermissionsListener listener, Collection permissions) { checkInstanceNotNull(); instance.checkPermissions(listener, permissions, ThreadFactory.makeMainThread()); @@ -122,8 +254,13 @@ public static void checkPermissions(MultiplePermissionsListener listener, * Checks is there is any permission request still ongoing. * If so, state of permissions must not be checked until it is resolved * or it will cause an exception. + * + * @deprecated There is no need to check if there is any pending request ongoing. If there is any, + * Dexter won't throw an exception anymore. Instead you can subscribe an + * {@link PermissionRequestErrorListener} instance to be notified whenever two + * requests are being processed at the same time. */ - public static boolean isRequestOngoing() { + @Deprecated public static boolean isRequestOngoing() { checkInstanceNotNull(); return instance.isRequestOngoing(); } @@ -132,8 +269,14 @@ public static boolean isRequestOngoing() { * Requests pending permissions if there were permissions lost. This method can be used to * recover the Dexter state during a configuration change, for example when the device is * rotated. + * + * @deprecated Use the builder instead: + * + * Dexter.withActivity(activity) + * .continueRequestingPendingPermissions(listener) */ - public static void continuePendingRequestsIfPossible(MultiplePermissionsListener listener) { + @Deprecated public static void continuePendingRequestsIfPossible( + MultiplePermissionsListener listener) { checkInstanceNotNull(); instance.continuePendingRequestsIfPossible(listener, ThreadFactory.makeMainThread()); } @@ -142,8 +285,13 @@ public static void continuePendingRequestsIfPossible(MultiplePermissionsListener * Requests pending permission if there was a permissions lost. This method can be used to * recover the Dexter state during a configuration change, for example when the device is * rotated. + * + * @deprecated Use the builder instead: + * + * Dexter.withActivity(activity) + * .continueRequestingPendingPermissions(listener) */ - public static void continuePendingRequestIfPossible(PermissionListener listener) { + @Deprecated public static void continuePendingRequestIfPossible(PermissionListener listener) { checkInstanceNotNull(); instance.continuePendingRequestIfPossible(listener, ThreadFactory.makeMainThread()); } diff --git a/dexter/src/main/java/com/karumi/dexter/DexterBuilder.java b/dexter/src/main/java/com/karumi/dexter/DexterBuilder.java new file mode 100644 index 00000000..352c67b5 --- /dev/null +++ b/dexter/src/main/java/com/karumi/dexter/DexterBuilder.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2016 Karumi. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +package com.karumi.dexter; + +import com.karumi.dexter.listener.PermissionRequestErrorListener; +import com.karumi.dexter.listener.multi.MultiplePermissionsListener; +import com.karumi.dexter.listener.single.PermissionListener; +import java.util.Collection; + +public interface DexterBuilder { + + DexterBuilder onSameThread(); + + DexterBuilder withErrorListener(PermissionRequestErrorListener errorListener); + + void check(); + + interface Permission { + DexterBuilder.SinglePermissionListener withPermission(String permission); + + DexterBuilder.MultiPermissionListener withPermissions(String... permissions); + + DexterBuilder.MultiPermissionListener withPermissions(Collection permissions); + + void continueRequestingPendingPermissions(PermissionListener listener); + + void continueRequestingPendingPermissions(MultiplePermissionsListener listener); + } + + interface SinglePermissionListener { + DexterBuilder withListener(PermissionListener listener); + } + + interface MultiPermissionListener { + DexterBuilder withListener(MultiplePermissionsListener listener); + } +} \ No newline at end of file diff --git a/dexter/src/main/java/com/karumi/dexter/DexterException.java b/dexter/src/main/java/com/karumi/dexter/DexterException.java new file mode 100644 index 00000000..7b2b2fd2 --- /dev/null +++ b/dexter/src/main/java/com/karumi/dexter/DexterException.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2016 Karumi. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.karumi.dexter; + +import com.karumi.dexter.listener.DexterError; + +final class DexterException extends IllegalStateException { + + public final DexterError error; + + public DexterException(String detailMessage, DexterError error) { + super(detailMessage); + this.error = error; + } +} diff --git a/dexter/src/main/java/com/karumi/dexter/DexterInstance.java b/dexter/src/main/java/com/karumi/dexter/DexterInstance.java index 3ea2e5c7..3fa64490 100644 --- a/dexter/src/main/java/com/karumi/dexter/DexterInstance.java +++ b/dexter/src/main/java/com/karumi/dexter/DexterInstance.java @@ -17,15 +17,18 @@ package com.karumi.dexter; import android.app.Activity; +import android.app.Application; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; +import com.karumi.dexter.listener.DexterError; import com.karumi.dexter.listener.PermissionDeniedResponse; import com.karumi.dexter.listener.PermissionGrantedResponse; import com.karumi.dexter.listener.PermissionRequest; import com.karumi.dexter.listener.multi.EmptyMultiplePermissionsListener; import com.karumi.dexter.listener.multi.MultiplePermissionsListener; import com.karumi.dexter.listener.single.PermissionListener; +import java.lang.ref.WeakReference; import java.util.Collection; import java.util.Collections; import java.util.LinkedList; @@ -42,13 +45,14 @@ final class DexterInstance { private static final MultiplePermissionsListener EMPTY_LISTENER = new EmptyMultiplePermissionsListener(); - private final Context context; + private final WeakReference context; private final AndroidPermissionService androidPermissionService; private final IntentProvider intentProvider; private final Collection pendingPermissions; private final MultiplePermissionsReport multiplePermissionsReport; private final AtomicBoolean isRequestingPermission; private final AtomicBoolean rationaleAccepted; + private final AtomicBoolean isShowingNativeDialog; private final Object pendingPermissionsMutex = new Object(); private Activity activity; @@ -56,18 +60,18 @@ final class DexterInstance { DexterInstance(Context context, AndroidPermissionService androidPermissionService, IntentProvider intentProvider) { - this.context = context.getApplicationContext(); + this.context = new WeakReference<>(context); this.androidPermissionService = androidPermissionService; this.intentProvider = intentProvider; this.pendingPermissions = new TreeSet<>(); this.multiplePermissionsReport = new MultiplePermissionsReport(); this.isRequestingPermission = new AtomicBoolean(); this.rationaleAccepted = new AtomicBoolean(); + this.isShowingNativeDialog = new AtomicBoolean(); } /** * Checks the state of a specific permission reporting it when ready to the listener. - * . * * @param listener The class that will be reported when the state of the permission is ready * @param permission One of the values found in {@link android.Manifest.permission} @@ -104,8 +108,7 @@ void continuePendingRequestIfPossible(PermissionListener listener, Thread thread * Check if there are some permissions pending to be confirmed by the user and restarts the * request for permission process. */ - void continuePendingRequestsIfPossible(MultiplePermissionsListener listener, - Thread thread) { + void continuePendingRequestsIfPossible(MultiplePermissionsListener listener, Thread thread) { if (!pendingPermissions.isEmpty()) { this.listener = new MultiplePermissionListenerThreadDecorator(listener, thread); if (!rationaleAccepted.get()) { @@ -178,9 +181,12 @@ boolean isRequestOngoing() { /** * Starts the native request permissions process */ - void requestPermissionsToSystem(Collection permissions) { - androidPermissionService.requestPermissions(activity, - permissions.toArray(new String[permissions.size()]), PERMISSIONS_REQUEST_CODE); + private void requestPermissionsToSystem(Collection permissions) { + if (!isShowingNativeDialog.get()) { + androidPermissionService.requestPermissions(activity, + permissions.toArray(new String[permissions.size()]), PERMISSIONS_REQUEST_CODE); + } + isShowingNativeDialog.set(true); } private PermissionStates getPermissionStates(Collection pendingPermissions) { @@ -217,8 +223,15 @@ private int checkSelfPermission(Activity activity, String permission) { } private void startTransparentActivityIfNeeded() { + Context context = this.context.get(); + if (context == null) { + return; + } + Intent intent = intentProvider.get(context, DexterActivity.class); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + if (context instanceof Application) { + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + } context.startActivity(intent); } @@ -273,6 +286,7 @@ private void onPermissionsChecked(Collection permissions) { activity = null; isRequestingPermission.set(false); rationaleAccepted.set(false); + isShowingNativeDialog.set(false); MultiplePermissionsListener currentListener = listener; listener = EMPTY_LISTENER; currentListener.onPermissionsChecked(multiplePermissionsReport); @@ -282,13 +296,15 @@ private void onPermissionsChecked(Collection permissions) { private void checkNoDexterRequestOngoing() { if (isRequestingPermission.getAndSet(true)) { - throw new IllegalStateException("Only one Dexter request at a time is allowed"); + throw new DexterException("Only one Dexter request at a time is allowed", + DexterError.REQUEST_ONGOING); } } private void checkRequestSomePermission(Collection permissions) { if (permissions.isEmpty()) { - throw new IllegalStateException("Dexter has to be called with at least one permission"); + throw new DexterException("Dexter has to be called with at least one permission", + DexterError.NO_PERMISSIONS_REQUESTED); } } @@ -299,20 +315,54 @@ private void checkSinglePermission(PermissionListener listener, String permissio checkMultiplePermissions(adapter, Collections.singleton(permission), thread); } - private void checkMultiplePermissions(MultiplePermissionsListener listener, - Collection permissions, Thread thread) { + private void checkMultiplePermissions(final MultiplePermissionsListener listener, + final Collection permissions, Thread thread) { checkNoDexterRequestOngoing(); checkRequestSomePermission(permissions); + if (context.get() == null) { + return; + } + pendingPermissions.clear(); pendingPermissions.addAll(permissions); multiplePermissionsReport.clear(); this.listener = new MultiplePermissionListenerThreadDecorator(listener, thread); - - startTransparentActivityIfNeeded(); + if (isEveryPermissionGranted(permissions, context.get()) && isContextInstanceOfActivity()) { + thread.execute(new Runnable() { + @Override public void run() { + MultiplePermissionsReport report = new MultiplePermissionsReport(); + for (String permission : permissions) { + report.addGrantedPermissionResponse(PermissionGrantedResponse.from(permission)); + } + isRequestingPermission.set(false); + listener.onPermissionsChecked(report); + } + }); + } else { + startTransparentActivityIfNeeded(); + } thread.loop(); } + /** + * To be able to provide backward compatibility with the Dexter's 2.X.X version we need to check + * at some point if the context instance is an instance of {@link android.app.Activity}. + */ + private boolean isContextInstanceOfActivity() { + return context.get() != null && context.get() instanceof Activity; + } + + private boolean isEveryPermissionGranted(Collection permissions, Context context) { + for (String permission : permissions) { + int permissionState = androidPermissionService.checkSelfPermission(context, permission); + if (permissionState != PackageManager.PERMISSION_GRANTED) { + return false; + } + } + return true; + } + private final class PermissionStates { private final Collection deniedPermissions = new LinkedList<>(); private final Collection grantedPermissions = new LinkedList<>(); @@ -333,4 +383,4 @@ private Collection getGrantedPermissions() { return grantedPermissions; } } -} +} \ No newline at end of file diff --git a/dexter/src/main/java/com/karumi/dexter/listener/DexterError.java b/dexter/src/main/java/com/karumi/dexter/listener/DexterError.java new file mode 100644 index 00000000..07cb5ffd --- /dev/null +++ b/dexter/src/main/java/com/karumi/dexter/listener/DexterError.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2016 Karumi. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.karumi.dexter.listener; + +public enum DexterError { + /** + * Error code used when the user tries to request permissions before all previous + * requests has finished. + */ + REQUEST_ONGOING, + + /** + * Error code used when Dexter is called with no permissions. + */ + NO_PERMISSIONS_REQUESTED +} diff --git a/dexter/src/main/java/com/karumi/dexter/listener/EmptyPermissionRequestErrorListener.java b/dexter/src/main/java/com/karumi/dexter/listener/EmptyPermissionRequestErrorListener.java new file mode 100644 index 00000000..2fd0037d --- /dev/null +++ b/dexter/src/main/java/com/karumi/dexter/listener/EmptyPermissionRequestErrorListener.java @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2016 Karumi. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.karumi.dexter.listener; + +public class EmptyPermissionRequestErrorListener implements PermissionRequestErrorListener { + @Override public void onError(DexterError error) { + } +} diff --git a/sample/src/main/java/com/karumi/dexter/sample/SampleApplication.java b/dexter/src/main/java/com/karumi/dexter/listener/PermissionRequestErrorListener.java similarity index 63% rename from sample/src/main/java/com/karumi/dexter/sample/SampleApplication.java rename to dexter/src/main/java/com/karumi/dexter/listener/PermissionRequestErrorListener.java index e3c60eca..5d74f7e5 100644 --- a/sample/src/main/java/com/karumi/dexter/sample/SampleApplication.java +++ b/dexter/src/main/java/com/karumi/dexter/listener/PermissionRequestErrorListener.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2015 Karumi. + * Copyright (C) 2016 Karumi. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,18 +14,14 @@ * limitations under the License. */ -package com.karumi.dexter.sample; - -import android.app.Application; -import com.karumi.dexter.Dexter; +package com.karumi.dexter.listener; /** - * Sample application that initializes the Dexter library. + * Listener to be notified when a Dexter error occurs. */ -public class SampleApplication extends Application { - - @Override public void onCreate() { - super.onCreate(); - Dexter.initialize(this); - } +public interface PermissionRequestErrorListener { + /** + * Method called whenever Dexter fails. + */ + void onError(DexterError error); } diff --git a/gradle.properties b/gradle.properties index 4e504eec..314e1994 100644 --- a/gradle.properties +++ b/gradle.properties @@ -2,7 +2,7 @@ POM_NAME=Dexter POM_ARTIFACT_ID=dexter POM_PACKAGING=aar -VERSION_NAME=2.3.2-SNAPSHOT +VERSION_NAME=3.0.0-SNAPSHOT VERSION_CODE=203001 GROUP=com.karumi diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 4df27c26..1f5dd79c 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Thu Nov 12 18:14:23 CET 2015 +#Fri Dec 23 13:30:58 CET 2016 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.4-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip diff --git a/sample/src/main/AndroidManifest.xml b/sample/src/main/AndroidManifest.xml index f39b4725..1b9fbd8a 100644 --- a/sample/src/main/AndroidManifest.xml +++ b/sample/src/main/AndroidManifest.xml @@ -23,7 +23,6 @@