Android 9.0 NotificationManager.notify() throwing java.lang.SecurityException
Asked Answered
S

2

11

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.

Squamous answered 10/1, 2019 at 1:18 Comment(7)
Can we see your code? If this is a customizable image, these users likely are choosing from some special location that does need the permission.Promptbook
The ringtone picekr didn't grant read rights to the uri?Mucor
what does ManagePreferences.notifySound() return?Magdamagdaia
ManagePreferense.notifySound() return a URI selected by a ringtone picker. What it returns is not relevant here because the result is only used when the SDK build level is less than 27 and we know the actual SDK level was 28. We do know what value was returned, and it happens to be the same ringtone that was configured in the notification channel (content://media/external/audio/media/145). This is not a coincidence. The app used that configured value to set up the default notification channel when the user first upgraded to Android 8.Squamous
I have the same issue. I get "SecurityException: does not have permission to content://media/external/audio/media/3532". To reproduce it, i set a Hangouts Message ringtone to my notification channel and don't have a Storage permission. When i am added Storage permission the issue is disappear. Also i don't have this issue with other ringtones.Franklyn
@Squamous have you found valid solution for this?Nace
For us, this is happening exclusively on Android 9.0+ (API level 28+), and only when targeting API level 28+, but across all manufacturers. Also, in our case, it does not matter whether the storage permission has been granted or not. Anyway, it appears that it happens because NotificationChannel#setSound has initially been called with a content:// URI on devices upgraded from API level 27 and below. Calling NotificationCompat.Builder#setSound later or changing the sound in the channel manually (via the UI) does not cause problems. See also: https://mcmap.net/q/1159832/-caused-by-java-lang-securityexception-uid-10243-does-not-have-permission-to-content-media-external-audio-media-5927-user-0Peeper
S
1

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.

Squamous answered 13/3, 2020 at 4:29 Comment(0)
B
0

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()
)
...
Bewail answered 13/5, 2019 at 12:15 Comment(3)
But the "right way" would only set default notification, instead of custom one which is read from content://media/external/...Bioclimatology
I'm not sure I'm following you. What I had wanted was to create the notification channel so that it's pre-configured to make sound, namely the one that the user has configured as the default sound for notifications. If the user-configured default notification sound happens to be a custom one, with content://media/external/... URI, this works when done the above "right way" and crashes with the stacktrace in the question when done the above "wrong way".Bewail
If the user leaves the channel settings as they are then, and changes the (default) notification sound, the sound for this channel's notifications will change too, as it should.Bewail

© 2022 - 2024 — McMap. All rights reserved.