Skip to content

Commit

Permalink
feat(firebase_messaging, android)!: android 13 notifications permissi…
Browse files Browse the repository at this point in the history
…on request (#9348)

`android.permission.POST_NOTIFICATIONS` has been added to the FirebaseMessaging "AndroidManifest.xml" for requesting notification permission.
  • Loading branch information
russellwheatley committed Aug 24, 2022
1 parent bb9b3b2 commit 43b3b06
Show file tree
Hide file tree
Showing 9 changed files with 153 additions and 19 deletions.
Expand Up @@ -35,7 +35,7 @@ def getRootProjectExtOrCoreProperty(name, firebaseCoreProject) {
}

android {
compileSdkVersion 31
compileSdkVersion 33
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
Expand All @@ -51,8 +51,8 @@ android {
api firebaseCoreProject
implementation platform("com.google.firebase:firebase-bom:${getRootProjectExtOrCoreProperty("FirebaseSDKVersion", firebaseCoreProject)}")
implementation 'com.google.firebase:firebase-messaging'
implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.0.0'
implementation 'androidx.annotation:annotation:1.1.0'
implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.1.0'
implementation 'androidx.annotation:annotation:1.4.0'
}
}

Expand Down
Expand Up @@ -3,6 +3,8 @@
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<!-- Permissions options for the `notification` group -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<application>
<service
android:name=".FlutterFirebaseMessagingBackgroundService"
Expand Down
Expand Up @@ -6,13 +6,17 @@

import static io.flutter.plugins.firebase.core.FlutterFirebasePluginRegistry.registerPlugin;

import android.Manifest;
import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.core.app.NotificationManagerCompat;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import com.google.android.gms.tasks.Task;
Expand Down Expand Up @@ -48,12 +52,13 @@ public class FlutterFirebaseMessagingPlugin extends BroadcastReceiver
private MethodChannel channel;
private Activity mainActivity;
private RemoteMessage initialMessage;
FlutterFirebasePermissionManager permissionManager;

private void initInstance(BinaryMessenger messenger) {
String channelName = "plugins.flutter.io/firebase_messaging";
channel = new MethodChannel(messenger, channelName);
channel.setMethodCallHandler(this);

permissionManager = new FlutterFirebasePermissionManager();
// Register broadcast receiver
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(FlutterFirebaseMessagingUtils.ACTION_TOKEN);
Expand All @@ -72,14 +77,13 @@ public void onAttachedToEngine(FlutterPluginBinding binding) {

@Override
public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
if (binding.getApplicationContext() != null) {
LocalBroadcastManager.getInstance(binding.getApplicationContext()).unregisterReceiver(this);
}
LocalBroadcastManager.getInstance(binding.getApplicationContext()).unregisterReceiver(this);
}

@Override
public void onAttachedToActivity(ActivityPluginBinding binding) {
binding.addOnNewIntentListener(this);
binding.addRequestPermissionsResultListener(permissionManager);
this.mainActivity = binding.getActivity();
if (mainActivity.getIntent() != null && mainActivity.getIntent().getExtras() != null) {
if ((mainActivity.getIntent().getFlags() & Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY)
Expand Down Expand Up @@ -247,7 +251,7 @@ private Task<Map<String, Object>> setAutoInitEnabled(Map<String, Object> argumen
return taskCompletionSource.getTask();
}

private Task<Map<String, Object>> getInitialMessage(Map<String, Object> arguments) {
private Task<Map<String, Object>> getInitialMessage() {
TaskCompletionSource<Map<String, Object>> taskCompletionSource = new TaskCompletionSource<>();

cachedThreadPool.execute(
Expand Down Expand Up @@ -311,16 +315,60 @@ private Task<Map<String, Object>> getInitialMessage(Map<String, Object> argument
return taskCompletionSource.getTask();
}

@RequiresApi(api = 33)
private Task<Map<String, Integer>> requestPermissions() {
TaskCompletionSource<Map<String, Integer>> taskCompletionSource = new TaskCompletionSource<>();
cachedThreadPool.execute(
() -> {
final Map<String, Integer> permissions = new HashMap<>();
try {
final boolean areNotificationsEnabled = checkPermissions();

if (!areNotificationsEnabled) {
permissionManager.requestPermissions(
mainActivity,
(notificationsEnabled) -> {
permissions.put("authorizationStatus", notificationsEnabled);
taskCompletionSource.setResult(permissions);
},
(String errorDescription) -> {
taskCompletionSource.setException(new Exception(errorDescription));
});
} else {
permissions.put("authorizationStatus", 1);
taskCompletionSource.setResult(permissions);
}

} catch (Exception e) {
taskCompletionSource.setException(e);
}
});

return taskCompletionSource.getTask();
}

@RequiresApi(api = 33)
private Boolean checkPermissions() {
return ContextHolder.getApplicationContext()
.checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS)
== PackageManager.PERMISSION_GRANTED;
}

private Task<Map<String, Integer>> getPermissions() {
TaskCompletionSource<Map<String, Integer>> taskCompletionSource = new TaskCompletionSource<>();

cachedThreadPool.execute(
() -> {
try {
final Map<String, Integer> permissions = new HashMap<>();
final boolean areNotificationsEnabled =
NotificationManagerCompat.from(mainActivity).areNotificationsEnabled();
permissions.put("authorizationStatus", areNotificationsEnabled ? 1 : 0);
if (Build.VERSION.SDK_INT >= 33) {
final boolean areNotificationsEnabled = checkPermissions();
permissions.put("authorizationStatus", areNotificationsEnabled ? 1 : 0);
} else {
final boolean areNotificationsEnabled =
NotificationManagerCompat.from(mainActivity).areNotificationsEnabled();
permissions.put("authorizationStatus", areNotificationsEnabled ? 1 : 0);
}
taskCompletionSource.setResult(permissions);
} catch (Exception e) {
taskCompletionSource.setException(e);
Expand Down Expand Up @@ -379,7 +427,7 @@ public void onMethodCall(final MethodCall call, @NonNull final Result result) {
methodCallTask = Tasks.forResult(null);
break;
case "Messaging#getInitialMessage":
methodCallTask = getInitialMessage(call.arguments());
methodCallTask = getInitialMessage();
break;
case "Messaging#deleteToken":
methodCallTask = deleteToken();
Expand All @@ -400,6 +448,14 @@ public void onMethodCall(final MethodCall call, @NonNull final Result result) {
methodCallTask = setAutoInitEnabled(call.arguments());
break;
case "Messaging#requestPermission":
if (Build.VERSION.SDK_INT >= 33) {
// Android version >= Android 13 requires user input if notification permission not set/granted
methodCallTask = requestPermissions();
} else {
// Android version < Android 13 doesn't require asking for runtime permissions.
methodCallTask = getPermissions();
}
break;
case "Messaging#getNotificationSettings":
methodCallTask = getPermissions();
break;
Expand Down
Expand Up @@ -16,6 +16,11 @@
import java.util.Objects;
import java.util.Set;

@FunctionalInterface
interface ErrorCallback {
void onError(String errorDescription);
}

class FlutterFirebaseMessagingUtils {
static final String IS_AUTO_INIT_ENABLED = "isAutoInitEnabled";
static final String SHARED_PREFERENCES_KEY = "io.flutter.firebase.messaging.callback";
Expand Down
@@ -0,0 +1,64 @@
package io.flutter.plugins.firebase.messaging;

import android.Manifest;
import android.app.Activity;
import android.content.pm.PackageManager;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.core.app.ActivityCompat;
import io.flutter.plugin.common.PluginRegistry;
import java.util.ArrayList;

class FlutterFirebasePermissionManager implements PluginRegistry.RequestPermissionsResultListener {

private final int permissionCode = 24;
@Nullable private RequestPermissionsSuccessCallback successCallback;
private boolean requestInProgress = false;

@FunctionalInterface
interface RequestPermissionsSuccessCallback {
void onSuccess(int results);
}

@Override
public boolean onRequestPermissionsResult(
int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
requestInProgress = false;
if (requestCode != permissionCode) {
return false;
}

int grantResult = grantResults[0];
assert this.successCallback != null;
this.successCallback.onSuccess(grantResult == PackageManager.PERMISSION_GRANTED ? 1 : 0);
return true;
}

@RequiresApi(api = 33)
public void requestPermissions(
Activity activity,
RequestPermissionsSuccessCallback successCallback,
ErrorCallback errorCallback) {
if (requestInProgress) {
errorCallback.onError(
"A request for permissions is already running, please wait for it to finish before doing another request.");
return;
}

if (activity == null) {
errorCallback.onError("Unable to detect current Android Activity.");
return;
}

this.successCallback = successCallback;
final ArrayList<String> permissions = new ArrayList<String>();
permissions.add(Manifest.permission.POST_NOTIFICATIONS);
final String[] requestNotificationPermission = permissions.toArray(new String[0]);

if (!requestInProgress) {
ActivityCompat.requestPermissions(activity, requestNotificationPermission, permissionCode);
requestInProgress = true;
}
}
}
Expand Up @@ -25,7 +25,7 @@ apply plugin: 'com.android.application'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"

android {
compileSdkVersion 31
compileSdkVersion 33

lintOptions {
disable 'InvalidPackage'
Expand All @@ -35,7 +35,7 @@ android {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "io.flutter.plugins.firebase.messaging.example"
minSdkVersion 19
targetSdkVersion 31
targetSdkVersion 33
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
}
Expand Down
@@ -1,6 +1,5 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="io.flutter.plugins.firebase.messaging.example">

<application
android:label="firebase_messaging_example"
android:icon="@mipmap/ic_launcher">
Expand All @@ -10,6 +9,7 @@
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:exported="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
Expand Down
4 changes: 2 additions & 2 deletions tests/android/app/build.gradle
Expand Up @@ -29,7 +29,7 @@ apply plugin: 'kotlin-android'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"

android {
compileSdkVersion flutter.compileSdkVersion
compileSdkVersion 33

compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
Expand All @@ -48,7 +48,7 @@ android {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "io.flutter.plugins.firebase.tests"
minSdkVersion 19
targetSdkVersion flutter.targetSdkVersion
targetSdkVersion 33
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
}
Expand Down
@@ -1,14 +1,21 @@
// Generated file.
//
// If you wish to remove Flutter's multidex support, delete this entire file.
//
// Modifications to this file should be done in a copy under a different name
// as this file may be regenerated.

package io.flutter.app;

import android.app.Application;
import android.content.Context;
import androidx.annotation.CallSuper;
import androidx.multidex.MultiDex;

/** Extension of {@link io.flutter.app.FlutterApplication}, adding multidex support. */
public class FlutterMultiDexApplication extends FlutterApplication {
/**
* Extension of {@link android.app.Application}, adding multidex support.
*/
public class FlutterMultiDexApplication extends Application {
@Override
@CallSuper
protected void attachBaseContext(Context base) {
Expand Down

0 comments on commit 43b3b06

Please sign in to comment.