Skip to content

Commit

Permalink
(android) Handle checking/requesting POST_NOTIFICATIONS runtime permi…
Browse files Browse the repository at this point in the history
…ssion on Android 13+.

Resolves dpa99c#777
  • Loading branch information
dpa99c committed Nov 23, 2022
1 parent 3785354 commit 3c184ee
Show file tree
Hide file tree
Showing 2 changed files with 148 additions and 22 deletions.
34 changes: 24 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1668,16 +1668,30 @@ Data message flow:
b. If the data message contains the [data message notification keys](#data-message-notifications), the plugin will display a system notification for the data message while in the background.

### grantPermission
Grant permission to receive push notifications (will trigger prompt) and return `hasPermission: true`.
iOS only (Android will always return true).
Grant run-time permission to receive push notifications (will trigger user permission prompt).
iOS & Android 13+ (Android <= 12 will always return true).

On Android, the `POST_NOTIFICATIONS` permission must be added to the `AndroidManifest.xml` file by inserting the following into your `config.xml` file:

```xml
<config-file target="AndroidManifest.xml" parent="/*">
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
</config-file>
```

**Parameters**:
- {function} success - callback function which will be passed the {boolean} permission result as an argument
- {function} error - callback function which will be passed a {string} error message as an argument
- {boolean} requestWithProvidesAppNotificationSettings - boolean which indicates if app provides AppNotificationSettingsButton (**iOS12+ only**)
- {boolean} requestWithProvidesAppNotificationSettings - (**iOS12+ only**) indicates if app provides AppNotificationSettingsButton

```javascript
FirebasePlugin.grantPermission(function(hasPermission){
console.log("Notifications permission was " + (hasPermission ? "granted" : "denied"));
});
```

### grantCriticalPermission
Grant critical permission to receive critical push notifications (will trigger additional prompt) and return `hasPermission: true`.
Grant critical permission to receive critical push notifications (will trigger additional prompt).
iOS 12.0+ only (Android will always return true).

**Parameters**:
Expand All @@ -1687,15 +1701,15 @@ iOS 12.0+ only (Android will always return true).
**Critical push notifications require a special entitlement that needs to be issued by Apple.**

```javascript
FirebasePlugin.grantPermission(function(hasPermission){
console.log("Permission was " + (hasPermission ? "granted" : "denied"));
FirebasePlugin.grantCriticalPermission(function(hasPermission){
console.log("Critical notifications permission was " + (hasPermission ? "granted" : "denied"));
});

```

### hasPermission
Check permission to receive push notifications and return the result to a callback function as boolean.
On iOS, returns true is runtime permission for remote notifications is granted and enabled in Settings.
On Android, returns true if remote notifications are enabled.
On iOS, returns true if runtime permission for remote notifications is granted and enabled in Settings.
On Android, returns true if global remote notifications are enabled in the device settings and (on Android 13+) runtime permission for remote notifications is granted.

**Parameters**:
- {function} success - callback function which will be passed the {boolean} permission result as an argument
Expand All @@ -1719,7 +1733,7 @@ iOS 12.0+ only (Android will always return true).

```javascript
FirebasePlugin.hasCriticalPermission(function(hasPermission){
console.log("Permission to send critical push notificaitons is " + (hasPermission ? "granted" : "denied"));
console.log("Permission to send critical push notifications is " + (hasPermission ? "granted" : "denied"));
});
```

Expand Down
136 changes: 124 additions & 12 deletions src/android/FirebasePlugin.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.app.ActivityCompat;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;

Expand Down Expand Up @@ -81,11 +82,13 @@
import org.apache.cordova.CallbackContext;
import org.apache.cordova.CordovaInterface;
import org.apache.cordova.CordovaPlugin;
import org.apache.cordova.CordovaWebView;
import org.apache.cordova.PluginResult;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.HashMap;
Expand Down Expand Up @@ -137,13 +140,16 @@ public class FirebasePlugin extends CordovaPlugin {
private static final String CRASHLYTICS_COLLECTION_ENABLED = "firebase_crashlytics_collection_enabled";
private static final String ANALYTICS_COLLECTION_ENABLED = "firebase_analytics_collection_enabled";
private static final String PERFORMANCE_COLLECTION_ENABLED = "firebase_performance_collection_enabled";
protected static final String POST_NOTIFICATIONS = "POST_NOTIFICATIONS";
protected static final int POST_NOTIFICATIONS_PERMISSION_REQUEST_ID = 1;

private static boolean inBackground = true;
private static ArrayList<Bundle> notificationStack = null;
private static CallbackContext notificationCallbackContext;
private static CallbackContext tokenRefreshCallbackContext;
private static CallbackContext activityResultCallbackContext;
private static CallbackContext authResultCallbackContext;
private static CallbackContext postNotificationPermissionRequestCallbackContext;

private static NotificationChannel defaultNotificationChannel = null;
public static String defaultChannelId = null;
Expand Down Expand Up @@ -228,7 +234,9 @@ public boolean execute(String action, JSONArray args, CallbackContext callbackCo
this.getToken(args, callbackContext);
} else if (action.equals("hasPermission")) {
this.hasPermission(callbackContext);
}else if (action.equals("subscribe")) {
} else if (action.equals("grantPermission")) {
this.grantPermission(callbackContext);
} else if (action.equals("subscribe")) {
this.subscribe(callbackContext, args.getString(0));
} else if (action.equals("unsubscribe")) {
this.unsubscribe(callbackContext, args.getString(0));
Expand Down Expand Up @@ -385,8 +393,7 @@ public boolean execute(String action, JSONArray args, CallbackContext callbackCo
this.removeFirestoreListener(args, callbackContext);
} else if (action.equals("functionsHttpsCallable")) {
this.functionsHttpsCallable(args, callbackContext);
} else if (action.equals("grantPermission")
|| action.equals("grantCriticalPermission")
} else if (action.equals("grantCriticalPermission")
|| action.equals("hasCriticalPermission")
|| action.equals("setBadgeNumber")
|| action.equals("getBadgeNumber")
Expand Down Expand Up @@ -627,13 +634,40 @@ public void onComplete(@NonNull Task<String> task) {
}

private void hasPermission(final CallbackContext callbackContext) {
if(cordovaActivity == null) return;
cordova.getThreadPool().execute(new Runnable() {
public void run() {
try {
NotificationManagerCompat notificationManagerCompat = NotificationManagerCompat.from(cordovaActivity);
boolean areNotificationsEnabled = notificationManagerCompat.areNotificationsEnabled();
callbackContext.success(conformBooleanForPluginResult(areNotificationsEnabled));

boolean hasRuntimePermission = true;
if(Build.VERSION.SDK_INT >= 33){ // Android 13+
hasRuntimePermission = hasRuntimePermission(POST_NOTIFICATIONS);
}

callbackContext.success(conformBooleanForPluginResult(areNotificationsEnabled && hasRuntimePermission));
} catch (Exception e) {
handleExceptionWithContext(e, callbackContext);
}
}
});
}

private void grantPermission(final CallbackContext callbackContext) {
CordovaPlugin plugin = this;
cordova.getThreadPool().execute(new Runnable() {
public void run() {
try {
if(Build.VERSION.SDK_INT >= 33){ // Android 13+
boolean hasRuntimePermission = hasRuntimePermission(POST_NOTIFICATIONS);
if(!hasRuntimePermission){
String[] permissions = new String[]{qualifyPermission(POST_NOTIFICATIONS)};
postNotificationPermissionRequestCallbackContext = callbackContext;
requestPermissions(plugin, POST_NOTIFICATIONS_PERMISSION_REQUEST_ID, permissions);
sendEmptyPluginResultAndKeepCallback(callbackContext);
}
}

} catch (Exception e) {
handleExceptionWithContext(e, callbackContext);
}
Expand Down Expand Up @@ -2238,11 +2272,11 @@ public void run() {
boolean timestamp = args.getBoolean(2);

Map<String, Object> docData = jsonStringToMap(jsonDoc);

if(timestamp){
docData.put("created", new Timestamp(new Date()));
docData.put("lastUpdate", new Timestamp(new Date()));
}
docData.put("lastUpdate", new Timestamp(new Date()));
}

firestore.collection(collection)
.add(docData)
Expand Down Expand Up @@ -2277,8 +2311,8 @@ public void run() {
Map<String, Object> docData = jsonStringToMap(jsonDoc);

if(timestamp){
docData.put("lastUpdate", new Timestamp(new Date()));
}
docData.put("lastUpdate", new Timestamp(new Date()));
}

firestore.collection(collection).document(documentId)
.set(docData)
Expand Down Expand Up @@ -2313,8 +2347,8 @@ public void run() {
Map<String, Object> docData = jsonStringToMap(jsonDoc);

if(timestamp){
docData.put("lastUpdate", new Timestamp(new Date()));
}
docData.put("lastUpdate", new Timestamp(new Date()));
}

firestore.collection(collection).document(documentId)
.update(docData)
Expand Down Expand Up @@ -2891,6 +2925,12 @@ protected void sendPluginResultAndKeepCallback(JSONObject result, CallbackContex
sendPluginResultAndKeepCallback(pluginresult, callbackContext);
}

protected void sendEmptyPluginResultAndKeepCallback(CallbackContext callbackContext){
PluginResult pluginresult = new PluginResult(PluginResult.Status.NO_RESULT);
pluginresult.setKeepCallback(true);
callbackContext.sendPluginResult(pluginresult);
}

protected void sendPluginResultAndKeepCallback(PluginResult pluginresult, CallbackContext callbackContext){
pluginresult.setKeepCallback(true);
callbackContext.sendPluginResult(pluginresult);
Expand Down Expand Up @@ -3181,4 +3221,76 @@ private void logExceptionToCrashlytics(Exception exception){
private int conformBooleanForPluginResult(boolean result){
return result ? 1 : 0;
}

protected String qualifyPermission(String permission){
if(permission.startsWith("android.permission.")){
return permission;
}else{
return "android.permission."+permission;
}
}

protected boolean hasRuntimePermission(String permission) throws Exception{
boolean hasRuntimePermission = true;
String qualifiedPermission = qualifyPermission(permission);
Method method = null;
try {
method = cordova.getClass().getMethod("hasPermission", qualifiedPermission.getClass());
Boolean bool = (Boolean) method.invoke(cordova, qualifiedPermission);
hasRuntimePermission = bool.booleanValue();
} catch (NoSuchMethodException e) {
Log.w(TAG, "Cordova v" + CordovaWebView.CORDOVA_VERSION + " does not support runtime permissions so defaulting to GRANTED for " + permission);
}
return hasRuntimePermission;
}

protected void requestPermissions(CordovaPlugin plugin, int requestCode, String [] permissions) throws Exception{
try {
java.lang.reflect.Method method = cordova.getClass().getMethod("requestPermissions", org.apache.cordova.CordovaPlugin.class ,int.class, java.lang.String[].class);
method.invoke(cordova, plugin, requestCode, permissions);
} catch (NoSuchMethodException e) {
throw new Exception("requestPermissions() method not found in CordovaInterface implementation of Cordova v" + CordovaWebView.CORDOVA_VERSION);
}
}

/************
* Overrides
***********/

/**
* then updates the list of status based on the grantResults before passing the result back via the context.
*
* @param requestCode - ID that was used when requesting permissions
* @param permissions - list of permissions that were requested
* @param grantResults - list of flags indicating if above permissions were granted or denied
*/
public void onRequestPermissionResult(int requestCode, String[] permissions, int[] grantResults) throws JSONException {
String sRequestId = String.valueOf(requestCode);
Log.v(TAG, "Received result for permissions request id=" + sRequestId);
try {
if(postNotificationPermissionRequestCallbackContext == null){
Log.e(TAG, "No callback context found for permissions request id=" + sRequestId);
return;
}

boolean postNotificationPermissionGranted = false;
for (int i = 0, len = permissions.length; i < len; i++) {
String androidPermission = permissions[i];

if(androidPermission.equals(qualifyPermission(POST_NOTIFICATIONS))){
postNotificationPermissionGranted = grantResults[i] == PackageManager.PERMISSION_GRANTED;
}
}

postNotificationPermissionRequestCallbackContext.success(postNotificationPermissionGranted ? 1 : 0);
postNotificationPermissionRequestCallbackContext = null;

}catch(Exception e ) {
if(postNotificationPermissionRequestCallbackContext != null){
handleExceptionWithContext(e, postNotificationPermissionRequestCallbackContext);
}else{
handleExceptionWithoutContext(e);
}
}
}
}

0 comments on commit 3c184ee

Please sign in to comment.