kotlinandroid-widgetandroid-imageviewandroid-7.0-nougat

How to pass image `Uri` to the `RemoteViews.setImageViewUri()` to set image in ImageView in a widget in Android without `FileUriExposedException`


As I understood from countless hours of trying to solve the issue, the activity which executes this method (RemoteViews.setImageViewUri()) is the "home screen" activity, and it has no permissions for reading arbitrary files. But my application (widget) has them (), and therefore can read and display images from system gallery (in MainActivity or in "configuration screen" of the widget). And there is no Intent to which you can give any permission flag (to allow the use of Uri or storage read).

So is it possible to display images (from external storage) in the widget at all? I mean, the ImageView is allowed in RemoteViews for something, right?

From https://developer.android.com/reference/android/widget/RemoteViews.html:

RemoteViews is limited to support for the following layouts:

  • AdapterViewFlipper
  • FrameLayout
  • GridLayout
  • GridView
  • LinearLayout
  • ListView
  • RelativeLayout
  • StackView
  • ViewFlipper

And the following widgets:

  • AnalogClock
  • Button
  • Chronometer
  • ImageButton
  • ImageView
  • ProgressBar
  • TextClock
  • TextView

As of API 31, the following widgets and layouts may also be used:

  • CheckBox
  • RadioButton
  • RadioGroup
  • Switch

Descendants of these classes are not supported.


Solution

  • As always, the answer is simple, but the lack of knowledge makes it that much harder. The "Image Uri" that "RemoteViews.setImageViewUri()" is expecting is the non-FileUriExposedException version of Uri. Now I can tell the story how I find out what is the "non-FileUriExposedException" version of Uri.

    Initially, I tried to pass just an absolute path as a Uri (I knew that I was using abs. path, but then suddenly forgot this):

    val image_uri = Uri.parse(return_absolute_file_path_to_image())
    

    But this didn't work. Then I prepended "content://" string to the return value — still no good. In the process I was able to have a small experience using Glide and Picasso libraries. I found a solution like this:

    internal fun update(
      context: Context,
      app_widget_manager: AppWidgetManager,
      app_widget_id: Int,
    ) {
      val image_uri = Uri.parse("file://" + return_absolute_file_path_to_image())
      // val image_uri = File(return_absolute_file_path_to_image()).toUri()
    
      views.setImageViewUri(R.id.imageView, image_uri)
    
      Picasso.get()
        .load(image_uri)
        .into(views, R.id.imageView, intArrayOf(app_widget_id))
    
      app_widget_manager.updateAppWidget(app_widget_id, views)
    }
    

    The difference is that "content://" has changed to "file://" or we can use File() constructor instead. We can also overwrite what goes into .into() method (heh) in such a way that we can overwrite methods that are responsible for error handling etc. (this is just Picasso things). This does work, but probably a bad way to go about the issue (because file's Uri is still kind of exposed, but I'm not sure).

    And it was a pretty clean solution, but I had to add +1 implementation dependency into build.gradle (:app).

    Finally, I saw Uri similar to this one: content://$provider/files/#. And I also saw that I can actually override openFile(uri: Uri, mode: String): ParcelFileDescriptor? method in ContentProvider's child class of mine (apparently ContentProvider does not require an overridden implementation of this method, but I can still do that). Basically, this is the method that should be called when "home screen" activity tries to get the image Uri. This method takes in previously mentioned Uri (content://$provider/files/#), but of course the part after content provider can be anything you like, even the absolute path of the image. And this method does not expose file's real URI (file:///path/to/image.png), that is why everything started to work as it should be. Now the function looks like this:

    internal fun update(
      context: Context,
      app_widget_manager: AppWidgetManager,
      app_widget_id: Int,
    ) {
      val views = RemoteViews(context.packageName, R.layout.gallery_slideshow)
    
      var image_uri = get_next_image_uri(context)
      views.setImageViewUri(R.id.imageView, image_uri)
    
      app_widget_manager.updateAppWidget(app_widget_id, views)
    }
    

    and get_next_image_uri(Context):

    internal fun get_next_image_uri(context: Context): Uri? {
      val authority = "${context.packageName}.provider"
      val uri_prefix = "content://$authority"
      val content_provider_uri = Uri.parse(uri_prefix)
      val cursor = context.contentResolver.query(
        content_provider_uri, null, null, null, null
      )
      cursor!!.moveToFirst()
      val gallery_image_path = cursor.getString(0)
      cursor.close()
      return Uri.parse("$uri_prefix/files/$gallery_image_path")
    }
    

    and the update() is called from onUpdate():

    class MyWidgetClass : AppWidgetProvider() {
      override fun onUpdate(
        context: Context,
        app_widget_manager: AppWidgetManager,
        app_widget_id_array: IntArray,
      ) {
        for (app_widget_id in app_widget_id_array) {
          update(context, app_widget_manager, app_widget_id)
        }
      }
      ...
    }
    

    So, there are 2 times, when something has to be fetched by using a Uri:

    1. context.contentResolver.query(content_provider_uri, null, null, null, null);
    2. views.setImageViewUri(R.id.imageView, image_uri)

    Both Uris are handled by the same class GalleryImagePathProvider : ContentProvider() {}. In the first case it is handled through the override fun query() method, and in the second case — through the override fun openFile() method. Now, in order for everything to work properly, we need to add some "magic" to the AndroidManifest.xml:

    <manifest ... >
      ...
      <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
      ...
      <application ... >
      ...
        <provider
          android:name=".GalleryImagePathProvider"
          android:authorities="${applicationId}.provider"
          android:exported="true"
          tools:ignore="ExportedContentProvider" />
      </application>
    </manifest ... >
    

    The key point here is android:exported="true", without it "home screen" activity can't display images (at least in my case). (Permission to read external storage is used when accessing system gallery's images.)

    That's all, I think I went over all the important things and problems that I had to solve to achieve the goal.


    P.S. I don't want to make a whole promotion, but (at this point I kinda have to) when I posted this question, I got the

    Calling all who code. Take the 2023 Developer Survey.

    notification and when I got to the questions about AI tools/language models, I found quite a big list of bots or search engines. I then laid my eyes on You.com text. And almost instantly (free and \wo any limits), I finally got to know how it feels when you have a "companion" that tries hard to solve all your coding questions and problems. And thanks to YouBot, I was able to crunch down a whole a lot of time of debugging by simply listening to the advices and suggestions from it. So, as a fellow coder, I can recommend it. (But sometime prompt input lag becomes really big, and after some time in standby mode you have to refresh the tab for it to be able to work properly again. Everything else is super nice and cool.)