Skip to content
This repository has been archived by the owner on Sep 3, 2023. It is now read-only.

Commit

Permalink
0.2.0
Browse files Browse the repository at this point in the history
  • Loading branch information
afollestad committed Dec 29, 2015
1 parent 746dfa8 commit 633a4df
Show file tree
Hide file tree
Showing 6 changed files with 267 additions and 30 deletions.
120 changes: 112 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,20 @@
Assent is designed to make Marshmallow's runtime permissions easier to use. Have the flexibility of
request permissions and receiving results through callback interfaces.

# Table of Contents

1. [Gradle Dependency](https://github.com/afollestad/assent#gradle-dependency)
1. [Repository](https://github.com/afollestad/assent#repository)
2. [Dependency](https://github.com/afollestad/assent#dependency)
2. [Basics](https://github.com/afollestad/assent#basics)
3. [Without AssentActivity](https://github.com/afollestad/assent#without-assentactivity)
4. [Using PermissionResultSet](https://github.com/afollestad/assent#using-permissionresultset)
5. [Fragments](https://github.com/afollestad/assent#fragments)
6. [Duplicate and Simultaneous Requests](https://github.com/afollestad/assent#duplicate-and-simultaneous-requests)
1. [Duplicate Request Handling](https://github.com/afollestad/assent#duplicate-request-handling)
2. [Simultaneous Request Handling](https://github.com/afollestad/assent#simultaneous-request-handling)
7. [AfterPermissionResult Annotation](https://github.com/afollestad/assent#afterpermissionresult-annotation)

---

# Gradle Dependency
Expand All @@ -11,7 +25,7 @@ Assent is designed to make Marshmallow's runtime permissions easier to use. Have
[![Build Status](https://travis-ci.org/afollestad/assent.svg)](https://travis-ci.org/afollestad/assent)
[![License](https://img.shields.io/badge/license-Apache%202-4EB1BA.svg?style=flat-square)](https://www.apache.org/licenses/LICENSE-2.0.html)

### Repository
#### Repository

Add this in your root `build.gradle` file (**not** your module `build.gradle` file):

Expand All @@ -24,14 +38,14 @@ allprojects {
}
```

### Dependency
#### Dependency

Add this to your module's `build.gradle` file:

```gradle
dependencies {
...
compile('com.github.afollestad:assent:0.1.3') {
compile('com.github.afollestad:assent:0.2.0') {
transitive = true
}
}
Expand All @@ -41,6 +55,9 @@ dependencies {

# Basics

**Note**: *you need to have needed permissions in your AndroidManifest.xml too, otherwise Android will
always deny them, even on Marshmallow.*

The first way to use this library is to have Activities which request permissions extend `AssentActivity`.
AssentActivity will handle some dirty work internally, so all that you have to do is use the `requestPermissions` method:

Expand All @@ -52,6 +69,7 @@ public class MainActivity extends AssentActivity {
super.onCreate(savedInstanceState);

if (!Assent.isPermissionGranted(Assent.WRITE_EXTERNAL_STORAGE)) {
// The if statement checks if the permission has already been granted before
Assent.requestPermissions(new AssentCallback() {
@Override
public void onPermissionResult(PermissionResultSet result) {
Expand Down Expand Up @@ -90,6 +108,7 @@ public class MainActivity extends AppCompatActivity {
Assent.setActivity(this, this);

if (!Assent.isPermissionGranted(Assent.WRITE_EXTERNAL_STORAGE)) {
// The if statement checks if the permission has already been granted before
Assent.requestPermissions(new AssentCallback() {
@Override
public void onPermissionResult(PermissionResultSet result) {
Expand Down Expand Up @@ -117,7 +136,7 @@ public class MainActivity extends AppCompatActivity {
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
// Lets Assent handle permission results and contact your callbacks
// Lets Assent handle permission results, and contact your callbacks
Assent.handleResult(permissions, grantResults);
}
}
Expand Down Expand Up @@ -172,9 +191,11 @@ public class MainFragment extends Fragment {

---

# Duplicate Request Handling
# Duplicate and Simultaneous Requests

If you were to do this:
#### Duplicate Request Handling

If you were to do this...

```java
Assent.requestPermissions(new AssentCallback() {
Expand All @@ -192,17 +213,100 @@ Assent.requestPermissions(new AssentCallback() {
}, 69, Assent.WRITE_EXTERNAL_STORAGE);
```

The permission would only be requested once, and both callbacks would be called at the same time.
...the permission would only be requested once, and both callbacks would be called at the same time.

An example situation where this would be useful: if you use tabs in your app, and multiple Fragments
which are created at the same request the same permission, the permission dialog would only be shown once
and both Fragments would be updated with the result.

#### Simultaneous Request Handling

If you were to do this...

```java
Assent.requestPermissions(new AssentCallback() {
@Override
public void onPermissionResult(PermissionResultSet result) {
// Permission granted or denied
}
}, 34, Assent.WRITE_EXTERNAL_STORAGE);

Assent.requestPermissions(new AssentCallback() {
@Override
public void onPermissionResult(PermissionResultSet result) {
// Permission granted or denied
}
}, 69, Assent.ACCESS_FINE_LOCATION);
```

...Assent would wait until the first permission request is done before executing the second request.

This is important, because if you were you request different permissions at the same time without Assent,
the first permission request would be cancelled and denied and the second one would be shown immediately.

---

# AfterPermissionResult Annotation

As a convenience, you can use the `AfterPermissionResult` annotation to have Assent invoke a method in
any class when a specific set of permissions is granted or denied.

```java
public class MainActivity extends AssentActivity {

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

// Request WRITE_EXTERNAL_STORAGE and ACCESS_FINE_LOCATION permissions, with the current class as the target
Assent.requestPermissions(MainActivity.this, 69,
Assent.WRITE_EXTERNAL_STORAGE, Assent.ACCESS_FINE_LOCATION);
}

@AfterPermissionResult(permissions = { Assent.WRITE_EXTERNAL_STORAGE, Assent.ACCESS_FINE_LOCATION })
public void onPermissionResult(PermissionResultSet result) {
// Use PermissionResultSet
}
}
```

Behind the scenes, Assent is actually using a callback. When the callback is received, it finds the
first `AfterPermissionResult` annotated method in the target class object (with a matching permission
set) and invokes it.

The target class couldn't be any object. It even works like this:

```java
public class OtherClass {

@AfterPermissionResult(permissions = {Assent.WRITE_EXTERNAL_STORAGE, Assent.ACCESS_FINE_LOCATION})
public void onPermissionResult(PermissionResultSet result) {
// Use permission result
}
}

public class MainActivity extends AssentActivity {

private OtherClass mOther;

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

mOther = new OtherClass();

// Request WRITE_EXTERNAL_STORAGE and ACCESS_FINE_LOCATION permissions, with mOther as the target
Assent.requestPermissions(mOther, 69,
Assent.WRITE_EXTERNAL_STORAGE, Assent.ACCESS_FINE_LOCATION);
}
}
```

---

# [LICENSE](/LICENSE.md)

###### Copyright 2016 Aidan Follestad
#### Copyright 2016 Aidan Follestad

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
Expand Down
2 changes: 1 addition & 1 deletion library/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ android {
minSdkVersion 14
targetSdkVersion 23
versionCode 4
versionName "0.1.3"
versionName "0.2.0"
}
buildTypes {
release {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.afollestad.assent;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* @author Aidan Follestad (afollestad)
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)

public @interface AfterPermissionResult {

String[] permissions();
}
97 changes: 77 additions & 20 deletions library/src/main/java/com/afollestad/assent/Assent.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,9 @@
import android.support.annotation.IntRange;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;

import java.util.ArrayList;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;

Expand All @@ -19,7 +18,7 @@ public class Assent extends AssentBase {

private static Assent mAssent;
private Activity mContext;
private final Map<String, ArrayList<AssentCallback>> mRequestQueue;
private final HashMap<String, CallbackStack> mRequestQueue;

private Assent() {
mRequestQueue = new HashMap<>();
Expand Down Expand Up @@ -51,44 +50,102 @@ private static Activity invalidateActivity() {
return context;
}

private static Map<String, ArrayList<AssentCallback>> requestQueue() {
private static HashMap<String, CallbackStack> requestQueue() {
return instance().mRequestQueue;
}

public static void handleResult(@NonNull String[] permissions, @NonNull int[] grantResults) {
synchronized (requestQueue()) {
final String cacheKey = getCacheKey(permissions);
final ArrayList<AssentCallback> callbacks = requestQueue().get(cacheKey);
if (callbacks == null) return;
final CallbackStack callbackStack = requestQueue().get(cacheKey);
if (callbackStack == null) return;
final PermissionResultSet result = PermissionResultSet.create(permissions, grantResults);
for (AssentCallback cb : callbacks)
cb.onPermissionResult(result);
callbackStack.sendResult(result);
requestQueue().remove(cacheKey);
LOG("Result for %s handled to %d callbacks; new queue size: %d",
cacheKey, callbacks.size(), requestQueue().size());
LOG("Result for %s handled to %d callbacks.", cacheKey, callbackStack.size());

if (requestQueue().size() > 0) {
for (Map.Entry<String, CallbackStack> entry : requestQueue().entrySet()) {
if (entry.getValue().isExecuted()) continue;
entry.getValue().execute(invalidateActivity());
}
}
}
}

public static boolean isPermissionGranted(@NonNull String permission) {
return ContextCompat.checkSelfPermission(invalidateActivity(), permission) == PackageManager.PERMISSION_GRANTED;
}

private static boolean arraysEqual(@Nullable String[] left, @Nullable String[] right) {
if (left == null || right == null) {
return (left == null) == (right == null);
} else if (left.length != right.length) {
return false;
}
for (int i = 0; i < left.length; i++) {
if (!left[i].equals(right[i]))
return false;
}
return true;
}

public static void requestPermissions(final @NonNull Object target,
@IntRange(from = 1, to = 99) int requestCode,
@NonNull String... permissions) {
final Method[] methods = target.getClass().getDeclaredMethods();
Method annotatedMethod = null;
for (Method m : methods) {
AfterPermissionResult annotation = m.getAnnotation(AfterPermissionResult.class);
if (annotation == null) continue;
else if (!arraysEqual(permissions, annotation.permissions())) continue;
annotatedMethod = m;
}
if (annotatedMethod == null)
throw new IllegalStateException(String.format("No AfterPermissionResult annotated methods found in %s with a matching permission set.", target.getClass().getName()));
else if (annotatedMethod.getParameterTypes().length != 1)
throw new IllegalStateException(String.format("Method %s should only have 1 parameter of type PermissionResultSet.", annotatedMethod.getName()));
else if (annotatedMethod.getParameterTypes()[0] != PermissionResultSet.class)
throw new IllegalStateException(String.format("Method %s should only have 1 parameter of type PermissionResultSet.", annotatedMethod.getName()));

final Method fMethod = annotatedMethod;
fMethod.setAccessible(true);
requestPermissions(new AssentCallback() {
@Override
public void onPermissionResult(PermissionResultSet result) {
LOG("Invoking %s for permission result.", fMethod.getName());
try {
fMethod.invoke(target, result);
} catch (Exception e) {
throw new IllegalStateException(String.format("Failed to invoke %s: %s", fMethod.getName(), e.getMessage()), e);
}
}
}, requestCode, permissions);
}

public static void requestPermissions(@NonNull AssentCallback callback,
@IntRange(from = 1, to = Integer.MAX_VALUE) int requestCode,
@NonNull String... permissions) {
synchronized (requestQueue()) {
final String cacheKey = getCacheKey(permissions);
ArrayList<AssentCallback> stack = requestQueue().get(cacheKey);
if (stack != null) {
stack.add(callback);
LOG("Added callback to stack for %s; new stack size: %d", cacheKey, stack.size());
CallbackStack callbackStack = requestQueue().get(cacheKey);
if (callbackStack != null) {
callbackStack.setRequestCode(requestCode);
callbackStack.push(callback);
LOG("Pushed callback to EXISTING stack %s... stack size: %d", cacheKey, callbackStack.size());
} else {
stack = new ArrayList<>(2);
stack.add(callback);
requestQueue().put(cacheKey, stack);
LOG("Added NEW callback stack for %s", cacheKey);
ActivityCompat.requestPermissions(invalidateActivity(), permissions, requestCode);
callbackStack = new CallbackStack(requestCode, permissions);
callbackStack.push(callback);
final boolean startNow = requestQueue().size() == 0;
requestQueue().put(cacheKey, callbackStack);
LOG("Added NEW callback stack %s", cacheKey);
if (startNow) {
LOG("Executing new permission stack now.");
callbackStack.execute(invalidateActivity());
} else {
LOG("New permission stack will be executed later.");
}
}
}
}
}
}
Loading

0 comments on commit 633a4df

Please sign in to comment.