Search code examples
androidandroid-recyclerviewbackgroundid3

Displaying Coverart (from id3) in Recyclerview in background


I am working on an app that uses a Recyclerview to display mp3 files, providing its cover art image along with other info. It works but is slow once it starts dealing with a dozen or more cover arts to retrieve, as I am currently doing this from the id3 on the main thread, which I know is not a good idea. Ideally, I would work with placeholders so that the images can be added as they become available. I've been looking into moving the retrieval to a background thread and have looked at different options: AsyncTask, Service, WorkManager. AsyncTask seems not to be the way to go as I face memory leaks (I need context to retrieve the cover art through MetadataRetriever). So I am leaning away from that. Yet I am struggling to figure out which approach is best in my case.

From what I understand I need to find an approach that allows multithreading and also a means to cancel the retrieval in case the user has already moved on (scrolling or navigating away). I am already using Glide, which I understand should help with the caching. I know I could rework the whole approach and provide the cover art as images separately, but that seems a last resort to me, as I would rather not weigh down the app with even more data.

The current version of the app is here (please note it will not run as I cannot openly divulge certain aspects). I am retrieving the cover art as follows (on the main thread):

static public Bitmap getCoverArt(Uri medUri, Context ctxt) {

    MediaMetadataRetriever mmr = new MediaMetadataRetriever();
    mmr.setDataSource(ctxt, medUri);

    byte[] data = mmr.getEmbeddedPicture();

    if (data != null) {
        return BitmapFactory.decodeByteArray(data, 0, data.length);
    } else {
        return null;
    }
}

I've found many examples with AsyncTask or just keeping the MetaDataRetriever on the main thread, but have yet to find an example that enables a dozen or more cover arts to be retrieved without slowing down the main thread. I would appreciate any help and pointers.


Solution

  • It turns out it does work with AsyncTask, as long as it is not a class onto itself but setup and called from a class with context. Here is a whittled down version of my approach (I am calling this from within my Adapter.):

    //set up titles and placeholder image so we needn't wait on the image to load
    titleTv.setText(selectedMed.getTitle());
    subtitleTv.setText(selectedMed.getSubtitle());
    imageIv.setImageResource(R.drawable.ic_launcher_foreground);
    imageIv.setAlpha((float) 0.2);
    final long[] duration = new long[1];
    
    //a Caching system that helps reduce the amount of loading needed. See: https://github.com/cbonan/BitmapFun?files=1
    if (lruCacheManager.getBitmapFromMemCache(selectedMed.getId() + position) != null) {
    

    //is there an earlier cached image to reuse? imageIv.setImageBitmap(lruCacheManager.getBitmapFromMemCache(selectedMed.getId() + position)); imageIv.setAlpha((float) 1.0);

            titleTv.setVisibility(View.GONE);
            subtitleTv.setVisibility(View.GONE);
        } else {
    
            //time to load and show the image. For good measure, the duration is also queried, as this also needs the setDataSource which causes slow down
            new AsyncTask<Uri, Void, Bitmap>() {
                @Override
                protected Bitmap doInBackground(Uri... uris) {
                    MediaMetadataRetriever mmr = new MediaMetadataRetriever();
                    mmr.setDataSource(ctxt, medUri);
                    byte[] data = mmr.getEmbeddedPicture();
                    Log.v(TAG, "async data: " + Arrays.toString(data));
    
                    String durationStr = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION);
                    duration[0] = Long.parseLong(durationStr);
    
                    if (data != null) {
                        InputStream is = new ByteArrayInputStream(mmr.getEmbeddedPicture());
                        return BitmapFactory.decodeStream(is);
                    } else {
                        return null;
                    }
                }
    
                @Override
                protected void onPostExecute(Bitmap bitmap) {
                    super.onPostExecute(bitmap);
    
                    durationTv.setVisibility(View.VISIBLE);
                    durationTv.setText(getDisplayTime(duration[0], false));
    
                    if (bitmap != null) {
                        imageIv.setImageBitmap(bitmap);
                        imageIv.setAlpha((float) 1.0);
    
                        titleTv.setVisibility(View.GONE);
                        subtitleTv.setVisibility(View.GONE);
                    } else {
                        titleTv.setVisibility(View.VISIBLE);
                        subtitleTv.setVisibility(View.VISIBLE);
                    }
    
                    lruCacheManager.addBitmapToMemCache(bitmap, selectedMed.getId() + position);
                }
            }.execute(medUri);
        }
    

    I have tried working with Glide for the caching, but I haven't been able to link the showing/hiding of the TextViews to whether there is a bitmap. In a way though, this is sleeker as I don't need to load the bulk of the Glide-library. So I am happy with this for now.