Reputation: 141
I am working on an Android app with a Compose Glance widget based on the Glance images example.
In my widget worker I am first calling an API and storing images to cacheDir
then display images from their URI in the widget with a "next" button to cycle through images.
My problem: When the widget is stopped or the Android Studio emulator runs the app again, the widget will die. I do not see any errors in the logcat from my app/widget, but I do see these warnings. The UIDs do not change, the PID changes when the emulator is restarted:
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
The warnings seem to be saying that though permissions from my app (uid=10185) to the launcher are set correctly the first time, the launcher (com.google.android.apps.nexuslauncher/u0a151) loses the permission when the app is reinstalled/device is rebooted.
I have a one idea, which is that I need to persist the permissions to the launcher. I'm not sure how this is done, as it appears the code from the example is already doing this as seen below. From the logs I can see that the code below sets the permissions to "com.google.android.apps.nexuslauncher" (no /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")
}
)
}
And incase it is the issue, my 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>
Upvotes: 1
Views: 401
Reputation: 141
After many trial and error the solution was in the end simple, but surprising. The permissions sometimes need to be set before the image is opened from within the widget.
In my code above, the permission for the URI are lost when the main activity is lost and the permissions, which are NOT persistable to the launcher are also lost.
Thus, unless the data is reset by a new Worker, the next time the Widget attempts to access the URI it has lost it's access. The solution though is quite easy, you just need to set the permission before the Widget attempts to open the URI.
I'm not sure if that is a security concern, but it seems to work perfectly fine on API 34.
Upvotes: 1