Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Load Encrypted Image from File Using Glide #1498

Closed
aritraroy opened this issue Oct 4, 2016 · 11 comments
Closed

Load Encrypted Image from File Using Glide #1498

aritraroy opened this issue Oct 4, 2016 · 11 comments
Labels

Comments

@aritraroy
Copy link

aritraroy commented Oct 4, 2016

I have been using Glide in several projects, and have always been a fan of it.

In my current project, I am implementing a small feature where I need to show some encrypted images in a RecyclerView. I would like to use Glide but I can not do something like this,

Glide.with(this).load(encryptedImageFile).into(imageView);

as I need to decrypt to images first and then set them.

I already have an AsyncTask that decrypts those images and gives me a Bitmap to set in an ImageView.

@Override
    public void onBindViewHolder(MediaViewHolder holder, int position) {
        super.onBindViewHolder(holder, position);

        EncryptedFileContainer encryptedFileContainer = getItem(position);

        if (encryptedFileContainer != null) {
            EncryptedFileDBModel encryptedFileDBModel = encryptedFileContainer.getEncryptedFileDBModel();
            holder.mImage.setTag(encryptedFileDBModel.getOriginalFileName());

            new BindEncryptedThumbnail(holder, encryptedFileContainer).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
        }
    }

and here is the AsyncTask,

static class BindEncryptedThumbnail extends AsyncTask<Object, Void, Bitmap> {
    private EncryptedFileContainer mEncryptedFileContainer;
    private MediaViewHolder mHolder;

    public BindEncryptedThumbnail(MediaViewHolder viewHolder, EncryptedFileContainer encryptedFileContainer) {
        this.mHolder = viewHolder;
        this.mEncryptedFileContainer = encryptedFileContainer;
    }

    protected Bitmap doInBackground(Object... objects) {
        if (mEncryptedFileContainer.getEncryptedFile() != null) {
            return mEncryptedFileContainer.getDecryptedThumb().getThumb(100);
        } else {
            return null;
        }
    }

    protected void onPostExecute(Bitmap thumbnail) {
        String name = (String) mHolder.mImage.getTag();
        EncryptedFile encryptedFile = mEncryptedFileContainer.getEncryptedFile();
        EncryptedFileDBModel encryptedFileDBModel = mEncryptedFileContainer.getEncryptedFileDBModel();
        if (encryptedFile != null && StringUtils.equals(name, encryptedFileDBModel.getOriginalFileName())
                && (thumbnail != null) && (mHolder.mImage != null)) {
            mHolder.mImage.setImageBitmap(thumbnail);
        } else {
            mHolder.mImage.setImageDrawable(ResourceUtils.getDrawable(R.drawable.img_placeholder));
        }
    }
}

and the method to decrypt the thumbnail and get the Bitmap,

public Bitmap getThumb(int thumbnailSize) {
        if ((!mThumbnailCreated) && (mThumbnailBitmap == null)) {
            CipherInputStream streamThumb = readStream();
            this.mThumbnailBitmap = ThumbnailEncryptionFactory
                    .getThumbnailfromStream(streamThumb, thumbnailSize);
        }
        return mThumbnailBitmap;
    }

Everything works fine. The images are decrypted and set correctly. But if there are a lot of images, then the memory footprint shoot up high and there are several GCs resulting in a slightly laggy scrolling.

I would love to use Glide here as well. Is there a way I can use Glide to do this if I pass it the Bitmap or the CipherInputStream with the decrypted bytes? Can that be done using ResourceDecoder in Glide or I need something else?

A detailed answer would be really helpful.

@TWiStErRob
Copy link
Collaborator

TWiStErRob commented Oct 5, 2016

#912 has some details, but not full.

Your implementation is a mini image loader, the only thing missing is the ability to cancel previous decrypts when the item is recycled and rebound.

You have two options:

  1. Use a ResourceDecoder which is useful if you want to cache the original image in an encrypted form. SOURCE and RESULT cache read by same cacheDecoder #707 may stand in the way if you can't tell if the image is a encrypted or not.
  2. Use a custom model loader/fetcher Glide...load(mEncryptedFileContainer.getEncryptedFile()).into(holder.mImage). The possible drawback is that you may want the original source file and transformed result image encrypted in cache.

Based on your names you are dealing with encrypted local files, so caching shouldn't be an issue. Mostly because you don't need to cache local files as they would be just duped in Glide cache. Based on all this and your code sample I think you need something like this (using inner class notation, but you can and should extract separate classes:

Glide
    .with(context)
    .load(getItem(position)) // custom load using registered factory
    .diskCacheStrategy(DiskCacheStrategy.NONE) // file already on disk, don't save it again
    //.override(100, 100) // getThumb(int thumbnailSize), but I suggest you let Glide figure the size out
    .placeholder(R.drawable.img_placeholder)
    .error(R.drawable.img_unavailable)
    .into(holder.mImage);

// in GlideModule, see https://github.com/bumptech/glide/wiki/Configuration
glide.register(EncryptedFileContainer.class, InputStream.class, new ModelLoaderFactory<EncryptedFileContainer, InputStream>() {
    @Override public ModelLoader<EncryptedFileContainer, InputStream> build(Context context, GenericLoaderFactory factories) {
        return new StreamModelLoader<EncryptedFileContainer>() {
            @Override public DataFetcher<InputStream> getResourceFetcher(final EncryptedFileContainer model, int width, int height) {
                return new DataFetcher<InputStream>() {
                    private InputStream stream;
                    @Override public InputStream loadData(Priority priority) throws Exception {
                        // prefer throwing an exception to returning null!
                        return stream = model.getDecryptedThumb().readStream();
                    }
                    @Override public void cleanup() {
                        cancel();
                    }
                    @Override public @NonNull String getId() {
                        return model.getEncryptedFileDBModel().getOriginalFileName();
                    }
                    @Override public void cancel() {
                        if (stream != null) {
                            try {
                                stream.close(); // interrupts decode if any
                                stream = null;
                            } catch (IOException ignore) {
                            }
                        }
                    }
                };
            }
        };
    }
    @Override public void teardown() {
        // no resources to free in factory
    }
});

Let me know if this is sufficient, or if I made a false assumption somewhere.

@TWiStErRob
Copy link
Collaborator

TWiStErRob commented Oct 5, 2016

Crosslink: http://stackoverflow.com/q/39852725/253468

Note: based on the SO question you could use #122, but you'd still have the memory issues.

@aritraroy
Copy link
Author

Hi,

I have tried your code and it works. The images are decrypted and set using Glide perfectly. Thanks a lot for it.

But there are some small improvements, that need to be done,

  1. Initially it was not caching the images in the disk and was decrypting the images every single time. I changed the cache strategy to diskCacheStrategy(DiskCacheStrategy.RESULT) and it then worked. Is it okay?

  2. Whenever I am scrolling the grid of images, the memory usage stays stable but the CPU usage still spikes up high upto 80% sometimes. As soon as I stop scrolling, the CPU usage settles down again. Is it expected? If not, how can this be improved?

  3. There are some encrypted images which are GIFs and others are JPGs, PNGs, etc. In case of normal images, the GIF images get animated automatically. But now the encrypted GIF images are not animating. I think Glide is unable to understand, which encrypted image is GIF and which is not, so it treats all encrypted images as non-GIF and they don't animate. How can this be solved?

  4. At some portions of my app, I want to show these encrypted images in full screen mode. It works fine with Glide, but it is using the module registered and is loading the low-quality thumbnail as it has been mentioned in the module,

@Override public InputStream loadData(Priority priority) throws Exception {
                        // prefer throwing an exception to returning null!
                        return stream = model.getDecryptedThumb().readStream();
                    }

How can I tell Glide to use a different module specification for those full screen images? The Model would be same for both the cases, i.e. EncryptedFileContainer.

  1. Slightly, off-topic, but is there a callback when Glide starts loading an image and another callback when it completes loading it? So that I can show/hide a custom progress while the loading is in progress. I don't need any progress percentage, just these two callbacks are enough.

Thanks again. :-)

@TWiStErRob
Copy link
Collaborator

TWiStErRob commented Oct 5, 2016

  1. Yes, NONE was intentional. There are two reasons: if the source image is encrypted there must be a reason, so you have to remember that Glide cache is not encrypted. This applies to both SOURCE and RESULT cache. Since this solution wraps the original stream and makes it decrypted Glide never knows it was encrypted so saving to SOURCE cache would also work. SOURCE and RESULT cache read by same cacheDecoder #707 I mentioned only applies to decoders. The other reason is that you should use caching for local files, because it just copies to Glide cache. From storage to storage, there's no gain, only wasting disk space for the user. Of course RESULT caching small thumbnails is a good idea.

  2. I don't know what you expect... images are not free, a lot of memory operations are involved to decode them and also to render them. Memory cache should kick in if you scroll back and forth, enabling RESULT like you did in 1) should also tremendously help with this, because it's easier for the CPU to decode a small resized decrypted stream than a big encrypted one. Worth mentioning to make sure to do the decryption on a stream, because decrypting to a byte[] and then wrapping that in a stream is not efficient.

  3. That shouldn't happen, all you changed is the source of the stream, the decoder is intact. Make sure you don't have .asBitmap() also. I think getDecryptedThumb() may be contianing only the first frame in case of GIF?

  4. It should be obvious that you need to change the fetcher to use getDecryptedImage() or whatever it's called. The problem is that for one model class you can't have two model loader factories. You have two options:

  • create a new model class and register that:

    class Image { EncryptedFileContainer model; enum { Thumb, Full } loadType; }

    and switch on loadType in the fetcher.

  • if model.getDecryptedThumb() and model.getDecryptedImage() are different types, maybe register them as custom models in Glide

progress.setVisibility(VISIBLE);
Glide.with...
.listener(both methods: { progress.setVisibility(GONE); return false; })
...;

@aritraroy
Copy link
Author

  1. So, with RESULT cache, are the images encrypted inside the cache as well, or do I need to switch to SOURCE cache or something else entirely?

  2. This is fine now.

  3. The problem here is that the thumbnail itself contains only one frame. Will fix it.

  4. Thanks for this idea. Will implement this soon.

  5. Thanks for the tip as well.

@TWiStErRob
Copy link
Collaborator

you have to remember that Glide cache is not encrypted. This applies to both SOURCE and RESULT cache.

So, no, it's not encrypted. Having an encrypted cache needs you to replace some of decoder, encoder, and sourceEncoder, check out #735. I keep assuming that your original files are on local storage, so you could simply use NONE.

@aritraroy
Copy link
Author

Thanks. I had initially used NONE, but that is decrypting the thumbnails every time. The files are locally available, but it is not about just reading the files, they need to be decrypted as well. So, a cache is needed for better performance.

@TWiStErRob
Copy link
Collaborator

Mind you that if you go ahead and enable Glide cache encryption (#735), you won't be in a much better position than using NONE.

I'll close this now since the original issue is solved.
May I ask why the images need encryption?

@aritraroy
Copy link
Author

I was also thinking of the same. Better I keep the cache unencrypted. Actually, there are some important documents of the users which I need to download and store locally. And as they are quite sensitive, I need to keep them encrypted.

Now Glide solves the issue loading encrypted images very well. Thanks to you. :-)

@idish
Copy link

idish commented Feb 6, 2017

Mind you that if you go ahead and enable Glide cache encryption (#735), you won't be in a much better position than using NONE.

I'm in the same situation as the OP. (i.e: saving the encrypted image files locally)

The major problem for me is that the encrypted image files are full screen sized, and I want to display them in a grid, so the decryption process from the original big image takes pretty heavy time to do for Glide.
And so I am looking for a way to make use of the disk caching properly.

I could add decoder and encoder for the disk caching, but I can see that you still suggest to not save anything using the disk cache.

I think that if I would use the disk cache to save the result image (default behavior), the decryption process from the disk cache would be much more efficient than encrypting it from the original big image file, as the disk cache has a "thumbnail" encrypted version (smaller in size), isn't it?

Maybe I misunderstood your original statement, or the OP isn't really in the same situation as mine.

@idish
Copy link

idish commented Feb 13, 2017

Mind you that if you go ahead and enable Glide cache encryption (#735), you won't be in a much better position than using NONE.

I'll close this now since the original issue is solved.
May I ask why the images need encryption?

I'm really looking for some help please :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

3 participants