0

I have an app that downloads images from the server, adds watermark then allows a user to share them via other apps. I use Picasso to load the images into targets.

The problem is that loading the images into targets take a bit of time so I need to find a way to wait for the process to finish. I've read about CompletableFuture but it only targets Android +24 and this is unacceptable.

Here's what I have done so far

//Global var
final List<Target> remoteImgTargets = new ArrayList<>();

//method
List<ImageToShare> remoteImages = new ArrayList<>();
final ArrayList<Uri> finishedImages = new ArrayList<>();

int countImages = remoteImages.size();
for (int i = 0; i < countImages; i++) {
            final int k=i;
            Target target = new Target() {

                @Override
                public void onBitmapLoaded(Bitmap bitmap, Picasso.LoadedFrom from) {

                    remoteImgTargets.remove(this);

                    //Add watermark here
                    finishedImages.add(Utils.getLocalBitmapUri(watermakedBmp, context));
                    Log.e("Targets", "Loaded: " + k);
                }

                @Override
                public void onBitmapFailed(Drawable errorDrawable) {
                    remoteImgTargets.remove(this);
                    Log.e("Targets", "onBitmapFailed(): ");
                }

                @Override
                public void onPrepareLoad(Drawable placeHolderDrawable) {
                    Log.e("Targets", "Preparing: " + k);
                }
            };
            remoteImgTargets.add(target);
            Picasso.with(context)
                    .load(remoteImages.get(k).getImageurl()) // Start loading the current target
                    .into(target);

        }

Further I use RxAndroid to observe the process and handle

ArrayList<Uri> globalUriArray = new ArrayList<>();
Observable<Uri> observable2 = Observable
                .fromIterable(finishedImages)
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread());

        Observer<Uri> observer = new Observer<Uri>() {
            @Override
            public void onSubscribe(Disposable d) {
                //TODO
            }

            @Override
            public void onNext(Uri uri) {
                globalUriArray.add(uri);
            }

            @Override
            public void onError(Throwable e) {
                //TODO
            }

            @Override
            public void onComplete() {
                if(globalUriArray.size()>0) {
                    //Display share intent
                }
            }
        };

        observable2.subscribe(observer);

*The problem * The target loading gets left behind and only a tiny fraction of the images are loaded.

Help needed How to wait for the Picasso targets to complete loading and maybe a fire callback if that's possible or anything.

Thanks in advance...

Joe Okatch
  • 660
  • 5
  • 9

2 Answers2

0

The Problem here is that, you are not synchronising everything, what I mean by that is you are loading Images on a separate thread and adding watermark and finally calling all this Rx on a Scheduler thread and loading of images occurs on a completely different main thread,

so you will have to synchronise all of fetching images and adding watermark on the scheduler thread and loading the image to the main thread, find find the example below. I have made it generic to give an idea on how it should be done.

 private void loadImageObserver(final String url) {
        Observable.create(new ObservableOnSubscribe<Bitmap>() {
            @Override
            public void subscribe(final ObservableEmitter<Bitmap> emitter) {
                //Use any-way to download bitmap from url
                //here Picasso is used to download the bitmap as was the question 

                try {
                    Bitmap bitmap = Picasso.get()
                            .load(url) // Start loading the current target
                            .get();
                    if (bitmap != null) {
                        emitter.onNext(bitmap);
                    }

                } catch (IOException e) {
                    e.printStackTrace();
                    emitter.onError(e);
                }


            }
        }).subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Observer<Bitmap>() {
                    @Override
                    public void onSubscribe(Disposable d) {
                        Log.d(TAG, "subscribed");
                    }

                    @Override
                    public void onNext(Bitmap bitmap) {
                        Log.d(TAG, "on next " + bitmap.getWidth() + " " + bitmap.getHeight() + " " + bitmap.getByteCount());
                    }

                    @Override
                    public void onError(Throwable e) {
                        Log.d(TAG, "error " + e.getLocalizedMessage());
                    }

                    @Override
                    public void onComplete() {
                        Log.d(TAG, "on complete");
                    }
                });
    }
Aniruddha K.M
  • 7,361
  • 3
  • 43
  • 52
0

I finally used an less than modern approach which worked well. At the time I asked this question I had knowledge gaps with RxJava reactivity. Here's what I did in case it helps:

//some helpful resources //Android image caching //Strange out of memory issue while loading an image to a Bitmap object

.... the Remote Image Processor class

import android.app.ActivityManager;
import android.content.ComponentCallbacks2;
import android.content.Context;
import android.content.Intent;
import android.content.res.Configuration;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.AsyncTask;
import android.util.Log;
import android.util.LruCache;

import androidx.localbroadcastmanager.content.LocalBroadcastManager;

import com.zahomy.xxx.xxx.models.ImageToShare;
import com.zahomy.xxx.xxx.xxx.Utils;
import com.zahomy.xxx.xxx.xxx.WaterMark;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;

public class RemoteImageProcessor implements ComponentCallbacks2 {
    String TAG = "RemoteImageProcessor > ";
    private TCLruCache cache;
    //accept a list of image urls
    //accept context
    //check if images are in cache then load them
    //else fetch the list from the server
    private List<ImageToShare> remoteImages;
    private ArrayList<Uri> mfinishedImages;
    //LinkedHashMap<String, Bitmap> downloadedBitmaps = new LinkedHashMap<>();
    private List<String> imagesToFetchAgain = new ArrayList<>();
    private Context context;
    private int mWatermarkPos;
    private String mRECEIVER_TAG; //unique to the activity that initiated this process
    private String mRECEIVER_MSG;
    private SetImageTask imgTask;

    /**
     *
     * @param remoteImages
     * @param context
     * @param mWatermarkPos
     * @param RECEIVER_TAG
     */

    public RemoteImageProcessor(List<ImageToShare> remoteImages, Context context,
                                int mWatermarkPos, LinkedHashMap<String, String> RECEIVER_TAG) {
        this.remoteImages = remoteImages;
        this.context = context;
        this.mWatermarkPos = mWatermarkPos;

        ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
        int maxKb = am.getMemoryClass() * 1024;
        int limitKb = maxKb / 2; // 1/8th of total ram
        cache = new TCLruCache(limitKb);
        mRECEIVER_TAG = RECEIVER_TAG.get("tag_name");
        mRECEIVER_MSG = RECEIVER_TAG.get("tag_message");
    }

    public void init() {
        //Get the net list of images to download
        getNonCachedImages();
    }

    /**
     * Prepare local cache
     */
    private class TCLruCache extends LruCache<String, Bitmap> {

        private TCLruCache(int maxSize) {
            super(maxSize);
        }

        @Override
        protected int sizeOf(String key, Bitmap value) {
            return value.getByteCount() / 1024; //kb Of Bitmap
        }
    }

    /**
     * We set a list images which will be downloaded again
     */
    private void getNonCachedImages() {
        //get all the images not available in cache into a list
        //download them and add to cache

        int loopSize = remoteImages.size();
        //Log.e(TAG, "cache size:"+cache.size());
        for(int i=0; i<loopSize; i++) {
            Bitmap bitmap = cache.get(remoteImages.get(i).getImageurl());

            if(bitmap == null) {
                imagesToFetchAgain.add(remoteImages.get(i).getImageurl());
            }
        }
        //Log.e(TAG, "imagesToFetchAgain:"+imagesToFetchAgain.size());
        if(imagesToFetchAgain.size()>0) {
            imgTask = new SetImageTask();
            imgTask.execute();
        }
    }

    private class SetImageTask extends AsyncTask<String, Void, Integer> {

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

            Bitmap bmp;
            try {
                int loopSize = imagesToFetchAgain.size();
                for(int i=0; i<loopSize; i++) {
                    String url = imagesToFetchAgain.get(i);
                    String fileName = null;
                    int cut = imagesToFetchAgain.get(i).lastIndexOf('/');
                    if (cut != -1) {
                        fileName = imagesToFetchAgain.get(i).substring(cut + 1);
                    }
                    bmp = getBitmapFromURL(imagesToFetchAgain.get(i), fileName);
                    if (bmp != null) {
                        cache.put(url, bmp);
                        //downloadedBitmaps.put(url, bmp);
//                        Log.e(TAG, "cache size:"+ cache.size());
                    }
                    if (isCancelled())
                        break;
                }
            } catch (Exception e) {
                e.printStackTrace();
                return 0;
            }
            return 1;
        }

        @Override
        protected void onPostExecute(final Integer result) {
            getBitmapImages();
        }

        private Bitmap getBitmapFromURL(String src, String filename) {
            File cacheDir = context.getDir("", Context.MODE_PRIVATE);
            File fileWithinMyDir = new File(cacheDir, "");
            try {
                URL url = new URL(src);
                String pathname = fileWithinMyDir+"/"+filename;
                File file = new File(pathname);
                URLConnection ucon = url.openConnection();
                InputStream is = ucon.getInputStream();
//                Log.e(TAG, "is:"+is.toString());
                BufferedInputStream bis = new BufferedInputStream(is);
                FileOutputStream fos = new FileOutputStream(file);
                int current;
                byte[] bytes = new byte[1024];
                while ((current = bis.read(bytes)) != -1) {
                    fos.write(bytes, 0, current);
                }
                return decodeFile(file);
            } catch (IOException e) {
                e.printStackTrace();
                return null;
            }
        }
    }

    private Bitmap decodeFile(File f){
        Bitmap b=null;
        try {
            int IMAGE_MAX_SIZE = 800;

            //Decode image size
            BitmapFactory.Options o = new BitmapFactory.Options();
            o.inJustDecodeBounds = true;
            o.inDither=false;                     //Disable Dithering mode
            o.inPurgeable=true;                   //Tell to gc that whether it needs free memory, the Bitmap can be cleared
            o.inInputShareable=true;              //Which kind of reference will be used to recover the Bitmap data after being clear, when it will be used in the future
            o.inTempStorage=new byte[32 * 1024];
            FileInputStream fis = new FileInputStream(f);
            BitmapFactory.decodeStream(fis, null, o);
            fis.close();

            int scale = 1;
            if (o.outHeight > IMAGE_MAX_SIZE || o.outWidth > IMAGE_MAX_SIZE) {
                scale = (int)Math.pow(2, (int) Math.ceil(Math.log(IMAGE_MAX_SIZE / (double) Math.max(o.outHeight, o.outWidth)) / Math.log(0.5)));
            }

            //Decode with inSampleSize
            BitmapFactory.Options o2 = new BitmapFactory.Options();
            o2.inSampleSize = scale;
            fis = new FileInputStream(f);
            b = BitmapFactory.decodeStream(fis, null, o2);
            fis.close();
            //fis.close();
        } catch (IOException e) {
            //TODO
        }
        return b;
    }

    /**
     * By this point the cache must have all the images that we need
     */
    private void getBitmapImages() {
        ArrayList<Uri> finishedImages = new ArrayList<>();
        mfinishedImages = new ArrayList<>();
        int loopSize = remoteImages.size();
//        Log.e(TAG, "getBitmapImages > total cache size:"+ cache.size());

        for(int i=0; i<loopSize; i++) {
            Bitmap bitmap = cache.get(remoteImages.get(i).getImageurl());
            if (bitmap != null) {
                ArrayList<String> caption = new ArrayList<>();
                caption.add(remoteImages.get(i).getPrice());
                caption.add(remoteImages.get(i).getTitle());
                caption.add(remoteImages.get(i).getDescription());

                Bitmap watermakedBmp = WaterMark.setWaterMark(bitmap, caption, context, mWatermarkPos);
                finishedImages.add(Utils.getLocalBitmapUri(watermakedBmp, context));
            }
        }
        mfinishedImages.addAll(finishedImages);

        //Sending broadcast
        sendBroadcast();
    }

    private void sendBroadcast() {
//        Log.e(TAG, "sendBroadcast size: "+mfinishedImages.size());
        Intent intent = new Intent(mRECEIVER_TAG);
        intent.putExtra(mRECEIVER_MSG, mfinishedImages);
        LocalBroadcastManager.getInstance(context).sendBroadcast(intent);
    }

    public void cancelDownload() {
        if(imgTask != null) {
            if(!imgTask.isCancelled()) {
                imgTask.cancel(true);
            }
            imgTask = null;
        }
    }

    @Override
    public void onConfigurationChanged(Configuration newConfig) {
    }

    @Override
    public void onLowMemory() {
    }

    @Override
    public void onTrimMemory(int level) {
        //super.onTrimMemory(level);
        if (level >= TRIM_MEMORY_MODERATE) { //60
            //Nearing middle of list of cached background apps; evict our entire thumbnail cache
            cache.evictAll();
        }
        else if (level >= TRIM_MEMORY_BACKGROUND) { //40
            //entering list of cached background apps; evict oldest half of our thumbnail cache
            cache.trimToSize(cache.size() / 2);
        }
    }
}

...... Image object

import android.os.Parcel;
import android.os.Parcelable;

public class ImageToShare implements Parcelable {
    String title;
    String price;
    String description;
    String imageurl;

    public ImageToShare(String title, String price, String description, String imageurl) {
        this.title = title;
        this.price = price;
        this.description = description;
        this.imageurl = imageurl;
    }

    public String getTitle() {
        return title;
    }

    public String getPrice() {
        return price;
    }

    public String getDescription() {
        return description;
    }

    public String getImageurl() {
        return imageurl;
    }

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel parcel, int i) {
        parcel.writeString(title);
        parcel.writeString(price);
        parcel.writeString(description);
        parcel.writeString(imageurl);
    }

    public ImageToShare(Parcel in) {
        this.title = in.readString();
        this.price = in.readString();
        this.description = in.readString();
        this.imageurl = in.readString();
    }

    public static final Parcelable.Creator<ImageToShare> CREATOR = new Parcelable.Creator<ImageToShare>() {
        public ImageToShare createFromParcel(Parcel in) {
            return new ImageToShare(in);
        }

        public ImageToShare[] newArray(int size) {
            return new ImageToShare[size];
        }
    };
}

..... in my utils class somewhere

public static Uri getLocalBitmapUri(Bitmap bmp, Context c) {
        Uri bmpUri = null;
        try {
            File file =  new File(c.getExternalFilesDir(Environment.DIRECTORY_PICTURES), "share_image_" + System.currentTimeMillis()+1 + ".png");
            FileOutputStream out = new FileOutputStream(file);
            bmp.compress(Bitmap.CompressFormat.PNG, 90, out);
            out.close();
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                bmpUri = FileProvider.getUriForFile(c,
                        c.getString(R.string.file_provider_authority),
                        file);
            } else {
                bmpUri = Uri.fromFile(file);
            }

        } catch (IOException e) {
            e.printStackTrace();
        }
        return bmpUri;
    }

....... Finally, I call the class here

//Prepare the selection
List<ImageToShare> remoteImages = new ArrayList<>();
RemoteImageProcessor imgProcessor = new RemoteImageProcessor(remoteImages,
                MyActivity.this, mWatermarkPos, mRECEIVER_TAG);
imgProcessor.init();
Joe Okatch
  • 660
  • 5
  • 9