Visitar URL original
feat(messaging,android): improve how messaging is determining permiss… · firebase/flutterfire@1c89b68 · GitHub
Skip to content

Commit 1c89b68

Browse files
LyokoneSelaseKay
andauthored
feat(messaging,android): improve how messaging is determining permission on Android (#18101)
* feat(messaging,android): improve how messaging is determining permission on Android * format * format * fix(messaging,android): harden notification permission status resolution Use PackageManager permission flags instead of SharedPreferences so soft deny, permanent deny, and never-asked are distinguished correctly even when another plugin requested POST_NOTIFICATIONS. Add AuthorizationStatus.deniedPermanently and reset Android 13+ e2e permission state between tests. * fix(messaging): use local functions in Android permission e2e helpers Getters are not valid inside the group() callback body, which broke dart analyze in the tests package. * fix(messaging,android): resolve permission status without hidden platform APIs PackageManager.getPermissionFlags() and the FLAG_PERMISSION_USER_SET / FLAG_PERMISSION_USER_FIXED constants are @hide/@Systemapi and are not part of the public SDK at any API level, so the plugin failed to compile. Resolve the status with public API instead: shouldShowRequestPermissionRationale() plus a SharedPreferences record of whether the prompt was ever shown. The flag is written when the permission is requested rather than only on denial, so a grant followed by a revoke from system settings still reports deniedPermanently. Also clear that record from the e2e permission reset helper so "never asked" is reproducible across tests, and fix a google-java-format violation. * fix(messaging,test): grant Android permission e2e helpers without instrumentation flutter test launches the example app without UiAutomation, so CI could not reset or grant POST_NOTIFICATIONS. Fall back to the host adb server from the emulator and fix google-java-format / ktfmt. --------- Co-authored-by: Jude Kwashie <judekwashie70@gmail.com>
1 parent 6d48e7c commit 1c89b68

9 files changed

Lines changed: 767 additions & 11 deletions

File tree

‎packages/firebase_messaging/firebase_messaging/android/src/main/java/io/flutter/plugins/firebase/messaging/FlutterFirebaseMessagingPlugin.java‎

Lines changed: 84 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@
88

99
import android.Manifest;
1010
import android.app.Activity;
11+
import android.content.Context;
1112
import android.content.Intent;
13+
import android.content.SharedPreferences;
1214
import android.content.pm.PackageManager;
1315
import android.os.Build;
1416
import android.os.Bundle;
@@ -17,6 +19,7 @@
1719
import androidx.annotation.Nullable;
1820
import androidx.annotation.RequiresApi;
1921
import androidx.annotation.VisibleForTesting;
22+
import androidx.core.app.ActivityCompat;
2023
import androidx.core.app.NotificationManagerCompat;
2124
import androidx.lifecycle.LiveData;
2225
import androidx.lifecycle.Observer;
@@ -373,6 +376,16 @@ private Task<Map<String, Object>> getInitialMessage() {
373376
return taskCompletionSource.getTask();
374377
}
375378

379+
// Wire values for Dart AuthorizationStatus (see convertToAuthorizationStatus).
380+
private static final int AUTH_NOT_DETERMINED = -1;
381+
private static final int AUTH_DENIED = 0;
382+
private static final int AUTH_AUTHORIZED = 1;
383+
private static final int AUTH_DENIED_PERMANENTLY = 3;
384+
385+
private static final String PERMISSIONS_PREFERENCES_FILE =
386+
"io.flutter.plugins.firebase.messaging.permissions";
387+
private static final String KEY_PERMISSION_REQUESTED = "notification_permission_requested";
388+
376389
@RequiresApi(api = 33)
377390
private Task<Map<String, Integer>> requestPermissions() {
378391
TaskCompletionSource<Map<String, Integer>> taskCompletionSource = new TaskCompletionSource<>();
@@ -385,14 +398,24 @@ private Task<Map<String, Integer>> requestPermissions() {
385398
if (!areNotificationsEnabled) {
386399
permissionManager.requestPermissions(
387400
mainActivity,
388-
(notificationsEnabled) -> {
389-
permissions.put("authorizationStatus", notificationsEnabled);
401+
(grantResult) -> {
402+
// Record that the OS has now asked the user, so a later
403+
// getNotificationSettings() can tell a permanent denial apart from
404+
// "never asked".
405+
markNotificationPermissionRequested();
406+
// After the OS dialog, resolve the full status (soft vs permanent deny)
407+
// instead of returning only the raw grant result.
408+
int status =
409+
grantResult == 1
410+
? AUTH_AUTHORIZED
411+
: resolveNotificationAuthorizationStatus();
412+
permissions.put("authorizationStatus", status);
390413
taskCompletionSource.setResult(permissions);
391414
},
392415
(String errorDescription) ->
393416
taskCompletionSource.setException(new Exception(errorDescription)));
394417
} else {
395-
permissions.put("authorizationStatus", 1);
418+
permissions.put("authorizationStatus", AUTH_AUTHORIZED);
396419
taskCompletionSource.setResult(permissions);
397420
}
398421

@@ -411,21 +434,74 @@ private Boolean checkPermissions() {
411434
== PackageManager.PERMISSION_GRANTED;
412435
}
413436

437+
private SharedPreferences getPermissionsPreferences() {
438+
return ContextHolder.getApplicationContext()
439+
.getSharedPreferences(PERMISSIONS_PREFERENCES_FILE, Context.MODE_PRIVATE);
440+
}
441+
442+
private void markNotificationPermissionRequested() {
443+
getPermissionsPreferences().edit().putBoolean(KEY_PERMISSION_REQUESTED, true).apply();
444+
}
445+
446+
/**
447+
* Resolves Android 13+ notification permission into Dart authorization codes.
448+
*
449+
* <p>A denied {@code POST_NOTIFICATIONS} is ambiguous: Android reports the same state for "never
450+
* asked" and "permanently denied", and it exposes no public API for reading the underlying
451+
* permission flags. {@link ActivityCompat#shouldShowRequestPermissionRationale} combined with a
452+
* SharedPreferences record of whether we ever showed the prompt breaks the tie. This mirrors how
453+
* {@code permission_handler} solves the same problem.
454+
*
455+
* <ul>
456+
* <li>granted → authorized (1)
457+
* <li>never asked → notDetermined (-1)
458+
* <li>soft deny (rationale can be shown) → denied (0)
459+
* <li>asked before, no rationale → deniedPermanently (3)
460+
* </ul>
461+
*
462+
* <p>Known limitation: if another plugin requested {@code POST_NOTIFICATIONS} and the user denied
463+
* it permanently, we have no record of the prompt and report notDetermined. Calling {@link
464+
* #requestPermissions()} in that state is a no-op that resolves to the correct status.
465+
*/
466+
@RequiresApi(api = 33)
467+
private int resolveNotificationAuthorizationStatus() {
468+
if (checkPermissions()) {
469+
return AUTH_AUTHORIZED;
470+
}
471+
472+
if (mainActivity != null
473+
&& ActivityCompat.shouldShowRequestPermissionRationale(
474+
mainActivity, Manifest.permission.POST_NOTIFICATIONS)) {
475+
// Denied at least once, but the OS will still show another prompt.
476+
return AUTH_DENIED;
477+
}
478+
479+
if (!getPermissionsPreferences().getBoolean(KEY_PERMISSION_REQUESTED, false)) {
480+
return AUTH_NOT_DETERMINED;
481+
}
482+
483+
// Asked before and no rationale is available. Without an Activity we cannot call
484+
// shouldShowRequestPermissionRationale at all, so report the softer status and let
485+
// callers retry once an Activity is attached.
486+
return mainActivity == null ? AUTH_DENIED : AUTH_DENIED_PERMANENTLY;
487+
}
488+
414489
private Task<Map<String, Integer>> getPermissions() {
415490
TaskCompletionSource<Map<String, Integer>> taskCompletionSource = new TaskCompletionSource<>();
416491

417492
cachedThreadPool.execute(
418493
() -> {
419494
try {
420495
final Map<String, Integer> permissions = new HashMap<>();
421-
final boolean areNotificationsEnabled;
422496
if (Build.VERSION.SDK_INT >= 33) {
423-
areNotificationsEnabled = checkPermissions();
497+
permissions.put("authorizationStatus", resolveNotificationAuthorizationStatus());
424498
} else {
425-
areNotificationsEnabled =
426-
NotificationManagerCompat.from(mainActivity).areNotificationsEnabled();
499+
final boolean areNotificationsEnabled =
500+
NotificationManagerCompat.from(ContextHolder.getApplicationContext())
501+
.areNotificationsEnabled();
502+
permissions.put(
503+
"authorizationStatus", areNotificationsEnabled ? AUTH_AUTHORIZED : AUTH_DENIED);
427504
}
428-
permissions.put("authorizationStatus", areNotificationsEnabled ? 1 : 0);
429505
taskCompletionSource.setResult(permissions);
430506
} catch (Exception e) {
431507
taskCompletionSource.setException(e);

0 commit comments

Comments
 (0)