0

I have an android homescreen widget created with the new Glance api which contains a lazy column. Each row in the column displays an image with ImageProvider(contentUri).

The image has been retrieved from a URL with Glide and saved to internal storage file with FileOutputStream(filename). see MainActivity below.

When attempting to retrieve and show the image with getUriForFile() my widget just shows a message "Loading..." and never displays the image. No crash occurs. It just never loads the image.

How can I pull the image from internal storage and display it in LazyColumnRow()?

Note that I am only showing one bitmap in this example as proof of concept, but intend to eventually show 10+ bitmaps. I am following the recommendation in below post, which suggests using URI for multiple images.

Crash in glance app widget image when trying to display bitmap

Note that when I pull the same URI in MainActivity() it works and displays the image in ImageView.

Composable defined in GlanceAppWidget Content()

@Composable
fun LazyColumnRow(
    /*LazyColumnRow is called from a LazyColumn, with each filename passed in here*/
) {
    val context = LocalContext.current
    val filepath = File(context.getFilesDir(), "my_images")
    val filename = File(filepath, "default_image.png") /*each LazyColumn item will have different filename in PROD*/
    val contentUri: Uri = FileProvider.getUriForFile(context,"${context.packageName}.fileprovider", filename)
    
    Row(modifier = GlanceModifier) {
        Image(
            modifier = GlanceModifier.size(28.dp),
            provider = ImageProvider(contentUri), /*this is not working*/
            contentDescription = "Image"
        )
    }
}

MainActivity class which downloads and stores Bitmap. It can also succesfully retrieve URI and display in imageView.

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
    private lateinit var binding: ActivityMainBinding

    @OptIn(ExperimentalAnimationApi::class)
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = inflate(layoutInflater)
        setContentView(binding.root)

        //Download image from url
        var bitmap: Bitmap? = null
        CoroutineScope(Dispatchers.IO).launch {
            bitmap = Glide.with(baseContext)
                .asBitmap()
                .load("https://picsum.photos/id/237/200")
                .submit()
                .get()
        }

        //store image in internal storage file
        val filepath = File(baseContext.getFilesDir(), "my_images")
        if (!filepath.exists()) {
            filepath.mkdirs()
        }
        val filename = File(filepath, "default_image.png")
        try {
            FileOutputStream(filename).use { out ->
                bitmap?.compress(Bitmap.CompressFormat.PNG, 100, out)
            }
        } catch (e: IOException) {
            e.printStackTrace()
        }

        //retrieve image from internal storage file
        val contentUri: Uri = getUriForFile(
            baseContext,
            "$packageName.fileprovider",
            filename)

        //display in imageView. This code works.
        val imageView = findViewById<ImageView>(R.id.myImage)
        imageView.setImageURI(contentUri)
    }
}

Content Provider declared in Manifest for FileProvider URI

<provider
    android:name="androidx.core.content.FileProvider"
    android:authorities="${applicationId}.fileprovider"
    android:exported="false"
    android:grantUriPermissions="true">
    <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/file_paths" />
</provider>

xml/file_paths

<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <files-path name="my_images" path="/" />
    <files-path name="my_docs" path="docs/" />
</paths>

mars8
  • 770
  • 1
  • 11
  • 25
  • `bitmap has been retrieved from a URL (via 'Glide') and saved to internal storage getFilesDir()` I do not believe that. The url will prabably have pointed to an image file like .jpg or .png. And in getFilesDir() again an image file will have been stored. If you used an intermediate bitmap then that looks pretty irrelevant. – blackapps Apr 20 '22 at 14:23
  • @blackapps you are right. I have updated the question to make it more clear. thanks. – mars8 Apr 20 '22 at 16:02
  • It makes no sense to use FileProvider to obtain an uri for a file you created in getFilesDir(). – blackapps Apr 20 '22 at 17:50
  • @blackapps i presume then that `FileProvider` is only used for accessing external files e.g. those created with `getExternalFilesDir(String)`. Is this the reason for the widget error? i.e. because the remote views cannot access internal files created with `getFilesDir()`, and i should instead save the files using `getExternalFilesDir(String)`? – mars8 Apr 20 '22 at 18:16
  • 1
    No. You use FileProvider if you want an external app to handle your file. If the app handles the file itself it can use original path. No need for an uri. – blackapps Apr 20 '22 at 18:33
  • @blackapps The file is being handled by remote views, which I thought was analogous to being handled by external app. If you have an alternative method of how to retrieve the image (not involving URIs) and can provide an answer then that would be much appreciated. – mars8 Apr 20 '22 at 19:05
  • I have no idea what you mean with remote view. – blackapps Apr 20 '22 at 19:33
  • @blackapps the widget is displayed in a RemoteViews object outside of the app which is managed by the android OS. https://developer.android.com/reference/android/widget/RemoteViews – mars8 Apr 20 '22 at 19:47
  • @blackapps i am still stuck on this issue. Would you be able to elaborate on your comment _"If the app handles the file itself it can use original path."_. How would you retrieve the image with original Path? – mars8 May 02 '22 at 14:00
  • I'm not familiar with RemoteViews so i dont know if they should be considered as belonging to the app or as external app. But anyhow. If an app can save an image to a path ten it can load that image from the same path. Why do you consider that a probem? Write/read. – blackapps May 02 '22 at 15:14
  • And if the app wants an external app to load that image then an uri of a file provider should be used. – blackapps May 02 '22 at 15:15
  • @mars8 did you solve the problem? – Subfly Dec 15 '22 at 07:55
  • @Subfly unfortunately this appears to be a known issue and I have not seen a way of fixing yet. I would recommend checking this [post](https://stackoverflow.com/a/74361579/15597975) where it eludes to upcoming changes in the api which may fix it. – mars8 Dec 16 '22 at 20:06
  • @mars8 After 1.5 months of trying, I have finally accepted my fate and returned to the old implementation of widgets. – Subfly Dec 17 '22 at 22:43

1 Answers1

0

You need to explicitly grant read permission to the Home activities, for example with:

/**
 * When sharing a content URI to an app widget, we need to grant read access to the Home activity.
 *
 * See https://stackoverflow.com/a/59108192/1474476
 */
fun Context.grantUriPermissionToHomeActivity(uri: Uri, flags: Int = Intent.FLAG_GRANT_READ_URI_PERMISSION) {
    grantUriPermission(packageManager.homeActivityPackages(), uri, flags)
}

fun PackageManager.homeActivityPackages(): List<String> {
    val intent = Intent(Intent.ACTION_MAIN)
    intent.addCategory(Intent.CATEGORY_HOME)
    return queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY)
        .map { it.activityInfo.packageName }
}

fun Context.grantUriPermission(packages: List<String>, uri: Uri, flags: Int) {
    for (name in packages) {
        grantUriPermission(name, uri, flags)
    }
}

Mickaël
  • 310
  • 3
  • 8