Android: загрузка растрового изображения из локального хранилища в виджет приложения (RemoteViews)

До сих пор я загружал растровое изображение в свой RemoteViews напрямую используя remoteViews.setImageViewBitmap(), В целом работает нормально.

Но у пары пользователей есть проблемы, и я думаю, что это при загрузке в растровом изображении, которое является очень большим. Я все равно кэширую растровое изображение в локальное хранилище, поэтому моя идея состоит в том, чтобы использовать 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")));

РЕДАКТИРОВАТЬ

Пытаясь использовать FileProvider как предложено CommonsWare ниже...

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>

код:

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 ответа

Виджет вашего приложения отображается на главном экране. Главный экран не может получить доступ к внутренней памяти вашего приложения.

Вместо этого попробуйте использоватьFileProvider обслуживать файлы из внутреннего хранилища, с Uri предоставлено FileProvider в вашем setImageViewUri() вызов.

ОБНОВЛЕНИЕ: я забыл о FileProviderОграничения разрешений. Я не знаю надежного способа предоставления разрешений Uri к процессу домашнего экрана через RemoteViews, В идеале вы бы просто разрешили экспортировать провайдера, но FileProvider не поддерживает это.

Ваши варианты, то, являются:

  1. Напишите свой собственный поток ContentProvider, родственный этому, где поставщик экспортируется, так что каждый может потреблять Uri ценности.

  2. Поместите файл на внешнее хранилище, а не на внутреннее хранилище, затем надейтесь, что на главном экране есть READ_EXTERNAL_STORAGE или же WRITE_EXTERNAL_STORAGE разрешения.

  3. Убедитесь, что ваше изображение всегда достаточно маленькое, чтобы ваша транзакция IPC оставалась ниже предела в 1 МБ, и продолжайте использовать оригинал setImageViewBitmap() подход.

Я прошу прощения за то, что забыл о FileProvider не позволяет провайдеру быть экспортированным.

Этот ответ может быть (довольно) немного поздно.

Это можно сделать, используя слегка модифицированный FileProvider это позволяет провайдеру быть экспортированным. Вот что вам нужно сделать:

  1. Скачать исходный код FileProvider отсюда
  2. Скопируйте файл в ваш проект, например: com.xyz.fileprovider.FileProvider.java
  3. изменять attachInfo(Context context, ProviderInfo info) так что не бросай SecurityException, если вы объявите своего провайдера как экспортированного:

    @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. Обновите файл манифеста, чтобы он ссылался на ваш измененный FileProvider и установить android:exported к истине:

    <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. Обновите свой код, чтобы вы использовали com.xyz.fileprovider.FileProvider вместо 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));
    

Метод remoteViews.setImageViewUriдополнительно имеет ограничение размера растрового изображения для ImageView(примерно 1-2 Мб). Таким образом, вы получите ошибки или виджет будет прозрачным.

Так remoteViews.setImageViewBitmap()это хорошая практика, но вы слишком создаете процедуру с BitmapFactory.Options inSampleSizeи попробуйте привязать растровое изображение к размеру экрана и поймать OutOfMemoryError.

Сегодня наткнулся на это, но решил, что предоставление доступа к любому приложению запуска - меньшая дыра в безопасности, чем экспорт всего провайдера:)

Итак, прямо перед установкой 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);
    }
}
Другие вопросы по тегам