Skip to content
This repository was archived by the owner on Oct 3, 2024. It is now read-only.

Conversation

SUPERCILEX
Copy link
Contributor

#62

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.");
Copy link
Contributor Author

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.

Copy link
Contributor

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Woops, thanks!

Copy link
Contributor

@samtstern samtstern left a 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;
Copy link
Contributor

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.");
Copy link
Contributor

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,
Copy link
Contributor

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?

Copy link
Contributor Author

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?

Copy link
Contributor

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.

Copy link
Contributor Author

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)

Copy link
Contributor

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>
Signed-off-by: Alex Saveau <saveau.alexandre@gmail.com>
Copy link
Contributor Author

@SUPERCILEX SUPERCILEX left a 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)
Copy link
Contributor Author

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);
}
Copy link
Contributor Author

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)
Copy link
Contributor Author

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)
Copy link
Contributor Author

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;
}
Copy link
Contributor Author

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.

Copy link
Contributor

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.

Copy link
Contributor

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.

Copy link
Contributor Author

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
}

Copy link
Contributor

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)
Copy link
Contributor Author

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) {
Copy link
Contributor Author

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
Copy link
Contributor

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(...)}"

Copy link
Contributor Author

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,
Copy link
Contributor

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.

Copy link
Contributor Author

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.

@samtstern
Copy link
Contributor

@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>
@SUPERCILEX
Copy link
Contributor Author

SUPERCILEX commented Jan 21, 2017

@samtstern I took advantage of us guarding our permission requests with #hasPermissions(...) to lower the api level needed to call #requestPermissions(android.app.Fragment, ...) in ddd3f74. Is that appropriate?

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
Copy link
Contributor

@SUPERCILEX this all LGTM, thank you! Going to squash and merge it.

@samtstern samtstern merged commit 209a517 into googlesamples:master Jan 23, 2017
@SUPERCILEX
Copy link
Contributor Author

Awesome, glad to have been able to clean it up!

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants