Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(firebase_messaging, android)!: android 13 notifications permission request #9348

Merged
merged 11 commits into from
Aug 24, 2022
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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"/>
Comment on lines +6 to +7
Copy link
Contributor

Choose a reason for hiding this comment

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

We should probably mention this in the changelog for this PR

Copy link
Member Author

Choose a reason for hiding this comment

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

We can manually edit this when doing the next release I guess 🤔

Copy link
Contributor

Choose a reason for hiding this comment

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

I think you can change the squash message when merging from GitHub directly

<application>
<service
android:name=".FlutterFirebaseMessagingBackgroundService"
Expand Down
Original file line number Diff line number Diff line change
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,64 @@ 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();
}

private Boolean checkPermissions() {
if (Build.VERSION.SDK_INT >= 33) {
return ContextHolder.getApplicationContext()
.checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS)
== PackageManager.PERMISSION_GRANTED;
}

throw new RuntimeException(
russellwheatley marked this conversation as resolved.
Show resolved Hide resolved
"Cannot check permissions using POST_NOTIFICATIONS on anything less than API level 33");
}

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 +431,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 +452,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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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