You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

重启/重建Activity后Launcher访问FileProvider权限被拒问题

解决Glance Widget重启/重装后图片权限丢失问题

问题背景

我正在开发一款基于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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.10 22:34:55