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

Posted

技术标签:

【中文标题】Android 9.0 NotificationManager.notify() 抛出 java.lang.SecurityException【英文标题】:Android 9.0 NotificationManager.notify() throwing java.lang.SecurityException 【发布时间】:2019-06-04 20:30:07 【问题描述】:

我自己无法重现此问题,但到目前为止已有 5 位用户报告了此问题。我最近确实发布了一个应用程序更新,将目标 SDK 从 27 更改为 28,我肯定在其中发挥了作用。所有 5 位用户都在某种 Pixel 设备上运行某种风格的 android 9。我也是。

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

当我在我的 Pixel 上执行此操作时,效果很好。但是 5 位用户报告它抛出了一个异常,例如

java.lang.RuntimeException: Unable to start activity ComponentInfonet.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 个用户手动授予“存储”权限,AFAIK 解决了这个问题。但为什么这是必要的。我没有访问外部存储本身,也没有设置通道配置来要求它。如果需要 READ_EXTERNAL_STORAGE 权限,通知管理器应该管理它。

用户报告问题正在运行以下: google/taimen/taimen:9/PQ1A.190105.004/5148680:user/release-keys 谷歌/crosshatch/crosshatch:9/PQ1A.190105.004/5148680:user/release-keys google/marlin/marlin:9/PQ1A.181205.002.A1/5129870:user/release-keys 谷歌/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。一夜之间,又有 2 个用户报告了运行 Android 9 的 Pixel 手机上的这种特殊崩溃。两者都运行针对 SDK 28 的版本。一个回复我并确认了问题当他们安装 SDK 27 版本的应用程序时消失了。这证实这是针对 SDK 28 的应用程序的问题,可能与不允许应用程序使用全局访问文件系统权限来破坏应用程序沙箱限制的更改有关。

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

【问题讨论】:

我们可以看看你的代码吗?如果这是一个可自定义的图像,这些用户很可能是从某个需要许可的特殊位置进行选择。 铃声 picekr 没有授予 uri 读取权限? ManagePreferences.notifySound() 返回什么? ManagePreferense.notifySound() 返回由铃声选择器选择的 URI。它返回的内容与此处无关,因为结果仅在 SDK 构建级别低于 27 时使用,并且我们知道实际的 SDK 级别为 28。我们确实知道返回了什么值,而且它恰好是相同的铃声在通知通道中配置 (content://media/external/audio/media/145)。这不是巧合。当用户首次升级到 Android 8 时,该应用使用该配置值来设置默认通知渠道。 我也有同样的问题。我收到“SecurityException:没有对 content://media/external/audio/media/3532 的权限”。为了重现它,我将环聊消息铃声设置到我的通知频道并且没有存储权限。当我添加存储权限时,问题就消失了。其他铃声我也没有这个问题。 【参考方案1】:

我遇到了这个问题。原来是通知通道的创建有问题。

错误的方式:

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()
)
...

【讨论】:

但是“正确的方式”只会设置默认通知,而不是从content://media/external/...读取的自定义通知 我不确定我是否在关注你。我想要的是创建通知通道,使其预先配置为发出声音,即用户配置为通知的默认声音的通道。如果用户配置的默认通知声音恰好是自定义通知声音,使用content://media/external/... URI,则在执行上述“正确方式”时会起作用,并且在执行上述“错误方式”时会因问题中的堆栈跟踪而崩溃。跨度> 如果用户保持原来的频道设置,并更改(默认)通知声音,该频道的通知声音也会随之改变。【参考方案2】:

与其说是一个解决方案,不如说是一个漫长而复杂的解决方法。

首先,我捕获通知引发的 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 权限并提示他们。

【讨论】:

以上是关于Android 9.0 NotificationManager.notify() 抛出 java.lang.SecurityException的主要内容,如果未能解决你的问题,请参考以下文章

Android 9.0/P(android p指安卓9.0版本) okhttp3网络请求出错

为啥 Android Studio 不会在 Android Pie (9.0) 上运行应用程序?

AdapterView 不支持 Android 8.1/9.0 removeView(View)

多进程中的 Android Pie (9.0) WebView

text Android 9.0新特性

Android 9.0更新