I am working on an application that makes use of a dynamically populated list view of which each item contains an image that needs to be downloaded. Additionally, when clicking on any item in the list, it opens a detail page which once again shows the same image in addition to a few more.
As we know, it is best to use an async task to do the actual download and good practice to use a caching mechanism to both speed up image display as well as minimize actual data usage in multiple downloads.
After searching around for quite a bit, I ended up using the Google example code located at http://developer.android.com/resources/samples/XmlAdapters/src/com/example/android/xmladapters/ImageDownloader.html which claims to be both an async and caching method to download images.
The problem I am having, when called, this class does not appear to be caching the images correctly. So here goes ....
The portion of code in my custom class (that builds the list view) where I call the image download is this:
ImageDownloader getimage = new ImageDownloader();
getimage.download(logoURL, ivLogo);
With ImageDownloader being the class referenced above.
Images load, but it is apparent they are loading from the web each time. I added some logging into ImageDownloader.class to help track what is happening when. What I found, the first time the image is requested, it fails the first call to get from cache (as expected), getBitmapFromCache returns null and forceDownload is called.
public void download(String url, ImageView imageView, String cookie) {
resetPurgeTimer();
Bitmap bitmap = getBitmapFromCache(url);
Log.i(LOG_TAG, "Bitmap is: " + bitmap);
if (bitmap == null) {
Log.i(LOG_TAG, "Forcing Download");
forceDownload(url, imageView, cookie);
} else {
cancelPotentialDownload(url, imageView);
imageView.setImageBitmap(bitmap);
}
}
As execution progresses down the class to the point where it adds the image/url to the cache
private void addBitmapToCache(String url, Bitmap bitmap) {
Log.i(LOG_TAG, "Incoming Add Cache Info: URL: " + url + " Bitmap: " + bitmap);
if (bitmap != null) {
Log.i(LOG_TAG, "Entering the add image to cache section");
synchronized (sHardBitmapCache) {
sHardBitmapCache.put(url, bitmap);
Bitmap returnedbitmap = getBitmapFromCache(url);
Log.i(LOG_TAG, "Returned bitmap immediately after adding: " + bitmap);
}
}
}
I added logging to both verify the cache was added, and then performed a getBitmapFromCache, and the logs show the returned bitmap/url hash returns properly.
Incoming request to get Image, URL: http://www.yourlogoresources.com/wp-content/uploads/2011/11/Wendys-logo.png
Trying Hard Cache with URL: http://www.yourlogoresources.com/wp-content/uploads/2011/11/Wendys-logo.png
Hard Bitmap is: null
Trying Soft Cache with URL: http://www.yourlogoresources.com/wp-content/uploads/2011/11/Wendys-logo.png
Return from initial entry call: null
Entering forced Download
Incoming Add Cache Info: URL:http://www.yourlogoresources.com/wp-content/uploads/2011/11/Wendys-logo.pngBitmap: android.graphics.Bitmap#40762f28
Entering the add image to cache section
Trying Hard Cache withURL: http://www.yourlogoresources.com/wpcontent/uploads/2011/11/Wendys-logo.png
Hard Bitmap is: android.graphics.Bitmap#40762f28
Returned bitmap immediately after adding:android.graphics.Bitmap#40762f28
So it appears the cache is being populated properly, however, the next time that image is requested (ie when you scroll the listview), the cached image is not found again with getBitmapFromCache(url) returning null from the entry point download.
Incoming request to get Image, URL: http://www.yourlogoresources.com/wp-content/uploads/2011/11/Wendys-logo.png
Trying Hard Cache with URL: http://www.yourlogoresources.com/wp-content/uploads/2011/11/Wendys-logo.png
Hard Bitmap is: null
Trying Soft Cache with URL: http://www.yourlogoresources.com/wp-content/uploads/2011/11/Wendys-logo.png
Return from initial entry call: null
Entering forced Download
So I am at a loss here as to why it appears the cache is populated, but when it is checked again, nothing but null is returned.
EDIT
I ended up using a static reference to the ImageDownloader from my main Activity as follows:
public class myActivity extends Activity implements OnClickListener {
public final static ImageDownloader GetImage = new ImageDownloader();
Then called that like this anywhere I needed an ImageDownload:
myActivity.GetImage.download(logoURL, ivLogo);
Thanks for all the help folks.
Look into the code of ImageDownloader.
It is using a primary cache
private final HashMap<String, Bitmap> sHardBitmapCache
and a secondary static cache
private final static ConcurrentHashMap<String, SoftReference<Bitmap>> sSoftBitmapCache
Entries kicked out of the primary cache are put into the secondary cache.
Now everytime you want to download an Image, if you create a new instance of ImageDownloader, you are effectively not using the caching mechanism because each ImageDownloader instance will only have that single image you downloaded using this instance.
PS: Your code in the question looked like you are using an instance per download.
Related
So I am doing an app that lets you take images (inside CameraActivity, using CameraX) and saves them to local storage. The images are later displayed in a RecyclerView, getting them by their URI.
My issue is that as soon as I open the fragment that contains the recyclerview with the images it gets very very slow. The images are small and don't necessarily need to be in good quality, so I thought of maybe compressing them or just saving them in lower quality right when they are taken to make the loading process faster. Is there any way I can do that right here at this point in the code?
This is some relevant code from CameraActivity:
imgView_cameraTrigger.setOnClickListener(v -> {
SimpleDateFormat mDateFormat = new SimpleDateFormat("yyyyMMddHHmmss", Locale.US);
File file = new File(getExternalFilesDir(Environment.DIRECTORY_DCIM), mDateFormat.format(new Date())+ ".jpg");
ImageCapture.OutputFileOptions outputFileOptions = new ImageCapture.OutputFileOptions.Builder(file).build(); // saves image file
imageCapture.takePicture(outputFileOptions, executor, new ImageCapture.OnImageSavedCallback () {
#Override
public void onImageSaved(#NonNull ImageCapture.OutputFileResults outputFileResults) {
new Handler(Looper.getMainLooper()).post(new Runnable() {
#Override
public void run() {
Toast.makeText(CameraActivity.this, "image saved successfully", Toast.LENGTH_SHORT).show();
// Toast.makeText(CameraActivity2.this, "PATH: " + file.getAbsolutePath(), Toast.LENGTH_SHORT).show();
Uri uri = Uri.fromFile(file);
ImageView preview = TrackFragment.getImageViewInTrackFragment();
preview.setImageURI(uri); // display taken image in imageview in track fragment
TrackFragment.setImgPreviewUri(uri); // pass uri to TrackFragment to use it there
CameraActivity.this.finish(); // exit camera activity
}
});
}
#Override
public void onError(#NonNull ImageCaptureException error) {
error.printStackTrace();
}
});
});
I thought of maybe compressing them or just saving them in lower quality right when they are taken to make the loading process faster
You probably can give resolution suggestions to CameraX to have it take a lower-resolution image, if that is what you mean. That would tie to code outside of your question.
However, a large part of your problem also lies here:
preview.setImageURI(uri); // display taken image in imageview in track fragment
The documentation for setImageURI() states:
This does Bitmap reading and decoding on the UI thread, which can cause a latency hiccup
In other words, using setImageURI() leads to poor performance and is rarely recommended.
Please use Glide, Picasso, or another image-loading library. Not only can they do the image loading on a background thread, but they can also automatically down-sample the image to fit your ImageView, improving loading speeds. They can also show a placeholder image while the image is being loaded, cache images in memory to reduce redundant image loads, etc.
FWIW, I demonstrate the use of Glide for image loading in this section of this free book, though changes in the US National Weather Service API may mean that the sample code itself does not work correctly now as it did when I published that final edition.
EDIT:
I asked a similar question a day before and I wrote this post based on information I gathered since that question -
First Cache Question. I know they are similar but this question is more concise to avoid extra information. I also didn't want to delete that post since it was answered, even though the answer didn't suffice.
I use a recyclerview to show photos I get from google places api. I know that I am not allowed to legally cache photos outside of runtime. I didn't know that was happening until I randomly opened my phone's gallery to see a lot of copies of the photos I get from google.
My first guess was that my use of Picasso was the issue so I added code to fix it,
.memoryPolicy(MemoryPolicy.NO_CACHE, MemoryPolicy.NO_STORE)
.networkPolicy(NetworkPolicy.NO_CACHE)
finding these in StackOverflow was pretty simple, but this didn't fix anything except that now it seems to only download them once especially when I delete them. I believe I eliminated the possible issues and am outlining the last one in this question.
private Uri getImageUri(Context inContext, Bitmap inImage) {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
inImage.compress(Bitmap.CompressFormat.JPEG, 100, bytes);
String path = MediaStore.Images.Media.insertImage(inContext.getContentResolver(), inImage, "Title", null);
return Uri.parse(path);
}
#Override
public void onBindViewHolder(#NonNull VenueViewHolder venueViewHolder, int i) {
Collections.sort(vIAL, (o1, o2) -> o1.getVenueName().compareToIgnoreCase(o2.getVenueName()));
VenueItem currentItem = vIAL.get(i);
Picasso.with(context)
.load(getImageUri(context, currentItem.getVenueImage()))
.fit()
.memoryPolicy(MemoryPolicy.NO_CACHE, MemoryPolicy.NO_STORE)
.networkPolicy(NetworkPolicy.NO_CACHE)
.into(venueViewHolder.vIV);
venueViewHolder.vTV.setText(currentItem.getVenueName());
Log.i(TAG, "The photo should have been on screen");
}
The URI method getImageUri is something I found as an answer to another problem I had, that I needed a URI to implement the Picasso library so that I can manipulate the photos before displaying them.
My question is - How do I remove the photos when the app closes?
UPDATE:
I changed my tactic to see what would happened and used Glide
#Override
public void onBindViewHolder(#NonNull VenueViewHolder venueViewHolder, int i) {
Collections.sort(vIAL, (o1, o2) -> o1.getVenueName().compareToIgnoreCase(o2.getVenueName()));
VenueItem currentItem = vIAL.get(i);
Glide.with(context)
.load(currentItem.getVenueImage())
.into(venueViewHolder.vIV);
venueViewHolder.vTV.setText(currentItem.getVenueName());
}
and it gave a fatal Error
E/JavaBinder: !!! FAILED BINDER TRANSACTION !!! (parcel size = 4344032)
This was one of the errors and it didn't occur the first time I ran this new code but it got worse the second and third time I ran it.
I shifted my code based on an answer I got early from #Padmini S but they used a url in the load part and I pass a bitmap because for the life of me I can't figure out how to get a URL from Google Places API instead of the code they provide in
Google Place Photos.
I'm relatively new to coding so this is me trying to piece together what I need to learn more. I'm just out of ideas of what to search for so I ask here,
based on the new information I gathered from the answer, can I replace
MediaStore.Images.Media.insertImage(inContext.getContentResolver(), inImage, "Title", null);
in my code so that my photos don't get saved to my phone's photos?
Final Update
I ended up rewriting a lot of the code surrounding this and deleting this code. I ended up making an arraylist class to hold the array list for the duration of runtime and it let me remove most of the extra code I wrote out of ignorance.
I really don't know whether it'll solve your problem or not , but you are storing the images in device local storage here,
private Uri getImageUri(Context inContext, Bitmap inImage) {
String path = MediaStore.Images.Media.insertImage(inContext.getContentResolver(), inImage, "Title", null);
}
I have never used google places api but usually apis will return you with an image url you can store that url in POJO class and display it directly in recyclerview row image view like I am doing in below code,
public void onBindViewHolder(#NonNull ViewHolder viewHolder, final int i) {
viewHolder.description.setText(links.get(i).getTitle());
Glide.with(context).load(links.get(i).getImage_url()).into(viewHolder.image);
}
Question in short:
How can I find out, that background loading of image has failed just before imageView.setImage(image) results in showing an empty picture although image.isError==false and image.getException==null?
Background:
In my simple JavaFX based photoviewer app I use a TableView() for showing a directory with jpg files. Whenever one selects an entry in the table the picture is loaded using javafx Image class and is shown using an ImageView.
I load the photos in background using true in the parameter of the Image-constructor.
Once a photo is loaded I keep it in a list ("Cache") for faster "showing again"
Here the code snippet:
public Object getMediaContent() {
Image image = (Image) content;
if (!isMediaContentValid()) { //if not already loaded or image in cache is invalid
try {
System.out.println("getMediaContent loading " + fileOnDisk);
content = new Image(fileOnDisk.toUri().toString(), true); //true=load in Background
} catch (Exception e) {
//will not occur with backgroundLoading: image.getException will get the exception
System.out.println("Exception while loading:");
e.printStackTrace();
}
} else {
System.out.println(fileOnDisk.toString() + "in Cache :-)...Error="+ image.isError() + " Exception=" + image.getException());
}
return content;
}
In isMediaContentValid() I test
if the image is already in the cache's list
and if image.isError() is false
and if image.getException() is null
The problem:
When the user selects photos very quickly (e.g. by using cursor down key) images are still loaded in background (for the cache) while the next photo's loading has already started.
My simple chache algorithm has problems to find out when I run out of memory because there might be enough memory when loading is started but not to complete all background tasks.
But I expected that this is not a problem because image.isError() would report true or image.getException() would be != null in this case. So I could free memory before retrying.
But isError() reports false, getException() reports null and the image is shown "empty" in imageView :-(
The question:
How can I find out, that background loading of image has failed just before imageView.setImage(image)?
How can I find out, that background loading of image has failed just before imageView.setImage(image)?
This is not possible. The whole point of loading the image in the background is that it's done asynchronously. There is no guarantee that the exception already occured at the time the method returns. You need to use a listener to the error property to be notified of a failure to load the image.
Example
Image image = new Image("https://stackoverflow.com/abc.jpg", true); // this image does not (currently) exist
image.errorProperty().addListener(o -> {
System.err.println("Error Loading Image " + image.getUrl());
image.getException().printStackTrace();
});
So at certain times I need to clear the Picasso cache within my application, however when I clear it, the image is the same. If the image URL changes, then it pulls the new image but if the image url is the same, then the old image remains.
Can anyone help me clear the cache so that the image is removed?
This is how I set Picasso up:
Cache picassoCache = new LruCache(MEMORY_CACHE_SIZE);
picassoCacheClearer = new PicassoCacheClearerImpl(picassoCache);
picasso = new Picasso.Builder(context)
.downloader(new OkHttpDownloader(context.getCacheDir(), IMAGE_CACHE_SIZE))
.memoryCache(picassoCache)
.build();
and then try and clear it by:
cache.clear();
Below is the code which actually loads the image:
picasso.load(carouselAction.getImageUrl())
.placeholder(R.drawable.ic_placeholder)
.into(viewHolder.plistImageView);
I can see that the Invalidate() method is now deprecated, so what else can i do?
I have had this same problem and I used this hacky method to get around it. To force it to refresh i would just change the url adding a version tag to it. This is used commonly in web development to make sure things aren't used from the cache.
For example i would load an image from example.com/mypic.png?version=1234
and since the url is different it wouldn't load it from cache.
String versionTag = "?version=" + new Date().getTime();
picasso.load(carouselAction.getImageUrl() + versionTag)
.placeholder(R.drawable.ic_placeholder)
.into(viewHolder.plistImageView);
So found out what the issue was. Turns out for this carousel it was using a different instance of Picasso.
I also had to extend OkHttpDownloader to expose the getClient() method in order to get the cache to delete.
I realised it was a different instance of Picasso by enabling the indicators which can be done by setting the following in the builder
.indicatorsEnabled(true);
In my Android app I use Picasso to load images. This normally works perfectly well.
Today I tried loading a static image from the google maps api, but this doesn't seem to work. When I open the example link as provided on their info page, I get to see the static map image perfectly well. When I load it in my Android app using the line below, I get nothing at all.
Picasso.with(getContext()).load("http://maps.googleapis.com/maps/api/staticmap?center=Brooklyn+Bridge,New+York,NY&zoom=13&size=370x250&maptype=roadmap%20&markers=color:blue|label:S|40.702147,-74.015794&markers=color:green|label:G|40.711614,-74.012318%20&markers=color:red|color:red|label:C|40.718217,-73.998284&sensor=false").into(mapView);
I also tried to download the image and uploading it to my personal webspace, from which it loads perfectly well, but somehow, it doesn't seem to load directly from the direct google API url.
Does anybody know why this is so, and how I can solve it?
The only programmatic point-of-failure that comes to mind is in parsing the URI. Looking at the current Picasso code (https://github.com/square/picasso/blob/master/picasso/src/main/java/com/squareup/picasso/Picasso.java) I see the following:
public RequestCreator load(String path) {
if (path == null) {
return new RequestCreator(this, null, 0);
}
if (path.trim().length() == 0) {
throw new IllegalArgumentException("Path must not be empty.");
}
return load(Uri.parse(path));
}
So I'd first debug
Uri.parse("http://maps.googleapis.com/maps/api/staticmap?center=Brooklyn+Bridge,New+York,NY&zoom=13&size=370x250&maptype=roadmap%20&markers=color:blue|label:S|40.702147,-74.015794&markers=color:green|label:G|40.711614,-74.012318%20&markers=color:red|color:red|label:C|40.718217,-73.998284&sensor=false")
and see what that Object looks like. Does it drop or confuse any of your parameters?
If that doesn't lead you anwhere, try downloading the file manually using a HttpClient [or similar]. Then at least you can fully debug the request/response.
Also, I know Google maps has some limits -- are you sure you haven't reached them?
replace http with https
replace | with %7C
add api key
The .loadMap() function has many declared variables. This is the heart of the whole process.
So what is required for the static maps API to give us an image is that we make an http request with a given url, for which an image response (URL) is received. Let us run through the meaning and utility of these variables. Yes, all of them have a completely different meaning!
The mapUrlInitial variable is always the same while making an API call. It has a query of center ( ?center ) which specifies that we want the location to be centered in the map.
The mapUrlProperties variable contains a string where you control the actual zooming of the image response you will get, the size ofthe image and the color of the marker which will point out our place.
The mapUrlMapType variable is a string where you can actually determine the marker size you want and the type of the map. We are using a roadtype map in the app.
Finally latLong is a string which concatenates the latitude and the longitude of the place we want to pinpoint!
We then concatenate all of these strings to form a feasible Url. The Url is then loaded as we have seen above, in the Picasso code. One thing we can notice is that an event object is always required for all of this to happen, because we are able to fetch the position details using the event object! Final Code:-
fun loadMap(event: Event): String{
//location handling
val mapUrlInitial = “https://maps.googleapis.com/maps/api/staticmap?center=”
val mapUrlProperties = “&zoom=12&size=1200×390&markers=color:red%7C”
val mapUrlMapType = “&markers=size:mid&maptype=roadmap”
val latLong: String = “” +event.latitude + “,” + event.longitude
return mapUrlInitial + latLong + mapUrlProperties + latLong + mapUrlMapType
}
//load image
Picasso.get()
.load(loadMap(event))
.placeholder(R.drawable.ic_map_black_24dp)
.into(rootView.image_map)