3

Until now I've been loading a bitmap into my RemoteViews directly using remoteViews.setImageViewBitmap(). It's working fine in general.

But a couple of users are having issues, and I think it is when loading in a bitmap that is very large. I cache the bitmap to local storage already anyway, so my idea is to use setImageViewUri() instead, as recommended elsewhere.

But I can't get it to work... I just get a blank widget. The following snippet illustrates what I'm doing (leaving out some the context but hopefully leaving enough)...

// Bitmap bmp is fetched from elsewhere... and then...

FileOutputStream fos = context.openFileOutput("img.png", Context.MODE_PRIVATE);
bmp.compress(Bitmap.CompressFormat.PNG, 100, fos);
fos.flush();
fos.close();

// ... later ...

// this works...
remoteViews.setImageViewBitmap(R.id.imageView, bmp);

// but this doesn't...
remoteViews.setImageViewUri(R.id.imageView, Uri.fromFile(new File(context.getFilesDir().getPath(), "img.png")));

EDIT

Trying to use FileProvider as suggested by CommonsWare below...

Manifest:

<provider
    android:name="android.support.v4.content.FileProvider"
    android:authorities="com.xyz.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:

<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <files-path name="cache" path="."/>
</paths>

code:

File imagePath = new File(context.getFilesDir(), ".");
File newFile = new File(imagePath, "img.png"); // img.png created as above, in top level folder
remoteViews.setImageViewUri(R.id.imageView, FileProvider.getUriForFile(context, "com.xyz.fileprovider", newFile));

But still I'm left with a blank widget, as before, and without an error in the logcat.

drmrbrewer
  • 11,491
  • 21
  • 85
  • 181

4 Answers4

5

Your app widget is being rendered by the home screen. The home screen cannot access internal storage of your app.

Instead, try using FileProvider to serve up files from internal storage, with a Uri supplied by FileProvider in your setImageViewUri() call.

UPDATE: I forgot about FileProvider's permission limitations. I do not know of a reliable way to grant permissions to the Uri to the home screen process via RemoteViews. Ideally, you would just allow the provider to be exported, but FileProvider does not support that.

Your options, then, are:

  1. Write your own streaming ContentProvider, akin to this one, where the provider is exported, so anyone can consume the Uri values.

  2. Put the file on external storage, instead of internal storage, then hope that the home screen has READ_EXTERNAL_STORAGE or WRITE_EXTERNAL_STORAGE permissions.

  3. Ensure that your image is always fairly small, so your IPC transaction stays below the 1MB limit, and go with your original setImageViewBitmap() approach.

I apologize for having forgotten about FileProvider not allowing the provider to be exported.

CommonsWare
  • 986,068
  • 189
  • 2,389
  • 2,491
  • I've added an attempt based on this approach in the Question. Please can you explain where I'm going wrong? Thanks. – drmrbrewer Feb 01 '16 at 21:01
  • @drmrbrewer: I would get get rid of `path="."`, and get rid of `"."` in the `File` constructor. Neither are needed and either might interfere with `FileProvider`. I also suggest that you use `getFilesDir()` consistently, replacing `openFileOutput()`. However, I suspect that your big problem is with `ContentProvider` permissions -- I edited my answer. – CommonsWare Feb 01 '16 at 21:11
  • Referring to your UPDATE, it all seems like a lot of hard work just to get a local bitmap into the `RemoteViews`... why is it made so hard? The problem I had is not so much with the size of *one* bitmap, but more with the same of *two*. The issue seems to occur when two widgets are being refreshed at about the same time, so I think the sum total of sizes is causing the problem. When they refresh the "big" widget on its own, it is fine. Could that happen... the combined size being the issue? I was hoping to try direct loading with `setImageViewUri()` quickly as a test of my hypothesis. – drmrbrewer Feb 01 '16 at 22:19
  • @drmrbrewer: "why is it made so hard?" -- there is a 1MB limit on standard IPC transactions, such as passing your `RemoteViews` from your process to an OS process, and from there to the home screen process. "Could that happen... the combined size being the issue?" -- not that I am aware of, but I can't completely rule it out. "I was hoping to try direct loading with setImageViewUri() quickly as a test of my hypothesis." -- then put the file on external storage temporarily, and see what happens. – CommonsWare Feb 01 '16 at 22:21
  • I'm trying to circumvent the 1MB limit by using a "link" to a bitmap already on local storage, rather than adding a massive Bitmap object into the `RemoteViews`. I just thought it should be easier to specify the location of a local file to be used in the `RemoteViews` without having to jump through hoops or using external storage (requiring extra permissions that users don't like granting). – drmrbrewer Feb 01 '16 at 22:39
  • Regarding my hypothesis, @CommonsWare, that it may be the *combined* size of the widget `RemoteViews` that is over the limit, I'm basing this e.g. on http://stackoverflow.com/q/18841346/4070848 and http://developer.android.com/reference/android/os/TransactionTooLargeException.html -- when the widget is refreshed on its own it's OK. It's refreshing after a reboot (where they're all refreshed at around the same time) that is the problem. – drmrbrewer Feb 02 '16 at 08:29
  • Late follow-up question @CommonsWare... in [this question](https://stackoverflow.com/q/7901138/4070848) the example code is loading a bitmap into the app widget with a Uri via `Uri.parse(file.getPath())`, i.e. without a `FileProvider` at all? Does that circumvent the "permission limitations" you mention for `FileProvider` with app widgets? Also I wonder whether you have a better answer on that question than the two existing answers... both work (I've tried) but both seem a bit hacky. – drmrbrewer Aug 04 '17 at 07:04
  • @drmrbrewer: "Does that circumvent the "permission limitations" you mention for FileProvider with app widgets?" -- I suggested using a file on external storage in my previous comment. That example is showing internal storage with a now-banned `MODE_WORLD_READABLE` flag. – CommonsWare Aug 04 '17 at 10:19
  • ah that explains why they didn't need a FileProvider (but now would). And did you perhaps have a better answer for that other question, assuming that a custom FileProvider is used that works with an app widget... the problem is that even if the bitmap content has changed, calling `setImageViewUri()` for the same Uri does not load the new bitmap... a workaround is to call `setImageViewUri` on an empty Uri (or a Uri pointing to duplicate bitmap) and then immediately again for the "actual" (different) Uri, to "trick" the widget and force it to load the new content. But seems kludgy. – drmrbrewer Aug 04 '17 at 10:41
2

This answer might be coming (quite) a bit late.

It is possible get this working by using a slightly modified FileProvider that allows the provider to be exported. Here is what you need to do:

  1. Download the source code of FileProvider from here.
  2. Copy the file to your project, e.g. to: com.xyz.fileprovider.FileProvider.java
  3. Modify attachInfo(Context context, ProviderInfo info) so that it does not throw a SecurityException, if you declare your provider as exported:

    @Override
    public void attachInfo(Context context, ProviderInfo info) {
        super.attachInfo(context, info);
    
        // Sanity check our security
    
        // Do not throw an exception if the provider is exported
        /*
        if (info.exported) {
            throw new SecurityException("Provider must not be exported");
        }
        */
        if (!info.grantUriPermissions) {
            throw new SecurityException("Provider must grant uri permissions");
        }
    
        mStrategy = getPathStrategy(context, info.authority);
    }
    
  4. Update your manifest file, so that it references your modified FileProvider and set android:exported to true:

    <provider>
        android:name="com.xyz.fileprovider.FileProvider"
        android:authorities="com.xyz.fileprovider"
        android:exported="true"
        android:grantUriPermissions="true">
        <meta-data
            android:name="android.support.FILE_PROVIDER_PATHS"
            android:resource="@xml/file_paths" />
    </provider>
    
  5. Update your code, so that you are using com.xyz.fileprovider.FileProvider instead of android.support.v4.content.FileProvider:

    // Bitmap bmp is fetched from elsewhere... and then...
    
    FileOutputStream fos = context.openFileOutput("img.png", Context.MODE_PRIVATE);
    bmp.compress(Bitmap.CompressFormat.PNG, 100, fos);
    fos.flush();
    fos.close();
    
    // ... later ...
    
    // this should work...
    File imagePath = new File(context.getFilesDir(), ".");
    File newFile = new File(imagePath, "img.png"); // img.png created as above, in top level folder
    
    remoteViews.setImageViewUri(R.id.imageView, Uri.parse("")); // this is necessary, without it the launcher is going to cache the Uri we set on the next line and effectively update the widget's image view just once!!!
    remoteViews.setImageViewUri(R.id.imageView, com.xyz.fileprovider.FileProvider.getUriForFile(context, "com.xyz.fileprovider", newFile));
    
ra3o.ra3
  • 852
  • 1
  • 8
  • 7
1

Method remoteViews.setImageViewUri additionally has limitation of bitmap size for ImageView (approximately 1-2 Mb ). So you will gets errors or widget will be transparent.

So remoteViews.setImageViewBitmap() is good practice, but you have too create procedure with BitmapFactory.Options inSampleSize and try to bounds bitmap to the screen size and catch OutOfMemoryError.

Style-7
  • 985
  • 12
  • 27
0

Came across this today but figured that granting access to any launcher app is a smaller security hole than exporting the whole provider :)

So right before setting the uri to the remote views I'm now granting access to the URI to each installed home screen:

protected void bindTeaserImage(Context context, final RemoteViews remoteViews, final TeaserInfo teaserInfo) {
    final Uri teaserImageUri = getTeaserImageUri(teaserInfo); // generates a content: URI for my FileProvider
    grantUriAccessToWidget(context, teaserImageUri);
    remoteViews.setImageViewUri(R.id.teaser_image, teaserImageUri);
}

protected void grantUriAccessToWidget(Context context, Uri uri) {
    Intent intent= new Intent(Intent.ACTION_MAIN);
    intent.addCategory(Intent.CATEGORY_HOME);
    List<ResolveInfo> resInfoList = context.getPackageManager().queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
    for (ResolveInfo resolveInfo : resInfoList) {
        String packageName = resolveInfo.activityInfo.packageName;
        context.grantUriPermission(packageName, uri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
    }
}