-
Notifications
You must be signed in to change notification settings - Fork 1.5k
Short circuit perms request at root method #76
Conversation
Signed-off-by: Alex Saveau <saveau.alexandre@gmail.com> (cherry picked from commit e6e7c57) Signed-off-by: Alex Saveau <saveau.alexandre@gmail.com>
Signed-off-by: Alex Saveau <saveau.alexandre@gmail.com>
@@ -352,7 +352,7 @@ private static Activity getActivity(@NonNull Object object) { | |||
} else if (object instanceof android.app.Fragment) { | |||
return ((android.app.Fragment) object).getActivity(); | |||
} else { | |||
return null; | |||
throw new IllegalArgumentException("Context cannot be null."); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I changed it to be IllegalArgumentException
since it is now called immediately using the method's argument instead of further down the stack.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wouldn't this message be more accurate as "Object must be an Activity or a Fragment"? This else
does not imply nullity.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Woops, thanks!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM in general, a few comments.
@StringRes int positiveButton, @StringRes int negativeButton, | ||
final int requestCode, @NonNull final String... perms) { | ||
if (hasPermissions(getActivity(object))) return false; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: prefer using brackets even for a one-line if statement.
@@ -352,7 +352,7 @@ private static Activity getActivity(@NonNull Object object) { | |||
} else if (object instanceof android.app.Fragment) { | |||
return ((android.app.Fragment) object).getActivity(); | |||
} else { | |||
return null; | |||
throw new IllegalArgumentException("Context cannot be null."); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wouldn't this message be more accurate as "Object must be an Activity or a Fragment"? This else
does not imply nullity.
*/ | ||
@SuppressLint("NewApi") | ||
public static void requestPermissions(@NonNull final Object object, @NonNull String rationale, | ||
public static boolean requestPermissions(@NonNull final Object object, @NonNull String rationale, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm unsure about changing this method to be non-void. What would you expect an app developer to do in the case where this returns false?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I meant to include checking for permissions in the if statement (I fixed that in my latest commit). This leaves us with two questions: should we be checking for permissions as well and then does it make sense to return the boolean if we are also checking for permissions?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Although I see both sides, I like the explicitness of the current model where you say if(!hasPermissions()) { requestPermissions() } in your app code since then you know exactly when you're about to launch UI. I think it also matches the simplest mental model.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@samtstern, so is issue #62 no longer relevant? (hasPermissions()
already checks for the API level)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The essence of #62 was tracking all the technical debt that has built up over time. This library made some bad initial decisions (using Object
all over the place rather than overloads) and with the complexity of the support library there are just a ton of branches and checks that I'd like to refactor to make the code overall smaller and more readable.
Signed-off-by: Alex Saveau <saveau.alexandre@gmail.com>
Signed-off-by: Alex Saveau <saveau.alexandre@gmail.com>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@samtstern Whew! I get what you mean by technical debt, EasyPermissions.java
was a jumbled mess! The new EasyPermissions.java
class has more lines of code than the old one, but that's only because it has to triplicate methods and documentation. Otherwise, things are so easy to understand now! If you should show a rationale, then show it; else, just request the permissions with all of that in one method related to the activity or fragment being passed. It's beautiful, enjoy! 😄
PS: The diff is garbage so I would recommend just reading through the new class. (It shouldn't be too difficult now!)
@@ -76,7 +78,7 @@ public void onClick(DialogInterface dialog, int which) { | |||
mAlertDialog = dialogBuilder.create(); | |||
} | |||
|
|||
@TargetApi(11) | |||
@RequiresApi(api = Build.VERSION_CODES.HONEYCOMB) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TargetApi
is technically deprecated.
From the RequiresApi
docs:
This is similar in purpose to the older @TargetApi annotation, but more clearly expresses that this is a requirement on the caller, rather than being used to "suppress" warnings within the method that exceed the minSdkVersion.
} else { // mHost instance of FragmentActivity | ||
ActivityCompat.requestPermissions( | ||
(FragmentActivity) mHost, mConfig.permissions, mConfig.requestCode); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
mHost = dialogFragment.getParentFragment() != null ? dialogFragment.getParentFragment() : dialogFragment.getActivity();
@NonNull String deniedPermission) { | ||
return !shouldShowRequestPermissionRationale(object, deniedPermission); | ||
} | ||
@RequiresApi(api = Build.VERSION_CODES.M) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not using Object allows us to do this! (Yay to lint and IDE smarts!)
* PermissionCallbacks#onPermissionsDenied(int, List)} | ||
* @return {@code true} if at least one permission in the list was permanently denied. | ||
*/ | ||
@RequiresApi(api = Build.VERSION_CODES.M) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Again, yay!
@NonNull String... perms) { | ||
if (hasPermissions(activity, perms)) { | ||
notifyAlreadyHasPermissions(activity, requestCode, perms); | ||
return; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the new model you'll see everywhere: if we already have the permissions or api level< M
, then we're done.
We also hope that the api consumer has their implements PermissionCallbacks
or annotations in the same object they pass into requestPermissions
. If they do, we notify them which provides nice api consistency.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
New model is great! Will try to make sense of the diff / new file and comment.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok so I think the use of notifyAlreadyHasPermissions
could be a breaking change for some developers.
Let's say you have this code
private foid foo() {
if (!EasyPermissions.hasPermissions(this, "BAR")) {
EasyPermissions.requestPermissions(this, "BAR");
return;
}
// Do some important stuff
}
@Override
public void onPermissionsGranted(List<String> perms) {
if (perms.contains("BAR")) {
foo();
}
}
The problem is that by notifying them of already having the permission, this could result in foo()
being called twice. For some operations, this could be very bad (anything that's not idempotent).
So I think instead of notifying listeners when they already have permissions, we may have to just Log a warning and then drop the whole thing in order to preserve current behavior.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@samtstern I got confused by this too at first, but think of it this way: anytime you call requestPermissions
, you can expect to get a callback. In your example the flow is still the same: no permissions -> request permissions -> foo gets called because user accepted permissions -> has permissions -> don't call request permissions -> continue as usual
. The only case I can think of that results in the method being recursively called infinity is if you do this which is wrong anyway:
@AfterPermissionsGranted(foo)
public void bar() {
EasyPermissions.requestPermissions(this, ...);
// Boom!
}
The usage I can see for getting a callback when you already have the permissions would be:
@Override
public void onCreate(instance) {
EasyPermissions.requestPermissions(this, ...);
}
@AfterPermissionsGranted(foo)
public void bar() {
// Happily do stuff
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's a good point. I am convinced :-)
} | ||
|
||
new AlertDialog.Builder(activity) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was used for api level < something lower than M
which means it's useless because we already have the permissions if we are lower than M.
} | ||
|
||
private static boolean shouldShowRequestPermissionRationale(@NonNull Object object, | ||
@NonNull String perm) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the only one I left with Object
because it helps reduce triplicating logic. (I didn't know triplicating
was a word, lol 😄)
* request code to track this request, must be < 256. | ||
* @param perms | ||
* a set of permissions to be requested. | ||
* @param activity Activity requesting permissions. Should implement {@link |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since we're now going to have a huge number of methods and overloads (which is good!) this file is going to be overloaded with duplicate docs.
In general, for overloads we should avoid duplicating the whole docstring and just say something short and then use {@see} to point to the method we delegate to which contains the full docstring.
"Requests permissions with default values for rationale dialog buttons.
{@see #requestPermissions(...)}"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done in b4f4ad9.
RationaleDialogFragment fragment = RationaleDialogFragment | ||
.newInstance(positiveButton, negativeButton, rationale, requestCode, perms); | ||
fragment.show(fragmentManager, DIALOG_TAG); | ||
public static void requestPermissions(@NonNull Fragment fragment, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Although it's verbose, it may be clearer to always use fully qualified names for fragments (so android.support.v4.app.Fragment
here. I think people come to the code with different assumptions about what an unqualified reference to Fragment
means.
Even if we don't do that in code, we should definitely mention it in the JavaDoc.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree with you that this can be confusing so I'm open to adding the full qualifier, but for the moment I just made everything a link in 941c08f. I really hope that's good enough because it kills me to add the full qualifier.
@SUPERCILEX thanks for taking this on! Sorely needed. |
Signed-off-by: Alex Saveau <saveau.alexandre@gmail.com>
Signed-off-by: Alex Saveau <saveau.alexandre@gmail.com>
Signed-off-by: Alex Saveau <saveau.alexandre@gmail.com>
@samtstern I took advantage of us guarding our permission requests with |
Signed-off-by: Alex Saveau <saveau.alexandre@gmail.com>
Signed-off-by: Alex Saveau <saveau.alexandre@gmail.com>
@SUPERCILEX this all LGTM, thank you! Going to squash and merge it. |
Awesome, glad to have been able to clean it up! |
#62