Android 9.0 NotificationManager.notify() 抛出 java.lang.SecurityException。

11

我自己无法重现这个问题,但到目前为止已经有5个用户报告了它。 我最近发布了一个应用程序更新,将目标SDK从27更改为28,这肯定是其中的一部分原因。 所有5个用户都在某种Pixel设备上运行某种Android 9。

应用程序通过调用设置通知并调用NotificationManager.notify()来响应警报情况。 此通知引用了一个尝试在外部存储上播放音频文件的通知通道。 我的应用程序在清单中包含READ_EXTERNAL_STORAGE权限。 但由于它本身没有访问任何外部存储中的内容,因此它没有要求用户授予该权限。

当我在我的Pixel上进行此操作时,它运行得很好。 但是5个用户报告它抛出异常,例如

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)

我已经告诉所有4个用户手动授权“存储”权限,据我所知,这解决了问题。但为什么需要这样做呢?我的应用程序本身没有访问外部存储器,也没有设置渠道配置要求它。如果需要READ_EXTERNAL_STORAGE权限,则通知管理器应该处理。

报告问题的用户正在运行以下版本: 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

我运行的是 google/taimen/taimen:9/PQ1A.181205.002/5086253:user/release-keys, 似乎比其他人落后,更新到 google/taimen/taimen:9/PQ1A.190105.004/5148680:user/release-keys 不会改变任何东西。我的设备仍然可以正常工作。

以下是所有代码及其执行分支的一些提示。堆栈跟踪很清楚,异常是在notify()调用中抛出的,而中止是因为应用程序没有安全访问通道指定的音频文件。

// 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();
}

最新消息。昨晚我发布了一个更新,将目标SDK从28回退到了27。在过夜期间,又有两个用户报告了这个问题:他们使用运行Android 9的Pixel手机,并且正在运行版本针对SDK 28的应用程序。其中一个用户回复我确认,在安装了应用程序SDK 27版本后,问题消失了。这证实了这是一个与应用程序针对SDK 28相关的问题,可能与禁止应用程序使用全局访问文件系统权限来打破应用程序沙盒限制的更改有关。

为什么它会影响某些用户而不影响其他用户仍然是一个谜。特别是对于我。当我有时间时,我会再次尝试在我的手机上重现这个问题。有两个理论: 1)只有从未授予READ_EXTERNAL_STORAGE权限的人才会受到影响。我的设备原本被授予该权限,但在尝试重现问题时我已经撤销了授权。 2)只有当应用程序设置通知渠道并使用外部音频文件时才会发生此问题。对于大多数用户来说,这是正确的,但在我的情况下,声音文件是手动设置的。


你能让我们看看你的代码吗?如果这是一个可定制的图像,那么这些用户可能正在从某个需要权限的特殊位置进行选择。 - TheWanderer
1
铃声选择器没有授予URI的读取权限? - Eugen Pechanec
2
ManagePreferense.notifySound() 返回由铃声选择器选定的URI。这里返回的结果并不重要,因为当SDK构建级别小于27时,该结果仅用于特定目的,而我们知道实际的SDK级别是28。我们确实知道返回的值是什么,并且恰好与通知渠道中配置的铃声相同(content://media/external/audio/media/145)。这不是巧合。当用户首次升级到Android 8时,应用程序使用该配置值来设置默认通知渠道。 - kencorbin
1
我遇到了相同的问题。我收到“SecurityException: does not have permission to content://media/external/audio/media/3532”错误。为了再现它,我将Hangouts消息铃声设置为我的通知渠道,并没有获得存储权限。当我添加存储权限时,问题就消失了。另外,我在使用其他铃声时并没有遇到这个问题。 - Valery Miller
1
对我们来说,这仅在Android 9.0+(API级别28+)上发生,并且仅在针对API级别28+的情况下跨所有制造商。此外,在我们的情况下,无论是否已授予存储权限都没有关系。不管怎样,似乎是因为在从API级别27及以下升级的设备上最初使用了content:// URI调用了NotificationChannel#setSound。稍后调用NotificationCompat.Builder#setSound或手动更改通道中的声音(通过UI)不会导致问题。另请参见:https://dev59.com/Jbbna4cB1Zd3GeqPUAXQ - caw
显示剩余2条评论
2个回答

1
不是一个解决方案,而是一个复杂的绕过方法。
首先,我捕获了通知抛出的SecurityException并设置了共享偏好标志。
try {
  myNM.notify(NOTIFICATION_ALERT, n);
} catch (SecurityException ex) {
  Log.e(ex);
  ManagePreferences.setNotifyAbort(true);
  return;
}

当应用程序启动时,它会检查此标志并设置它,提示用户授予READ_EXTERNAL_PERMISSION权限。不包括代码,因为它是一个将权限绑定到不同首选项设置的复杂系统的一部分,只有在授予所需权限时才允许特定设置,并在未授予权限时更改它。
这有所帮助,但我们仍然意味着用户第一次需要生成警报时不会收到通知。为了解决这个问题,我们在启动初始化中添加了一些内容,检查是否可能存在问题,如果存在,则生成常规通知并立即取消它。
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);
  }
}

已经接近成功了。但是如果在用户升级到Android 9之后打开应用程序之前发生警报通知,我们仍然会错过它。为了解决这个问题,我编写了一个广播接收器,监听android.intent.action.MY_PACKAGE_REPLACED和android.intent.action.BOOT_COMPLETED,每次我的应用程序升级或Android系统升级时都会调用该接收器。此接收器没有执行任何特殊操作。但是它存在的事实意味着我的应用程序将启动并通过初始化逻辑。这会检测到用户需要READ_EXTERNAL_STORAGE权限,并提示用户进行授权。


0

我遇到了这个问题。结果发现通知渠道的创建有问题。

错误的方法:

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

notificationManager.createNotificationChannel(notificationChannel)

正确的方式:

...
notificationChannel.setSound(
    Settings.System.DEFAULT_NOTIFICATION_URI,
    AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_NOTIFICATION).build()
)
...

1
但是“正确的方法”只会设置默认通知,而不是从content://media/external/...读取的自定义通知。 - Tapemaster
我不确定我是否理解你的意思。我想要创建通知渠道,使其预先配置为发出声音,即用户配置为通知默认声音的那个声音。如果用户配置的默认通知声音恰好是自定义的,具有 content://media/external/... URI,则通过上述“正确方法”完成时可以正常工作,但通过上述“错误方法”完成时会导致堆栈跟踪中提到的崩溃。 - Jule
如果用户将频道设置保持不变,只更改(默认)通知声音,则该频道的通知声音也会随之更改,这正是应该的。 - Jule

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接