ddxv
ddxv

Reputation: 141

Permission Denial: opening file provider from launcher after reboot or restart Activity

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

Answers (1)

ddxv
ddxv

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

Related Questions