Android: 将本地存储中的位图加载到应用小部件(RemoteViews)

3

到目前为止,我一直在使用remoteViews.setImageViewBitmap()直接将位图加载到我的RemoteViews中。总体上运作良好。

但是有几个用户遇到了问题,我认为可能是由于加载非常大的位图时出现的问题。我已经将位图缓存到本地存储器中,所以我的想法是改用建议中的setImageViewUri()

但我无法使其正常工作... 我只得到一个空白小部件。下面的代码片段说明了我的操作(省略了一些上下文但希望保留足够的信息)...

// 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")));

编辑

尝试使用下面CommonsWare建议的FileProvider...

清单:

<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>

代码:

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));

但仍然存在一个空的小部件,与之前一样,并且在logcat中没有错误。
4个回答

5

您的应用小部件由主屏幕呈现。主屏幕无法访问您的应用程序的内部存储。

相反,尝试使用 FileProvider从内部存储中提供文件,并在setImageViewUri()调用中使用FileProvider提供的Uri

更新: 我忘记了FileProvider的权限限制。我不知道通过RemoteViews向主屏幕进程授予Uri权限的可靠方法。理想情况下,您只需允许提供程序被导出即可,但FileProvider不支持该功能。

因此,您有以下选项:

  1. 编写自己的流媒体ContentProvider,类似于这个,其中提供程序已导出,因此任何人都可以使用Uri值。

  2. 将文件放在外部存储器上,而不是内部存储器上,然后希望主屏幕具有READ_EXTERNAL_STORAGEWRITE_EXTERNAL_STORAGE权限。

  3. 确保您的图像始终相当小,以使IPC事务保持在1MB以下,并使用原始的setImageViewBitmap()方法。

对于我忘记FileProvider不允许提供程序导出表示道歉。


@drmrbrewer:我建议你删除path=".",并且在File构造函数中也删除"."。这两个都是不必要的,而且可能会干扰FileProvider。我还建议你始终使用getFilesDir(),替换openFileOutput()。但是,我怀疑你的主要问题在于ContentProvider权限——我已经编辑了我的答案。 - CommonsWare
关于您的UPDATE,为了将本地位图放入RemoteViews中,似乎需要付出很多努力...为什么要这样做呢?我的问题不是一个位图的大小,而是两个位图的大小。当两个小部件同时刷新时,问题似乎会发生,因此我认为总大小是问题的原因。当它们单独刷新“大”小部件时,一切都很好。这可能发生吗...组合大小成为问题?我希望尝试使用setImageViewUri()进行直接加载,以快速测试我的假设。 - drmrbrewer
@drmrbrewer:“为什么这么难?”--标准IPC事务有1MB的限制,例如将您的RemoteViews从您的进程传递到操作系统进程,然后再传递到主屏幕进程。 “可能会发生这种情况...组合大小成为问题吗?”--据我所知不会,但我不能完全排除这种可能性。“我希望尝试使用setImageViewUri()进行直接加载,以快速测试我的假设。”--然后将文件暂时放在外部存储上,看看会发生什么。 - CommonsWare
我正在尝试通过使用指向本地存储中已有的位图的“链接”,而不是将大型位图对象添加到RemoteViews中,来规避1MB限制。我认为应该更容易地指定要在RemoteViews中使用的本地文件的位置,而无需跳过障碍或使用外部存储(需要用户不喜欢授予的额外权限)。 - drmrbrewer
关于我的假设,@CommonsWare,即小部件“RemoteViews”的组合大小可能超过了限制,我基于http://stackoverflow.com/q/18841346/4070848和http:// developer.android.com/reference/android/os/TransactionTooLargeException.html等来支持这个假设 - 当小部件单独刷新时没问题。 重新启动后(在此期间它们都大约同时刷新)刷新就成了问题。 - drmrbrewer
显示剩余4条评论

2
这个回答可能有点晚了。如果使用一个稍微修改过的FileProvider,允许提供程序被导出,那么这是可以实现的。下面是需要进行的操作:
  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));
    

非常感谢您详细的回答。我已经成功地让它工作了,没有任何问题。 - Henning

1

方法remoteViews.setImageViewUri还有一个限制,即对于ImageView的位图大小有限制(大约为1-2 MB)。因此,您将会得到错误或小部件将变成透明。

因此,remoteViews.setImageViewBitmap()是一个好的做法,但是您需要创建一个带有BitmapFactory.Options inSampleSize过程,并尝试将位图边界调整为屏幕大小并捕获OutOfMemoryError


0
今天遇到了这个问题,但发现授予任何启动器应用程序访问权限比导出整个提供程序更安全小一些 :)
因此,在设置URI到远程视图之前,我现在将访问URI授予每个已安装的主屏幕:
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);
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接