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();