Android 9.0 NotificationManager.notify() throwing

2020-06-04 05:12发布

I have been unable to reproduce this problem myself, but so far 5 users have reported it. I did recently publish an app update that changed the target SDK from 27 to 28 which I sure plays a part in this. All 5 users are running some flavor Android 9 on some kind of Pixel device. As am I.

The app responds to an alert situation by calling setting up a notification and calling NotificationManager.notify(). This notification references a notification channel that tries to play an audio file located on external storage. My app does include the READ_EXTERNAL_STORAGE permission in the manifest. But since it is not, itself, accessing anything in external storage, it has not asked the user to grant it that permission.

When I do this on my Pixel, this works just fine. But 5 users reported it throwing an exception like

java.lang.RuntimeException: Unable to start activity ComponentInfo{net.anei.cadpage/net.anei.cadpage.CadPageActivity}: java.lang.SecurityException: UID 10132 does not have permission to content://media/external/audio/media/145 [user 0]
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2914)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3049)
at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:78)
at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:108)
at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:68)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1809)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loop(Looper.java:193)
at android.app.ActivityThread.main(ActivityThread.java:6680)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:493)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:858)
Caused by: java.lang.SecurityException: UID 10132 does not have permission to content://media/external/audio/media/145 [user 0]
at android.os.Parcel.createException(Parcel.java:1950)
at android.os.Parcel.readException(Parcel.java:1918)
at android.os.Parcel.readException(Parcel.java:1868)
at android.app.INotificationManager$Stub$Proxy.enqueueNotificationWithTag(INotificationManager.java:1559)
at android.app.NotificationManager.notifyAsUser(NotificationManager.java:405)
at android.app.NotificationManager.notify(NotificationManager.java:370)
at android.app.NotificationManager.notify(NotificationManager.java:346)
at net.anei.cadpage.ManageNotification.show(ManageNotification.java:186)
at net.anei.cadpage.ReminderReceiver.scheduleNotification(ReminderReceiver.java:46)
at net.anei.cadpage.ManageNotification.show(ManageNotification.java:161)
at net.anei.cadpage.CadPageActivity.startup(CadPageActivity.java:211)
at net.anei.cadpage.CadPageActivity.onCreate(CadPageActivity.java:93)
at android.app.Activity.performCreate(Activity.java:7144)
at android.app.Activity.performCreate(Activity.java:7135)
at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1271)
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2894)
... 11 more
Caused by: android.os.RemoteException: Remote stack trace:
at com.android.server.am.ActivityManagerService.checkGrantUriPermissionLocked(ActivityManagerService.java:9752)
at com.android.server.am.ActivityManagerService.checkGrantUriPermission(ActivityManagerService.java:9769)
at com.android.server.notification.NotificationRecord.visitGrantableUri(NotificationRecord.java:1096)
at com.android.server.notification.NotificationRecord.calculateGrantableUris(NotificationRecord.java:1072)
at com.android.server.notification.NotificationRecord.<init>(NotificationRecord.java:201)

I have told all 4 users to manually grant the "Storage" permission, and AFAIK that resolves the issue. But why should this be necessary. My did not access external storage itself, or set up the channel configuration to require it. If READ_EXTERNAL_STORAGE permission is needed, the Notification Manager should be managing that.

User reporting problem were running the following: google/taimen/taimen:9/PQ1A.190105.004/5148680:user/release-keys google/crosshatch/crosshatch:9/PQ1A.190105.004/5148680:user/release-keys google/marlin/marlin:9/PQ1A.181205.002.A1/5129870:user/release-keys google/sailfish/sailfish:9/PQ1A.181205.002.A1/5129870:user/release-keys google/walleye/walleye:9/PQ1A.181205.002/5086253:user/release-keys

I am running google/taimen/taimen:9/PQ1A.181205.002/5086253:user/release-keys which seems to be behind everyone else, updating to google/taimen/taimen:9/PQ1A.190105.004/5148680:user/release-keys doesn't change anything. Still works fine on my device.

Here is all the code with some hints as to which branches are taken. The stack trace is pretty clear that the exception was thrown in the notify() call. And that the abort was thrown because the app did not have security access to the audio file specified by the channel.

// Build and launch the notification
Notification n = buildNotification(context, message);

NotificationManager myNM = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
assert myNM != null;

// Seems this is needed for the number value to take effect on the Notification
activeNotice = true;
myNM.cancel(NOTIFICATION_ALERT);
myNM.notify(NOTIFICATION_ALERT, n);

........

private static Notification buildNotification(Context context, SmsMmsMessage message) {

/*
 * Ok, let's create our Notification object and set up all its parameters.
 */
NotificationCompat.Builder nbuild = new NotificationCompat.Builder(context, ALERT_CHANNEL_ID);

// Set auto-cancel flag
nbuild.setAutoCancel(true);

// Set display icon
nbuild.setSmallIcon(R.drawable.ic_stat_notify);

// From Oreo on, these are set at the notification channel level
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {  // False

  // Maximum priority
  nbuild.setPriority(NotificationCompat.PRIORITY_MAX);

  // Message category
  nbuild.setCategory(NotificationCompat.CATEGORY_CALL);

  // Set public visibility
  nbuild.setVisibility(NotificationCompat.VISIBILITY_PUBLIC);

  // Set up LED pattern and color
  if (ManagePreferences.flashLED()) {
    /*
     * Set up LED blinking pattern
     */
    int col = getLEDColor(context);
    int[] led_pattern = getLEDPattern(context);
    nbuild.setLights(col, led_pattern[0], led_pattern[1]);
  }

  /*
   * Set up vibrate pattern
   */
  // If vibrate is ON, or if phone is set to vibrate
  AudioManager AM = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
  assert AM != null;
  if ((ManagePreferences.vibrate() || AudioManager.RINGER_MODE_VIBRATE == AM.getRingerMode())) {
    long[] vibrate_pattern = getVibratePattern(context);
    if (vibrate_pattern != null) {
      nbuild.setVibrate(vibrate_pattern);
    } else {
      nbuild.setDefaults(Notification.DEFAULT_VIBRATE);
    }
  }
}

if ( ManagePreferences.notifyEnabled()) {  // false

  // Are we doing are own alert sound?
  if (ManagePreferences.notifyOverride()) {

    // Save previous volume and set volume to max
    overrideVolumeControl(context);

    // Start Media Player
    startMediaPlayer(context, 0);
  } else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O){
    Uri alarmSoundURI = Uri.parse(ManagePreferences.notifySound());
    nbuild.setSound(alarmSoundURI);
  }
}

String call = message.getTitle();
nbuild.setContentTitle(context.getString(R.string.cadpage_alert));
nbuild.setContentText(call);
nbuild.setStyle(new NotificationCompat.InboxStyle().addLine(call).addLine(message.getAddress()));
nbuild.setWhen(message.getIncidentDate().getTime());

// The default intent when the notification is clicked (Inbox)
Intent smsIntent = CadPageActivity.getLaunchIntent(context, true);
PendingIntent notifIntent = PendingIntent.getActivity(context, 0, smsIntent, 0);
nbuild.setContentIntent(notifIntent);

// Set intent to execute if the "clear all" notifications button is pressed -
// basically stop any future reminders.
Intent deleteIntent = new Intent(new Intent(context, ReminderReceiver.class));
deleteIntent.setAction(Intent.ACTION_DELETE);
PendingIntent pendingDeleteIntent = PendingIntent.getBroadcast(context, 0, deleteIntent, 0);
nbuild.setDeleteIntent(pendingDeleteIntent);

return nbuild.build();
}

Latest news. Last night I published an update backing the target SDK back to 27 from 28. Overnight 2 more users reported this particular crash on Pixel phones running Android 9. Both were running the version targeting SDK 28. One got back to me and confirmed that the problem disappeared when they installed the SDK 27 version of the app. This confirms that this is an issue with apps targeting SDK 28, probably related to the change disallowing apps from using world access file system permissions to defeat the application sandbox restrictions.

It is still a mystery why it affects some users but not others. Specifically me. When I get some time, I am going to make another attempt to reproduce the problem on my phone. Two theories are 1) It only hits people who have never granted READ_EXTERNAL_STORAGE permission. Mine had originally been granted that permission and I revoked it when attempting to reproduce the problem. 2) It only happens when the notification channel using the external audio file was originally set up by the app. That would have been true for most users, but in my case the sound file was set up manually.

2条回答
可以哭但决不认输i
2楼-- · 2020-06-04 06:10

I had this issue. Turns out that the creation of the notification channel was at fault.

Wrong way:

val notifictionChannel = NotificationChannel(...)
notificationChannel.setSound(
    RingtoneManager.getActualDefaultRingtoneUri(context, RingtoneManager.TYPE_NOTIFICATION),
    AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_NOTIFICATION).build()
)

notificationManager.createNotificationChannel(notificationChannel)

Right way:

...
notificationChannel.setSound(
    Settings.System.DEFAULT_NOTIFICATION_URI,
    AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_NOTIFICATION).build()
)
...
查看更多
爱情/是我丢掉的垃圾
3楼-- · 2020-06-04 06:10

Not so much a solution as a long complicated workaround.

First, I catch the SecurityException that is thrown by the notification and set a shared preference flag

try {
  myNM.notify(NOTIFICATION_ALERT, n);
} catch (SecurityException ex) {
  Log.e(ex);
  ManagePreferences.setNotifyAbort(true);
  return;
}

When the app starts up it checks this flag and it is set, prompts the user to grant the READ_EXTERNAL_PERMISSION. Not including the code because it is part of a complicated system that ties permissions to different preference settings, only allowing certain settings if a required permission is granted and changing it if the permission is not granted.

That helps, but we still means the user will not get notified the first time an alert needs to be generated. To address that, we add something to are startup initialization that checks to see if there might be a problem, and if there is, generates a regular notification and immediately cancels it.

if (audioAlert && Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
  if (! ManagePreferences.notifyCheckAbort() &&
      ! PermissionManager.isGranted(context, PermissionManager.READ_EXTERNAL_STORAGE)) {
    Log.v("Checking Notification Security");
    ManagePreferences.setNotifyCheckAbort(true);
    ManageNotification.show(context, null, false, false);
    NotificationManager myNM = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
    assert myNM != null;
    myNM.cancel(NOTIFICATION_ALERT);
  }
}

Getting closer. But we still miss an alert notification if it happens after the user upgrades to Android 9 but before they open the app. To address that, I wrote a broadcast receiver that listens for android.intent.action.MY_PACKAGE_REPLACED and android.intent.action.BOOT_COMPLETED which gets called every time my app is upgraded, or the Android system is upgraded. This receiver does not do anything special. But the fact that it exists means that my app gets started up and goes through the initialization logic. Which detects that the user needs the READ_EXTERNAL_STORAGE permission and prompts them for it.

查看更多
登录 后发表回答