Glide: Share image in cache

Created on 7 May 2015  路  56Comments  路  Source: bumptech/glide

I have a question in regarding sharing an image via Intent Share API which was already downloaded in cache(or not in the worst case scenario). Do you have any suggestions regarding this? Would it work with the FileProvider class in the support lib? The current implementation now loads the URL in a custom target, writes the bitmap into a common file(share.jpeg) and shares that one with a custom ContentProvider. Works so far but it feels like extra unnecessary work which makes the user wait for a while for an image which is already there.

question

Most helpful comment

I think what you need to do is point your FileProvider to your cache directory:

<!-- Based on default Glide operation, see InternalCacheDiskCacheFactory instantiated from GlideBuilder.createGlide() -->
<cache-path name="share" path="image_manager_disk_cache" tools:path="DiskCache.Factory.DEFAULT_DISK_CACHE_DIR" />

and then ask Glide for a File through downloadOnly. You can then FileProvider.getUriForFile and hopefully would work. Here's something hopefully reusable I whipped up quickly and not tested:

new ShareTask(this).execute("http://blah");

This will share your uri regardless of having it in cache or not (your worst case). To make sure it's cached you should use DiskCacheStrategy.ALL somewhere else in your app where you load the same uri as described in Loading and Caching on Background Threads

class ShareTask extends AsyncTask<String, Void, File> {
    private final Context context;
    public ShareTask(Context context) {
        this.context = context;
    }
    @Override protected File doInBackground(String... params) {
        String url = params[0]; // should be easy to extend to share multiple images at once
        try {
            // Glide v3
            return Glide
                    .with(context)
                    .load(url)
                    .downloadOnly(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL)
                    .get() // needs to be called on background thread
                    ;
            // Glide v4
            return Glide
                    .with(context)
                    .downloadOnly()
                    .load(url)
                    .submit(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL)
                    .get() // needs to be called on background thread
                    ;
        } catch (Exception ex) {
            Log.w("SHARE", "Sharing " + uri + " failed", ex);
            return null;
        }
    }
    @Override protected void onPostExecute(File result) {
        if (result == null) { return; }
        Uri uri = FileProvider.getUriForFile(context, context.getPackageName(), result);
        share(uri); // startActivity probably needs UI thread
    }

    private void share(Uri result) {
        Intent intent = new Intent(Intent.ACTION_SEND);
        intent.setType("image/jpeg");
        intent.putExtra(Intent.EXTRA_SUBJECT, "Shared image");
        intent.putExtra(Intent.EXTRA_TEXT, "Look what I found!");
        intent.putExtra(Intent.EXTRA_STREAM, uri);
        context.startActivity(Intent.createChooser(intent, "Share image"));
    }
}

You could convert the above to AsyncTask<FutureTarget<File>, Void, File> if you want more flexibility and call just get() in the background. The way I presented above has the benefit of just firing it from anywhere in the app and you'll have the same sharing logic (size and everything).

Here are some further references:

All 56 comments

I think what you need to do is point your FileProvider to your cache directory:

<!-- Based on default Glide operation, see InternalCacheDiskCacheFactory instantiated from GlideBuilder.createGlide() -->
<cache-path name="share" path="image_manager_disk_cache" tools:path="DiskCache.Factory.DEFAULT_DISK_CACHE_DIR" />

and then ask Glide for a File through downloadOnly. You can then FileProvider.getUriForFile and hopefully would work. Here's something hopefully reusable I whipped up quickly and not tested:

new ShareTask(this).execute("http://blah");

This will share your uri regardless of having it in cache or not (your worst case). To make sure it's cached you should use DiskCacheStrategy.ALL somewhere else in your app where you load the same uri as described in Loading and Caching on Background Threads

class ShareTask extends AsyncTask<String, Void, File> {
    private final Context context;
    public ShareTask(Context context) {
        this.context = context;
    }
    @Override protected File doInBackground(String... params) {
        String url = params[0]; // should be easy to extend to share multiple images at once
        try {
            // Glide v3
            return Glide
                    .with(context)
                    .load(url)
                    .downloadOnly(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL)
                    .get() // needs to be called on background thread
                    ;
            // Glide v4
            return Glide
                    .with(context)
                    .downloadOnly()
                    .load(url)
                    .submit(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL)
                    .get() // needs to be called on background thread
                    ;
        } catch (Exception ex) {
            Log.w("SHARE", "Sharing " + uri + " failed", ex);
            return null;
        }
    }
    @Override protected void onPostExecute(File result) {
        if (result == null) { return; }
        Uri uri = FileProvider.getUriForFile(context, context.getPackageName(), result);
        share(uri); // startActivity probably needs UI thread
    }

    private void share(Uri result) {
        Intent intent = new Intent(Intent.ACTION_SEND);
        intent.setType("image/jpeg");
        intent.putExtra(Intent.EXTRA_SUBJECT, "Shared image");
        intent.putExtra(Intent.EXTRA_TEXT, "Look what I found!");
        intent.putExtra(Intent.EXTRA_STREAM, uri);
        context.startActivity(Intent.createChooser(intent, "Share image"));
    }
}

You could convert the above to AsyncTask<FutureTarget<File>, Void, File> if you want more flexibility and call just get() in the background. The way I presented above has the benefit of just firing it from anywhere in the app and you'll have the same sharing logic (size and everything).

Here are some further references:

@TWiStErRob Thanks for all the info. Everything looks great so I'll give it a try and let you know.

@TWiStErRob downloadOnly actually can be called on a background thread so you can avoid the pre execute here if you want.

Otherwise spot on.

As long as the disk cache size is sufficiently large, you can probably rely on images being present in the cache for long enough for them to be shared (it's a simple LRU eviction order). If you're using a small cache and/or for whatever reason have high churn, you may want to consider copying the File out of the cache first.

Thanks @sjudd, updated the code. I remembered into(Target) needing main, but downloadOnly takes care of that for us.

The File should stay in cache while sharing, assuming the share button has been pressed when the app is settled and not after a fling on a list with huge images. Even then the default cache size is so big it should be ok, but it's good to be aware of this, thanks for calling that out!

@TWiStErRob @sjudd I've tried to implement it but sadly is not treated like an image by apps like Gmail (G+ or Twitter discard it). I've send an email containing the file and after changing the file extension the image was there. Any thoughs?

That's easy to solve. Take a look at android.support.v4.content.FileProvider#getType. It tries to figure out the mime of the file based on extension. The problem is that cache files don't have extension as you mentioned, so the fallback is application/octet-stream.

Replace

<provider android:name="android.support.v4.content.FileProvider"

in your AndroidManifest.xml with a reference to this class in your codebase:

public class ImageFileProvider extends android.support.v4.content.FileProvider {
    @Override public String getType(Uri uri) { return "image/jpeg"; }
}

or if you want to be more compatible you can call super and replace "application/octet-stream" only.
or check if the Uri starts with the cache path and only then return jpeg, so you can use the same provider to share any random file.

Sharing both jpg and png from cache can be an issue, in that case you can try something like: image/*, but that's a rough case (edit: confirmed not working). The best would be obviously looking at the magic at the beginning of the file, but I think getType is called on the UI thread. You can use something like:

// don't forget to close that stream, this is just a simplied code
return toMime(new ImageHeaderParser(new AutoCloseInputStream(openFile(uri, "r"))).getType());

where toMime is your implementation, and you don't have to use ImageHeaderParser, it just conveniently exists in Glide.

@TWiStErRob Thanks, that fixed it and now the images pop out in the apps :+1:

I just try the code above to download image from server with glide. But I get error in this line:

Uri uri = FileProvider.getUriForFile(context, context.getPackageName(), result);

Below is the exception catched in Logcat:

java.lang.NullPointerException
    at android.support.v4.content.FileProvider.parsePathStrategy(FileProvider.java:560)
    at android.support.v4.content.FileProvider.getPathStrategy(FileProvider.java:534)
    at android.support.v4.content.FileProvider.getUriForFile(FileProvider.java:376)
    at com.myapp.profile.ViewImages$ShareTask.onPostExecute(ViewImages.java:64)
    at com.myapp.profile.ViewImages$ShareTask.onPostExecute(ViewImages.java:40)
    at android.os.AsyncTask.finish(AsyncTask.java:632)
    at android.os.AsyncTask.access$600(AsyncTask.java:177)
    at android.os.AsyncTask$InternalHandler.handleMessage(AsyncTask.java:645)
    at android.os.Handler.dispatchMessage(Handler.java:102)
    at android.os.Looper.loop(Looper.java:149)
    at android.app.ActivityThread.main(ActivityThread.java:5257)
    at java.lang.reflect.Method.invokeNative(Native Method)
    at java.lang.reflect.Method.invoke(Method.java:515)
    at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:793)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:609)
    at dalvik.system.NativeStart.main(Native Method)

What probably cause it?

@ixsans revalidate your FileProvider setup.

A static guess based on FileProvider.java:560

final ProviderInfo info = context.getPackageManager()
        .resolveContentProvider(authority, PackageManager.GET_META_DATA);
final XmlResourceParser in = info.loadXmlMetaData( //560
        context.getPackageManager(), META_DATA_FILE_PROVIDER_PATHS);

you're most likely using the wrong authority and it doesn't find the ContentProvider (info == null). Check if you've misplaced the tag.

If you're using Gradle/IDEA and assembleDebug your getPackageName() may have an additional .debug at the end (check android { buildTypes { debug { applicationIdSuffix). If that's the case a workaround without hardcoding the package name and making it possible to install release and debug version on the same device is to use the android:authorities="${applicationId}.share" in the manifest and

Uri uri = FileProvider.getUriForFile(context, context.getPackageName() + ".share", result);

_The .share suffix is optional, I just have a real ContentProvider which already took the package name as authority._

Thanks @TWiStErRob for the fast response, I'll try it.

@TWiStErRob I am trying to do a similar thing in an app that I am working on. The difficulty that I am running into is that I cannot reliably determine the content type of the file in the cache. Sometimes it may be jpg, png, gif, or other types. Sometimes the content type can be determined using MediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_MIMETYPE), but I have found this API to not be reliable on certain devices. A lot of times it just returns null.

Do you have any thoughts on how to get or preserve the mime type when adding a file to the cache? The server that originally serves the file has the content type, but that gets lost as soon as the file is added to the Glide cache.

@nguyer you could try to replace the networking layer with a custom one based on one you like and cache Url -> Content Type associations yourself upon receiving response and query map that when needed later. Though persistence may be an issue here. You'll need to work on it.

You can also try new ImageHeaderParser(stream).getType(), though I don't thiink it's more reliable than extractMetadata.

Consider: Do you really need the right content type? Based on Glide if the receiving application uses similar mechanisms it'll just call BitmapFactory.decode* which doesn't need content type, so always saying you have an image/jpeg may be an alternative; similarly you can try image/* or image/vnd.unknown (I just made it up).
@TepesLucian do you have any insights on this, how well did the hardcode content type work out?

Hmm, on a second thought, are you sure you have different file types? I think Glide re-encodes resized images, so if you're sharing from the RESULT cache you can only have jpeg or png.

@TWiStErRob in my case hardcoding "image/jpeg" worked because i always know that the file are images. Not particulary sure if it matters if the actual file is jpeg or png but from my testing i've seen that the common applications like gmail/g+/twitter(etc.) rely on FileProvider.getType(Uri uri) to treat the incoming file as an image rather then the type you can pass through Intent.setType("image/jpeg").

@TWiStErRob @TepesLucian In our application we have an option to save the media - which can be of various types, including GIFs and videos - to the device. To do that we copy the file into the external storage directory and send an ACTION_MEDIA_SCANNER_SCAN_FILE intent so that it is shown on the user's Gallery. If the file does not have the correct extension or the Intent does not have the correct mime type set, the media will not be added to the MediaStore and will not be shown in the gallery.

What do you think about exposing the Resource type? Any other way to fix this problem besides adding an HTTP interceptor and storing the type?

The above code is not giviing you the file from the SOURCE cache, it's a brand new image resized and transformed in a different way than the displayed image. You probably can't even hit the RESULT cache even if matching the size of the displayed image in downloadOnly and applying the same transformations (centerCrop/fitCenter/...) to it.
Note that the default cache is RESULT so if you just load the url into an image view without any extra args the original stream is not even saved to a file, so the above share code will re-download the file, that's why I put the note for using ALL above the code.

@isacssouza I think if you want to share the original media, then you have to save it first and then hand it to Glide to show, that way you have control over filename/ext for sharing. I think your use case is trying to use Glide as a networking library or file manager, which it is not. Cache is an internal and integral part of Glide, not a file storage.

What do you mean by Resource type? The type of a Resource class is held in the generic argument, and in most cases does not reflect the original source, e.g. both jpg and png files are decoded into Bitmap objects; Glide doesn't know where the file came from.

I think you are correct in your statement that we are using Glide for more than its purpose. It just so happens that downloading and caching is very well done by Glide and and we piggybacked on that.

By Resource type I meant the resource (a File in my case) mime type.

Just regarding this part "ACTION_MEDIA_SCANNER_SCAN_FILE intent so that it
is shown on the user's Gallery", i guess you can populate the media store
related tables manually.

having a list view with image and share button of each item & below is the code i'm using it to share the cache image when share button is clicked.

new DownloadImageFromCacheTask(MainActivity.this).execute(stImageUrl);

private class DownloadImageFromCacheTask extends AsyncTask<String, Void, File> {
    private final Context context;

    public DownloadImageFromCacheTask(Context context) {
        this.context = context;
    }

    @Override
    protected File doInBackground(String... params) {

        FutureTarget<File> future = Glide.with(getApplicationContext())
                .load(params[0])
                .downloadOnly(500, 500);

        File file = null;
        try {
            file = future.get();
        } catch (Exception e) {
            e.printStackTrace();
        }

        return file;
    }

    @Override
    protected void onPostExecute(File result) {
        if (result == null) {
            return ;
        }

        Uri uri = FileProvider.getUriForFile(getApplicationContext(), "com.xxx.fileprovider", result);
      share(uri);
    }

    private void share(Uri result) {

        Intent waIntent = new Intent();
        waIntent.setAction(Intent.ACTION_SEND);
        waIntent.setType("image/*");
        waIntent.putExtra(Intent.EXTRA_TEXT, "sharing from my app");
        waIntent.putExtra(Intent.EXTRA_STREAM, result);
        waIntent.setPackage("com.whatsapp");
        startActivity(waIntent);
    }
}

its working fine when the image is loaded in image view ( when the image in cache ) but taking time when image is not loaded in image view (when image is not in cache).

is ter any way to check the image is available in cache or not in glide?

@SaravanarajaGITHUB If the image is not displayed it means that Glide is either downloading it or decoding it from cache. Obviously if Glide (.load.into in adapter) doesn't have access to the image yet, then Glide (.load.downloadOnly) won't have access to the same image either. There's one thing that might solve this, and that is adding .diskCacheStrategy(ALL) to your .load() in the adapter which will cache the original image (SOURCE), so DownloadImageFromCacheTask should hit the cache (downloadOnly implies SOURCE cache as well). I mentioned this "boldly" in my original response.

Thanks for replying @TWiStErRob

Glide.with(context).
load(dish_in.getStrImageURL()). // http path
dontAnimate().dontTransform().
diskCacheStrategy(DiskCacheStrategy.ALL).
thumbnail(Glide.with(context).load(dish_in.getStrImageThumbPath())). // SD card path
dontAnimate().dontTransform().    
placeholder(context.getResources().
getDrawable(R.drawable.app_image_based_bg)).
into(ivDish);

this was the line in adapter was before posted my issue, as i said its working fine when my HD image is downloaded but not for undownloaded image . In my item list view when i tap the share button i trying to fetch the un-downloaded HD image from cache I know that makes some abnormal response,so i want to disable the share button until HD data is viewed in Image View (downloaded image will be in cache ). is their any way to check the HD image is set to image view Or respective image is in cache?

@SaravanarajaGITHUB so it's just the case of the image is not yet downloaded, you have to wait for it to finish before getting a File reference to it. Double check that you're passing the correct url to the AsyncTask, that can also easily cause a cache miss.

Also consider not showing the share button until it's loaded on the basis of "Why would anyone want to share something they don't see?":

shareButton.setVisibility(INVISIBLE);
Glide
        .with...
        .listener(new RequestListener<String, Bitmap>() {
            @Override public boolean onException(Exception e, String model, Target<Bitmap> target, boolean isFirstResource) {
                return false;
            }
            @Override public boolean onResourceReady(Bitmap resource, String model, Target<Bitmap> target, boolean isFromMemoryCache, boolean isFirstResource) {
                shareButton.setVisibility(VISIBLE);
                return false;
            }
        })

which means you won't have this problem any more.

Also note that dontAnimate().dontTransform(). is there twice, if you meant it for the thumbnail it went outside the parentheses. Also you can just say: .placeholder(R.drawable.app_image_based_bg).

its working .!! Changed above code according to my requirement. Thanks for your Time @TWiStErRob

i have successfully implemented this method, but the problem is when i share to other applications, sometimes failed because some applications has problem cannot read the cursor or something

An UriResolver from this link
https://github.com/Repo-IEEEsb/IeeeManager-Android/blob/master/mobile/src/main/java/es/ieeesb/ieeemanager/tools/UriResolver.java
will fail too to resolve the Uri that passed from our share intent

The whole point of a ContentProvider is to forget Files exists and work with Cursors and Streams only. The support-v4 FileProvider bridges the gap. The Uri you get should be a content:// Uri so it goes into getDataColumn, but the FileProvider doesn't handle the _data column. Which is because that UriResolver is used to work around the Content system: it tries to get the File path from a content Uri, but even if it manages that the actual File would be unreadable due to file system permissions. Report the bug to those application that they should be using ContentResolver.openInputStream(Uri) or similar for the object retrieved from EXTRA_STREAM. I think the name should be pretty clear there: that extra represents a stream and not a File. This will benefit everyone!

@TWiStErRob Thank you for your clear explanation. Some applications that i tested, even that has many users at Google Play failed to handle this. I will try to report bug to them :)

Hi, I follow your comment https://github.com/bumptech/glide/issues/459#issuecomment-99960446 and able to get the content uri but when sharing through Facebook Messenger Expression, it failed. The reason is the cached file the the content uri used has no extension (if .0 is not counted as ext). But if I manually duplicated the file in root explorer and add .jpg to the file, it works. Is there way we can add the extension to the cache or is there any other way around that?

@rathasok7 https://github.com/bumptech/glide/issues/459#issuecomment-100238254?
(You may also try manipulating the returned cursor and set a file name yourself.)

sorry the comment you made on May 8, 2015 above about using ShareTask to get the uri.

459#issuecomment-99960446

@TWiStErRob let me rephrase the question. I am able to get the content uri from the ShareTask. But when using that uri to share using Facebook Messenger Platform, it said it cannot process the file. After some trail and error, i found out that the root of the issue is because the cache file has no .extension (it use generic .0 as extension). To test this, i duplicate one of the cache file and add .jpg to it, then i use fileprovider to get uri from that cache file with .jpg extension and share through Facebook Messenger Expression. and it works. So my question is: is there any fix around this issue? is there way we can add extension to the cache file?

No, you can't modify the file, and the comment I linked is a potential solution. Another potential (more hacky) solution is that you "lie" about the file name by overriding query and change the DISPLAY_NAME column in the cursor before returning it to have .jpg at the end.

@TWiStErRob sorry.. can you be more specific? am new to android.

@rathasok7 updated above comment to be more explicit and here's the hacky workaround I was talking about (not tested):

public class ImageFileProvider extends android.support.v4.content.FileProvider {
    @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
        Cursor cursor = super.query(uri, projection, selection, selectionArgs, sortOrder);
        Cursor override = copyCursorWithChangedDisplayName((MatrixCursor)cursor);
        cursor.close();
        return override;
    }
    private MatrixCursor copyCursorWithChangedDisplayName(MatrixCursor original) {
        MatrixCursor override = new MatrixCursor(original.getColumnNames(), original.getCount()/* == 1 */);
        if (original.moveToFirst()) {
            override.addRow(copyCurrentRowWithChangedDisplayName(original));
        }
        return override;
    }
    /** Based on super.query() */
    private Object[] copyCurrentRowWithChangedDisplayName(MatrixCursor original) {
        String[] columns = original.getColumnNames();
        Object[] row = new Object[columns.length];
        int i = 0;
        for (String column : columns) {
            int columnIndex = original.getColumnIndexOrThrow(column);
            Object value = null;
            if (OpenableColumns.DISPLAY_NAME.equals(column)) {
                value = replaceExtension(original.getString(columnIndex), ".jpg");
            } else if (OpenableColumns.SIZE.equals(column)) {
                value = original.getLong(columnIndex);
            }
            row[i++] = value;
        }
        return row;
    }

    private String replaceExtension(String name, String ext) {
        if (!name.endsWith(ext)) {
            name = name + ext;
        }
        return name;
    }
}

@TWiStErRob I will try your solution. Many Thanks.

@TWiStErRob ok. now I can get the file name with extension using getContentResolver().query(); Facebook Expression need uri to share. Do I have to to create a new uri? What do you mean about lying the system?

// Below is the code we need to use to share to Facebook Messenger Expression
// contentUri points to the content being shared to Messenger
ShareToMessengerParams shareToMessengerParams =
ShareToMessengerParams.newBuilder(uri, "image/*")
.build();

@TWiStErRob Hi, I have choose other path. First I load the image from remote server using glide.into SimpleTarget, then manually save the file to local storage. When sharing, i just create a uri from the local file.
Thanks for your support. This is a great framework.

@TWiStErRob Hey using this solution I was able to share jpeg and png files and I also tried changing image/jpeg to image/* ....but this is not working for gif file. Can you please suggest a solution by which I can share gif file as well, I am able to retrieve the uri using your shareTask example.

public class ImageFileProvider extends android.support.v4.content.FileProvider {
    @Override public String getType(Uri uri) { return "image/jpeg"; }
}

Thanks,

@TWiStErRob can you please explain this line in detail and there params
return toMime(new ImageHeaderParser(new AutoCloseInputStream(openFile(uri, "r"))).getType());
Is it like in getType() method I need to write this instead of return image/* or image/jpeg?

Also in that line what the openFile method is, I mean what param r is indicating.

Yes, it's a replacement for the hardcode mime type. And that means openFile is the method in FileProvider. "r": https://developer.android.com/reference/android/content/ContentProvider.html#openFile(android.net.Uri,%20java.lang.String)

@TWiStErRob thanks for the explanation but after implementing it, the error I am getting with normal png and jpeg image as well as gif is "The file format is not supported" trying with "WhatsApp".

Although when I write the line as return image/jpeg or image/*....it at shares png and jpeg file. Only issue with gif.

Can you please suggest something else for this.

@TWiStErRob Thanks, this really helped a lot

Just wanted to share a couple of classes that solved sharing for me.
You would need to setup FileProvider, of course, and be sure to have this set in paths

<cache-path
        name="share"
        path="image_manager_disk_cache"
        tools:path="DiskCache.Factory.DEFAULT_DISK_CACHE_DIR"/>

Then you would load an image as Bitmap, for example like this 鈥撀爄t would load images from list one after another in case crop is missing, until it gets an image, and then it would call a callback with a bitmap

 open fun loadAsBitmap(urls: List<String>, onLoaded: (Bitmap) -> Unit) {
        Glide.with(baseContext)
                .load(urls[0])
                .asBitmap().fitCenter()
                .listener(object : RequestListener<String, Bitmap> {

                    override fun onException(e: Exception?,
                                             model: String?,
                                             target: Target<Bitmap>?,
                                             isFirstResource: Boolean): Boolean {
                        if (urls.size > 1) {
                            loadAsBitmap(urls.drop(1), onLoaded)
                        }

                        return true
                    }

                    override fun onResourceReady(bitmap: Bitmap?,
                                                 model: String?,
                                                 target: Target<Bitmap>?,
                                                 isFromMemoryCache: Boolean,
                                                 isFirstResource: Boolean): Boolean {
                        if (bitmap != null) {
                            onLoaded(bitmap)
                        }
                        return true
                    }

                })
                .into(object : SimpleTarget<Bitmap>() {
                    override fun onResourceReady(bitmap: Bitmap?, glideAnimation: GlideAnimation<in Bitmap>?) {

                    }

                })
    }

Next step would be to save that bitmap to a temporary file. I have only jpegs so the class is super simple, but if you have pngs, gifs or anything else you'll need to extend it somehow, maybe by getting a mime type from a bitmap, I don't know:

@Singleton
class BitmapSaver @Inject constructor(
        private val baseContext: BaseContext
) {

    companion object {
        val SHARE_IMAGE_FILENAME = "share_image.jpg"
    }

    fun saveBitmapToCacheFile(bitmap: Bitmap, fileName: String) {
        val cacheFile = File(baseContext.cacheDir, "images")
        cacheFile.mkdirs()
        val imageFile = File(cacheFile, fileName)
        val stream = FileOutputStream(imageFile)
        bitmap.compress(Bitmap.CompressFormat.JPEG, 100, stream)
        stream.close()
    }

    fun uriForCacheFile(fileName: String): Uri? {
        val cacheFile = File(baseContext.cacheDir, "images")
        return FileProvider.getUriForFile(baseContext, BuildConfig.FILES_AUTHORITY, File(cacheFile, fileName))
    }
}

And the last step would be to put this all together and show a share dialog. I have a helper class for that, but the idea is the usual 鈥撀爕ou construct a ShareCompat.IntentBuilder for a stream of uri.

imageLoader.loadAsBitmap(listOf(photo.full_image_url)) {
            bitmapSaver.saveBitmapToCacheFile(it, BitmapSaver.SHARE_IMAGE_FILENAME)
            val uri = bitmapSaver.uriForCacheFile(BitmapSaver.SHARE_IMAGE_FILENAME)
            uri?.let {
                uiNavigator.showShareLocalImageDialog(uri,
                                                      resources.getString(R.string.share_photo_message),
                                                      R.string.share)
            }
        }

Main benefit of all that is that you have a proper file extension and proper file, and all social networks just catch that you are sharing a jpeg image and work fine with it.

@aaverin who's cleaning up after this?

  • You're leaking a potentially huge bitmap (simpletarget loads original size); which is only freed when Base context dies.
  • You're also leaking a potentially large (100) JPEG File which won't be deleted until uninstall or another share.

You're also:

  • doing I/O on UI thread.
  • using the wrong path in file provider
  • looks like you wanted to put alien files into Glide's internal cache, which may be unsafe (unknown/undocumented bevavior)
  • reencoding everything as JPEG. You cannot reencode a PNG or GIF without losing quality, even with 100. And you lose the ability to share animated GIF. Even for your JPEG-only case it would be simpler.

At this point you could've just downloaded the original file with any networking lib and shared that: simpler and more flexible.

@TWiStErRob I'm trying your ShareTask but I don't get the image is showed. My purpose is give to SUGGEST_COLUMN_RESULT_CARD_IMAGE where the cached image is for showing it in global search for Android TV, any suggestions?

Thanks in advance.

You need to put the FileProvider Uri in the column, not the file path, and the file should exists. Check the flow step by step in debug.

@TWiStErRob Thank you for your quick response. Let me show you the code (it's Kotlin but I think you won't have problems with it).

    override fun doInBackground(vararg params: String): File? {
        val url = params[0] // should be easy to extend to share multiple images at once
        try {
            return Glide
                    .with(context)
                    .load(url)
                    .downloadOnly(SIZE_ORIGINAL, SIZE_ORIGINAL)
                    .get() // needs to be called on background thread
        } catch (ex: Exception) {
            Log.w("SHARE", "Sharing $url failed", ex)
            return null
        }
    }

    override fun onPostExecute(file: File?) {
        if (file == null) {
            return
        }
        uri = FileProvider.getUriForFile(context, AUTHORITY, file)

        VideoDatabaseOpenHelper.addVideoForDeepLink2(video, uri)
    }

And this is the method where the map is done:

fun addVideoForDeepLink2(video: ItaasVodCatalogItem, uri: Uri?) {
            val MILLISECONDS = 1000
            val initialValues = ContentValues()
            initialValues.put(VideoDatabaseHandler.COLUMN_ID, video.id)
            initialValues.put(VideoDatabaseHandler.COLUMN_NAME, video.title)
            initialValues.put(VideoDatabaseHandler.COLUMN_DATA_TYPE, "video/mp4")
            initialValues.put(VideoDatabaseHandler.COLUMN_PRODUCTION_YEAR, "2015")
            initialValues.put(VideoDatabaseHandler.COLUMN_DURATION, video.duration * MILLISECONDS)
            initialValues.put(VideoDatabaseHandler.COLUMN_CARD_IMG, uri.toString())
            initialValues.put(VideoDatabaseHandler.COLUMN_ACTION, "GLOBAL_SEARCH")
            mDatabase?.insert(VideoDatabaseHandler.LEANBACK_TABLE, null, initialValues)
        }

If I map the image url directly, the image is shown but with URI doesn`t work yet. I attach database info:
image

Thank you.

EDIT: Ok, now I know that my file doesn't exist, the next condition is false:
new File(uri.getPath());
Any suggestions why the file doesn't exist?

Hi,

I've tried another approach, instead of having database table, I've created a MatrixCursor so I can pass the uri. Besides, I've checked that the cached image from content uri exists (I pass the uri to ImageView and the image is showed). But the cover images are not showed in global search yet.

<manifest>
    <application>
        <provider
            android:name=".common.data.ImageFileProvider"
            android:authorities="com.telefonica.video.stv.fileprovider"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/file_paths" />
        </provider>
    </application>

</manifest>

file_paths.xml:

<paths xmlns:tools="http://schemas.android.com/tools">
    <cache-path
        name="share"
        path="image_manager_disk_cache"
        tools:path="DiskCache.Factory.DEFAULT_DISK_CACHE_DIR" />
</paths>

Method where I fill cursor with data:

    fun addVideoForDeepLink4(video: ItaasVodCatalogItem, contentUri: Uri?) {
        var row: MatrixCursor.RowBuilder = matrixCursor.newRow()

        row.add(VideoDatabaseHandler.COLUMN_CARD_IMG, contentUri)
                .add(VideoDatabaseHandler.COLUMN_ID, video.id)
                .add(VideoDatabaseHandler.COLUMN_NAME, video.title)
                .add(VideoDatabaseHandler.COLUMN_DATA_TYPE, "video/mp4")
                .add(VideoDatabaseHandler.COLUMN_DURATION, video.duration * DateUtils.SECOND_IN_MILLIS)
                .add(VideoDatabaseHandler.COLUMN_PRODUCTION_YEAR, "1990")
                .add(VideoDatabaseHandler.COLUMN_ACTION, "GLOBAL_SEARCH")
    }

Where COLUMN_CARD_IMG = SearchManager.SUGGEST_COLUMN_RESULT_CARD_IMAGE

Ok, finally I found what was missing. I'm implementing the global search for Android TV and it is necessary to grant uri permission to Google app for Android TV. So in onPostExecute method:

  override fun onPostExecute(file: File?) {
            if (file == null) {
                return
            }
            if (file.exists()) {
                contentUri = FileProvider.getUriForFile(context, AUTHORITY, file)
                context.grantUriPermission(
                       "com.google.android.katniss", //the package of Google app for Android TV: https://play.google.com/store/apps/details?id=com.google.android.katniss&hl=es
                        contentUri,
                        Intent.FLAG_GRANT_READ_URI_PERMISSION
                )
                addVideoForDeepLink(video, contentUri.toString())
            }
        }

P.S. It is not necessary to use a MatrixCursor for passing the uri, we can use a SQLite database and we can pass the uri as a string:

 initialValues.put(SearchManager.SUGGEST_COLUMN_RESULT_CARD_IMAGE, uri.toString()) //kotlin code

For those who fails to share cached image with apps (such as Google Keep, WeChat Moments) that rely on file type (which FileProvider cannot guess because the cache file has no file extension), here is a working subclass of FileProvider (with Glide 4) that deals with this perfectly (Thanks to https://github.com/bumptech/glide/issues/459#issuecomment-100238254):

package com.yourapplication;

import android.net.Uri;
import android.os.ParcelFileDescriptor;
import android.support.v4.content.FileProvider;
import android.text.TextUtils;
import android.webkit.MimeTypeMap;

import com.bumptech.glide.load.ImageHeaderParser;
import com.bumptech.glide.load.resource.bitmap.DefaultImageHeaderParser;

import java.io.FileInputStream;

public class ImageTypeFileProvider extends FileProvider {

    private ImageHeaderParser mImageHeaderParser = new DefaultImageHeaderParser();

    @Override
    public String getType(Uri uri) {

        String type = super.getType(uri);
        if (!TextUtils.equals(type, "application/octet-stream")) {
            return type;
        }

        try {
            ParcelFileDescriptor parcelFileDescriptor = openFile(uri, "r");
            if (parcelFileDescriptor == null) {
                return type;
            }
            try {
                FileInputStream fileInputStream = new FileInputStream(
                        parcelFileDescriptor.getFileDescriptor());
                try {
                    ImageHeaderParser.ImageType imageType = mImageHeaderParser.getType(
                            fileInputStream);
                    type = getTypeFromImageType(imageType, type);
                } finally {
                    fileInputStream.close();
                }
            } finally {
                parcelFileDescriptor.close();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return type;
    }

    private static String getTypeFromImageType(ImageHeaderParser.ImageType imageType,
                                               String defaultType) {
        String extension;
        switch (imageType) {
            case GIF:
                extension = "gif";
                break;
            case JPEG:
                extension = "jpg";
                break;
            case PNG_A:
            case PNG:
                extension = "png";
                break;
            case WEBP_A:
            case WEBP:
                extension = "webp";
                break;
            default:
                return defaultType;
        }
        // See FileProvider.getType(Uri)
        return MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
    }
}

Hi! I wanted to share image from cache in Glide 4.0.0-RC1. Please see this: https://github.com/bumptech/glide/issues/2069

@DreaminginCodeZH
There isn't a static method FileProvider.getType(Uri) for FileProvider, so how can I get the mime type of the image ?
Because, I need to set the type for intent like intent.setType("image/png")

hi, I want to check I have download the file or not for use v4 Glide.downloaOnly. How do I do?

@TWiStErRob thanks for the explanation but after implementing it, the error I am getting with normal png and jpeg image as well as gif is "The file format is not supported" trying with "WhatsApp".

Although when I write the line as return image/jpeg or image/*....it at shares png and jpeg file. Only issue with gif.

Can you please suggest something else for this.

@DevavrataSharma

Can you solve it? I have the same problem! In the physical phone android 8 works in the emulator android 8 gives the same error.

@ghuiii FileProvider.getType is the method you're overriding, it doesn't give you the ability to determine mime type, it gives you the ability to communicate the mime type to consumers of your content provider. What @DreaminginCodeZH is referring to is the core of the original super implementation.

@wvmoreira re "where is DefaultImageHeaderParser?"

  • Glide 4: com.bumptech.glide.load.resource.bitmap.DefaultImageHeaderParser
  • Glide 3: com.bumptech.glide.load.resource.bitmap.ImageHeaderParser
  • Glide 2: com.bumptech.glide.resize.load.ImageHeaderParser

@wvmoreira have you tried https://github.com/bumptech/glide/issues/459#issuecomment-309262076? it looks really promising (haven't run it, just read it).

For those who fails to share cached image with apps (such as Google Keep, WeChat Moments) that rely on file type (which FileProvider cannot guess because the cache file has no file extension), here is a working subclass of FileProvider (with Glide 4) that deals with this perfectly (Thanks to #459 (comment)):

package com.yourapplication;

import android.net.Uri;
import android.os.ParcelFileDescriptor;
import android.support.v4.content.FileProvider;
import android.text.TextUtils;
import android.webkit.MimeTypeMap;

import com.bumptech.glide.load.ImageHeaderParser;
import com.bumptech.glide.load.resource.bitmap.DefaultImageHeaderParser;

import java.io.FileInputStream;

public class ImageTypeFileProvider extends FileProvider {

    private ImageHeaderParser mImageHeaderParser = new DefaultImageHeaderParser();

    @Override
    public String getType(Uri uri) {

        String type = super.getType(uri);
        if (!TextUtils.equals(type, "application/octet-stream")) {
            return type;
        }

        try {
            ParcelFileDescriptor parcelFileDescriptor = openFile(uri, "r");
            if (parcelFileDescriptor == null) {
                return type;
            }
            try {
                FileInputStream fileInputStream = new FileInputStream(
                        parcelFileDescriptor.getFileDescriptor());
                try {
                    ImageHeaderParser.ImageType imageType = mImageHeaderParser.getType(
                            fileInputStream);
                    type = getTypeFromImageType(imageType, type);
                } finally {
                    fileInputStream.close();
                }
            } finally {
                parcelFileDescriptor.close();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return type;
    }

    private static String getTypeFromImageType(ImageHeaderParser.ImageType imageType,
                                               String defaultType) {
        String extension;
        switch (imageType) {
            case GIF:
                extension = "gif";
                break;
            case JPEG:
                extension = "jpg";
                break;
            case PNG_A:
            case PNG:
                extension = "png";
                break;
            case WEBP_A:
            case WEBP:
                extension = "webp";
                break;
            default:
                return defaultType;
        }
        // See FileProvider.getType(Uri)
        return MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
    }
}

Used it for custom file provider.
@TWiStErRob this does not work with current version of WhatsApp (For telegram and other apps it works fine), anything else which I need to do?

Was this page helpful?
0 / 5 - 0 ratings

Related issues

Anton111111 picture Anton111111  路  3Comments

sergeyfitis picture sergeyfitis  路  3Comments

r4m1n picture r4m1n  路  3Comments

kenneth2008 picture kenneth2008  路  3Comments

sant527 picture sant527  路  3Comments