重启/重建Activity后Launcher访问FileProvider权限被拒问题
问题背景
我正在开发一款基于Glance images示例的Android Compose Glance Widget应用:在Widget Worker中调用API将图片存储到cacheDir,通过URI在Widget中展示,同时支持「下一张」按钮切换图片。
遇到的问题:Widget停止运行或模拟器重启应用后,Widget崩溃。Logcat中无应用/Widget错误,但出现以下警告(UID不变,重启模拟器后PID改变):
Permission Denial: opening provider com.thirdgate.stormtracker.ImageFileProvider from ProcessRecord{a75fce2 1089:com.google.android.apps.nexuslauncher/u0a151} (pid=1089, uid=10151) that is not exported from UID 10185
Permission Denial: opening provider com.thirdgate.stormtracker.ImageFileProvider from ProcessRecord{a75fce2 1089:com.google.android.apps.nexuslauncher/u0a151} (pid=1089, uid=10151) that is not exported from UID 10185
Error inflating RemoteViews android.widget.RemoteViews$ActionException: java.lang.SecurityException: Permission Denial: opening provider com.thirdgate.stormtracker.ImageFileProvider from ProcessRecord{a75fce2 1089:com.google.android.apps.nexuslauncher/u0a151} (pid=1089, uid=10151) that is not exported from UID 10185
警告显示:首次给应用(uid=10185)对应的Launcher设置权限正常,但应用重装/设备重启后,Launcher(com.google.android.apps.nexuslauncher/u0a151)会丢失权限。虽然代码中已尝试持久化权限,但日志显示权限仅授予了「com.google.android.apps.nexuslauncher」(不含/u0a151)。
现有权限处理代码
// Called from Widget Worker which after saving files to cacheDir, calls MyWidget.update() for ((index, myImg) in myImagesBytes.withIndex()) { val fileName = "compareModels_$index.jpg" val imageFile = File(context.cacheDir, fileName).apply { writeBytes(myImg) } val contentUri = getUriForFile( context, "${applicationContext.packageName}.provider", imageFile, ) // Find the current launcher every time to ensure it has read permissions val intent = Intent(Intent.ACTION_MAIN).apply { addCategory(Intent.CATEGORY_HOME) } val resolveInfo = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { context.packageManager.resolveActivity( intent, PackageManager.ResolveInfoFlags.of(PackageManager.MATCH_DEFAULT_ONLY.toLong()), ) } else { @Suppress("DEPRECATION") context.packageManager.resolveActivity( intent, PackageManager.MATCH_DEFAULT_ONLY, ) } val launcherName = resolveInfo?.activityInfo?.packageName if (launcherName != null) { context.grantUriPermission( launcherName, contentUri, FLAG_GRANT_READ_URI_PERMISSION or FLAG_GRANT_PERSISTABLE_URI_PERMISSION, ) } else { Log.e("ImageWorker", "launcherName was null, did not set permissions") } }
Manifest配置
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"> <queries> <intent> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.HOME" /> </intent> </queries> <application android:allowBackup="true" android:dataExtractionRules="@xml/data_extraction_rules" android:fullBackupContent="@xml/backup_rules" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.StormTracker" tools:targetApi="31"> <activity android:name="com.thirdgate.stormtracker.MainActivity" android:exported="true" android:theme="@style/Theme.StormTracker"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <receiver android:name="com.thirdgate.stormtracker.ImageGlanceWidgetReceiver" android:exported="true"> <intent-filter> <action android:name="android.appwidget.action.APPWIDGET_UPDATE" /> </intent-filter> <meta-data android:name="android.appwidget.provider" android:resource="@xml/my_app_widget_info" /> </receiver> <provider android:name="com.thirdgate.stormtracker.ImageFileProvider" android:authorities="com.thirdgate.stormtracker.provider" android:exported="false" android:grantUriPermissions="true"> <meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/filepaths" /> </provider> </application> <uses-permission android:name="android.permission.INTERNET" /> </manifest>
解决方案
1. 优化权限授予逻辑,确保持久化生效
FLAG_GRANT_PERSISTABLE_URI_PERMISSION需要配合主动的权限校验才能在重启后保留。修改代码,先撤销旧权限再重新授予,同时针对Android 12+确认持久化权限:
if (launcherName != null) { // 先撤销旧权限,避免权限冲突 context.revokeUriPermission(contentUri, FLAG_GRANT_READ_URI_PERMISSION) // 重新授予带持久化标记的权限 context.grantUriPermission( launcherName, contentUri, FLAG_GRANT_READ_URI_PERMISSION or FLAG_GRANT_PERSISTABLE_URI_PERMISSION ) // Android 12+ 确认持久化权限 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { try { context.contentResolver.takePersistableUriPermission( contentUri, FLAG_GRANT_READ_URI_PERMISSION ) } catch (e: SecurityException) { Log.d("ImageWorker", "Persistable permission already exists", e) } } }
2. 处理应用重装后的文件重建
应用重装后cacheDir会被清空,旧URI指向无效文件也会触发权限问题。在生成URI前先检查文件是否存在,不存在则重新下载:
val imageFile = File(context.cacheDir, fileName) if (!imageFile.exists()) { imageFile.writeBytes(myImg) } // 后续生成URI和授予权限的逻辑不变
3. 直接通过UID授予权限
日志中Launcher的UID是u0a151,直接通过包名获取UID再授予权限更可靠:
val launcherName = resolveInfo?.activityInfo?.packageName if (launcherName != null) { try { val launcherUid = context.packageManager.getApplicationInfo(launcherName, 0).uid // 传入null作为包名,通过UID指定权限接收者 context.grantUriPermission( null, contentUri, FLAG_GRANT_READ_URI_PERMISSION or FLAG_GRANT_PERSISTABLE_URI_PERMISSION, launcherUid ) } catch (e: PackageManager.NameNotFoundException) { Log.e("ImageWorker", "Launcher package not found", e) } }
4. 在Widget更新接收器中触发权限检查
在ImageGlanceWidgetReceiver的onUpdate方法中,每次更新Widget时重新检查权限:
override fun onUpdate( context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray ) { super.onUpdate(context, appWidgetManager, appWidgetIds) // 调用自定义的权限检查方法 checkAndGrantImagePermissions(context) }
这样系统重启后Widget自动更新时,会重新确认权限。
5. 验证FileProvider路径配置
确保res/xml/filepaths.xml正确配置cacheDir访问路径:
<?xml version="1.0" encoding="utf-8"?> <paths xmlns:android="http://schemas.android.com/apk/res/android"> <cache-path name="cache_images" path="." /> </paths>
path="."表示允许访问cacheDir下所有文件,保证生成的URI有效。
内容的提问来源于stack exchange,提问作者ddxv

