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.
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
:
context.contentResolver.query(content_provider_uri, null, null, null, null)
;views.setImageViewUri(R.id.imageView, image_uri)
Both Uri
s 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.)