I'm trying to use Glide to display thumbnails from the Google Photos library in a RecyclerView. In order to fetch images from this library, I must make two HTTP requests: first I must get the MediaItem from the id (I've already obtained a list of ids in a previous step), and second I must request the actual image from thumbnailUrl. This is the recommended process, as baseUrls expire after one hour so you aren't supposed to store them:
val googlePhotosThumbnailUrl =
App.googlePhotosService.getMediaItem(asset.googlePhotosId) // First HTTP request fetches MediaItem
.run {
val baseUrl = this.baseUrl
val thumbnailUrl = "$baseUrl=w400-h400" // Appends the requested dimensions to the Url.
thumbnailUrl // Second HTTP request fetches this URL
}
The problem is that Glide's load() method doesn't appear to support chaining HTTP requests like what's shown above:
GlideApp.with(itemView.context)
.asBitmap()
.load(googlePhotosThumbnailUrl)
.diskCacheStrategy(DiskCacheStrategy.ALL)
.into(binding.imageViewLargeThumbnail)
The above code executes synchronously, so loading is incredibly slow. I've managed to fix this by using coroutines as shown below. But the problem with this is Glide doesn't cache any of the images, so if I scroll down and back up Glide refetches every image:
override fun bindAsset(asset: GooglePhotosAsset, position: Int) {
this.asset = asset
this.index = position
// We set the loading animation here for Google Photos assets, since for those we need to fetch a mediaItem and then a baseUrl.
// This forces us to perform the process in a coroutine, and Glide can't set the loading animation until the baseUrl is fetched.
binding.imageViewLargeThumbnail.setImageResource(R.drawable.loading_animation)
fragment.lifecycleScope.launch(Dispatchers.Default) {
val googlePhotosThumbnailUrl = App.googlePhotosService.getMediaItem(asset.googlePhotosId) // First HTTP request fetches MediaItem
.run {
val baseUrl = this.baseUrl
val thumbnailUrl = "$baseUrl=w400-h400" // Appends the requested dimensions to the Url.
thumbnailUrl // Second HTTP request fetches this URL
}
withContext(Dispatchers.Main) {
GlideApp.with(itemView.context)
.asBitmap()
.load(googlePhotosThumbnailUrl)
.diskCacheStrategy(DiskCacheStrategy.ALL)
.fitCenter()
.into(binding.imageViewLargeThumbnail)
}
}
}
The only potentially relevant answer I've found is this one, but it seems super complicated and outdated. Are there any better solutions?
Related
I am using Person Object to build chat app notifications like Gmail. So I have created the person object. But i want to set icon from a image URL coming from server an not from drawable resource. I am using Coil library for loading images . The below code is working fine,
By default the android generates the icon with the first letter passed to the title.
So, How can i show the image coming from server as a URL in icon with best practice of memory and resource usages. Below is my Person object.
Here is the Official link of Person.
And this is what I referred to Notification Messaging style tutorial
val senderPerson: Person = Person.Builder().also {person->
person.setKey(message.getSenderKey(prefs))
person.setName(message.getNotificationTitle())
person.setImportant(true)
//****HERE I WANT TO SET IMAGE FROM URL******
// person.setIcon(IconCompat.createWithResource(this, R.drawable.placeholder_transaparent))
}.build()
You'd load the image URL asynchronously using the Coil Request, and return the fetched icon in a closure.
Coil returns a drawable, and you can get Icon from a Drawable through a Bitmap using IconCompat.createWithBitmap((drawable as BitmapDrawable).bitmap):
private fun asyncLoadIcon(imageUrl: String?, setIcon: (IconCompat?) -> Unit) {
if (imageUrl.isNullOrEmpty())
setIcon(null)
else {
// using COIL to load the image
val request = ImageRequest.Builder(this)
.data(imageUrl)
.target { drawable ->
setIcon(IconCompat.createWithBitmap((drawable as BitmapDrawable).bitmap)) // // Return the fetched icon from the URL
}
.listener(object : ImageRequest.Listener { // Return null icon if the URL is wrong
override fun onError(request: ImageRequest, result: ErrorResult) {
setIcon(null)
}
})
.build()
imageLoader.enqueue(request)
}
}
This code returns a null icon if the URL is wrong or if it's empty/null.
Then build the notification message with that function:
asyncLoadIcon("https://my_icon_url.png") { // set the icon url
val person = Person.Builder().apply {
setName("John Doe")
setIcon(it)
}.build()
// Build the notification with the person
.....
}
For some enhancements, you'd enable caching, and disable hardware bitmaps; but I do recommend other libraries like Glide and Picasso.
.memoryCachePolicy(CachePolicy.ENABLED)
.diskCachePolicy(CachePolicy.ENABLED)
.allowHardware(false) // Disable hardware bitmaps
I have a retrofit request
#GET("{link}")
suspend fun getFilePart(#Path(value = "link") link: String): Deferred<NetworkResponse<ResponseBody, NetworkError>>
and when i call it i pass a 'link'
val base = if (BuildConfig.DEBUG) BuildConfig.TRANSFER_URL_DEBUG else BuildConfig.TRANSFER_URL
apiManager.appApiService(base).getFilePart(it.link)
Lets say the link is something like "https://storage_dev.example.com/10002/6d197e1e57e37070760c4ae28bf1..." but in the Logcat i see that some characters get urlEncoded.
For example
the following Url
https://storage_dev.example.com/10002/6d197e1e57e37070760c4ae28bf18d813abd35a372b6a1f462e4cef21e505860.1&Somethingelse
turns to
https://storage_dev.example.com/10002/6d197e1e57e37070760c4ae28bf18d813abd35a372b6a1f462e4cef21e505860.1%3FSomethingelse
As i can see the link is a String that has many characters inside that get encoded like "&" has turned to "%3F"
How can i prevent this?
You can add encoded = true to your request param to tell retrofit to not encode it again:
/**
* Specifies whether the parameter {#linkplain #value() name} and value are already URL encoded.
*/
boolean encoded() default false;
Example:
#Path(value = "link", encoded = true)
If your link includes the baseurl part you should use #Url to avoid that problem
#GET
suspend fun getFilePart(#Url link: String): Deferred<NetworkResponse<ResponseBody, NetworkError>>
I think I'm late but however this is how I solved it ..
my issue was the url to containes " so on request url it gets encoded then looks like this domain.com/api/%22SOME_URL_%22
simply just add interceptor to catch the request and decode it.
if(it.request().url().toString().contains("api/MY_SUB_DOMAIN")){
val newUrl = java.net.URLDecoder.decode( it.request().url().toString(),
StandardCharsets.UTF_8.name()) // <--- This is your main solution (decode)
.replace("\"", "") // <---- I had to do this to remove parenthasis "
requestBuilder.url(newUrl) // <--- DONT FORGET TO ASSAIGN THE NEW URL
}
first time writing here on stackoverflow.
(You bet I'm a noob in Android development)
I've been experimenting with a quasi-Spotify clone app that has a Recyclerview showing song thumbnails & load mp3s from Firestore db via URL. The app is displaying images using Glide and plays mp3 using ExoPlayer.
All is working fine except the loading of images (about 12 of them currently) got a bit slower after I've enabled ExoPlayer to play using Cache. Before implementing Cache for ExoPlayer Glide displayed image immediately upon launch (less than 1 second) but after using Cache for ExoPlayer it takes about 3~4 seconds to display 6~7 rows of Recyclerview.
ExoPlayer prep BEFORE using cacheDataSoruce
private fun prepPlayerNormal(url: String?) { // NORMAL
val uri = Uri.parse(url)
val localMp3Uri = RawResourceDataSource.buildRawResourceUri(R.raw.rocounty_demo)
val mediaItem = MediaItem.fromUri(uri)
val mediaSource = ProgressiveMediaSource.Factory(DefaultDataSourceFactory(receivedContext, dataSourceFactory), DefaultExtractorsFactory()).createMediaSource(mediaItem)
exoPlayer.setMediaSource(mediaSource)
exoPlayer.prepare()
exoPlayer.playWhenReady = true
}
ExoPlayer prep AFTER using cacheDataSoruce
private fun prepPlayerWithCache(url:String?) {
val mp3Uri = Uri.parse(url)
val mediaItem = MediaItem.fromUri(mp3Uri)
val mediaSource = ProgressiveMediaSource.Factory(cacheDataSourceFactory).createMediaSource(mediaItem)
exoPlayer.setMediaSource(mediaSource, true)
exoPlayer.prepare()
exoPlayer.playWhenReady = true
}
And this is my Caching helper class (called from MainActivity inside OnCreate()):
class MyCacher(private val receivedContext: Context, private val cacheDir: File, private val mpInstanceReceived: MyMediaPlayer ) {
companion object {
var simpleCache: SimpleCache? = null
var leastRecentlyUsedCacheEvictor: LeastRecentlyUsedCacheEvictor? = null
var exoDatabaseProvider: ExoDatabaseProvider? = null
var exoPlayerCacheSize: Long = 90 * 1024 * 256
}
fun initCacheVariables() {
if (leastRecentlyUsedCacheEvictor == null) {
leastRecentlyUsedCacheEvictor = LeastRecentlyUsedCacheEvictor(exoPlayerCacheSize)
Log.d(TAG, "initCacheVariables: inside leastRecentlyUsed....")
}
if (exoDatabaseProvider == null) {
exoDatabaseProvider = ExoDatabaseProvider(receivedContext)
Log.d(TAG, "initCacheVariables: inside exoDatabaseProvider ... ")
}
if (simpleCache == null) {
simpleCache = SimpleCache(cacheDir, leastRecentlyUsedCacheEvictor!!, exoDatabaseProvider!!)
Log.d(TAG, "initCacheVariables: inside simpleCache..")
}
mpInstanceReceived.initExoPlayerWithCache()
}
}
As far as I understand, ExoPlayer's caching uses RAM while Glide reads from cache written on the Disk. How both are affected by each other is a mystery to me.
I've searched a forum but found no related topics so far.
SUMMARY: After executing ExoPlayer to stream mp3 with CacheDataSource, Glide's loading speed got way slower (2-3 seconds delay to display thumbnail size image on 6 to 7 rows of Recycler view)
QUESTION1: How is ExoPlayer's play from cache affecting my Glide loading speed?
QUESTION2: Is this normal? What should I do to revert my Glide to have its previous loading speed.
Note) Size of the image files on Firestore are range from 200kb-1.0MB (some files are intentionally large for testing purposes)
Android Studio 4.2/Kotlin
Test emulator: NEXUS 5X API 25
Thanks so much in advance!
After struggling (and excessive searching) for about a week, I've found a solution to this. It turns out that ExoPlayer and Glide were sharing the same folder and SimpleCache's constructor was deleting Glide's cache as they were unrecognized files.
You can solve this issue by using a different location for your ExoPlayer's cache (or adding a subfolder)
Source link: https://github.com/bumptech/glide/issues/4429#issuecomment-737870711
I am using a binding adapter to load images in a recycler view. Images appear fine. While fast scrolling I noticed sometimes I was getting a 'connection leaked' message from Picasso.
The problem comes from dead image links, hardcoding all of my image urls to point nowhere produces the error for every image after scrolling the first couple off the screen.
W/OkHttpClient: A connection to https://s3-eu-west-1.amazonaws.com/ was leaked. Did you forget to close a response body?
The code is basically identical to this sample.
BindingUtils.kt
object BindingUtils {
#BindingAdapter("imageUrl")
#JvmStatic
fun setImageUrl(imageView: ImageView, url: String) {
Picasso.with(imageView.context).load(url).into(imageView)
}
xml
<ImageView
android:id="#+id/imageview_merchant_background"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#color/primary"
android:scaleType="centerCrop"
app:imageUrl="#{viewModel.background}"/>
gradle
implementation "com.squareup.retrofit2:retrofit:$rootProject.retrofitVersion"
implementation "com.squareup.retrofit2:adapter-rxjava2:$rootProject.retrofitVersion"
implementation "com.squareup.retrofit2:converter-gson:$rootProject.retrofitVersion"
implementation "com.squareup.okhttp3:logging-interceptor:$rootProject.okhttpLoggingVersion"
implementation "com.squareup.picasso:picasso:$rootProject.picassoVersion"
retrofitVersion = '2.3.0'
okhttpLoggingVersion = '3.6.0'
picassoVersion = '2.5.2'
I can see several references to people needing to closing connections for standard Okhttp requests but seeing as that Picasso load call is a one-liner how can this be leaking?
Under the hood Picasso is using okhttp3 for handling its network requests. See here the code for Picasso's NetworkRequestHandler class: https://github.com/square/picasso/blob/0728bb1c619746001c60296d975fbc6bd92a05d2/picasso/src/main/java/com/squareup/picasso/NetworkRequestHandler.java
There is a load function that handles an okhttp Request:
#Override public Result load(Request request, int networkPolicy) throws IOException {
okhttp3.Request downloaderRequest = createRequest(request, networkPolicy);
Response response = downloader.load(downloaderRequest);
ResponseBody body = response.body();
if (!response.isSuccessful()) {
body.close();
throw new ResponseException(response.code(), request.networkPolicy);
}
// Cache response is only null when the response comes fully from the network. Both completely
// cached and conditionally cached responses will have a non-null cache response.
Picasso.LoadedFrom loadedFrom = response.cacheResponse() == null ? NETWORK : DISK;
// Sometimes response content length is zero when requests are being replayed. Haven't found
// root cause to this but retrying the request seems safe to do so.
if (loadedFrom == DISK && body.contentLength() == 0) {
body.close();
throw new ContentLengthException("Received response with 0 content-length header.");
}
if (loadedFrom == NETWORK && body.contentLength() > 0) {
stats.dispatchDownloadFinished(body.contentLength());
}
InputStream is = body.byteStream();
return new Result(is, loadedFrom);
}
I am not too familiar with the Picasso project, but it seems like the response body object is not closed in all cases. You may have spotted a bug in Picasso and may want to file an issue at picasso's github
Wild guess, if it has to do anything with the leaking of the context of your activity. Try with applicationContext
Picasso.with(imageView.context.applicationContext).load(url).into(imageView)
I am building a Cordova app for Android. I have to parse a JSON that consists of posts. Each post has text (title, description, category etc.) and images (an array of images - can be one or more). My aim is to store the JSON data for offline use (save to SQLlite database). Right now the example code below works, but the sequence is not how I expected to be:
request JSON (ok)
Wait for all promises (ok)
Parse JSON (finishes before all the images are downloaded)
Store to database the information but the images still downloading (in background thread - no harm for the UI).
What I would like to have is to store to database, when all the images have been downloaded. I' ve tried many things such as replacing the second for-loop with a recursive function (to handle the async function as stated here) and many other similar approaches but I believe that the problem starts from the 1st for loop which doesn't wait for the checkCache to finish. What do you think? How can I overcome this issue? If you need any further explanation just ask me.
My setup is:
Cordova 4.0.0, Angular 1.3.1 and ImgCache 1.0
My approach is:
1st. Request JSON:
promise1 = $http({method: 'GET', url: baseURL + options1};
promise2 = $http({method: 'GET', url: baseURL + options2};
//...
2nd. Wait for all promises
return $q.all([promise1,promise2,...]).then(function(data){
var promise1size = data[0].data.posts_number;//posts.length;
parseJSON(data[0],promise1size,'category1');
var promise2size = data[1].data.posts_number;//posts.length;
parseJSON(data[1],promise1size,'category2');
//similar for the rest promises
});
3rd. Parse JSON
function parseJSON(respdata,size,category){
console.log("Parsing "+size+" "+category);
for(i=0;i<size;i++){
var item = {};
item ["id"] = respdata.data.object[i].id;
item ["title"] = respdata.data.object[i].title;
item ["description"] = respdata.data.object[i].description;
var jsarray = respdata.data.object[i].categories;
item ["category"] = jsarray[0].title;
item ["catid"] = jsarray[0].id;
//Other JSON keys here similar as above
//Here it starts...
var jsattachement = respdata.data.object[i].attachments;
var atsize = jsattachement.length;
if(atsize>0){
var images=[];
for(j=0;j<atsize;j++){
(function(j){checkCache(jsattachement[j].url)}(j));//here is the problem
multimedia.push({title:item["title"], src:ImgCache.returnCachedURL(jsattachement[j].url), w:400,h:300});
images.push({title:item["title"],src:ImgCache.returnCachedURL(jsattachement[j].url),w:400,h:300});
}
item ["attachement"] = images;
}else
item ["attachement"] = [];
if(category=='category1')
response.category1.push(item);
else if(category=='category2')
response.category2.push(item);
//else if...
//else if...
}
}
};
checkCache function:
function checkCache (imgsrc){
ImgCache.isCached(imgsrc, function(src, success) {
if(!success){
ImgCache.cacheFile(src, function(){});
}
});
};
4th. Store to database
Here I save the parsed information to the database. On step 3 I use the returnCachedURL function for the images (which is not asynchronous) so to have the local path of the image ready even if it might not have been downloaded yet (but eventually will).
I did this:
Similar to you but, use update sql to store the image for every downloaded image. Then, I found that some of my users want to play with me, they disconnect the internet connection in the middle of image downloading! so that I have incomplete record in the sql! Jesus!
then I change to this: I create a temporary global var. e.g. tempvar = {}, then: tempvar.data1 = 'hello', tempvar.preparedatacompleted = true, tempvar.ImagesToBeDownload = 5, tempvar.ImagesDownloaded = 0, tempvar.imagearray = ......
then, everytime image downloaded, add 1 to the number, if that no. = total no., call the function that save all data and images in this tempvar to the sql.