diff --git a/build.gradle b/build.gradle index 6dd57e4138..3d55c61d44 100644 --- a/build.gradle +++ b/build.gradle @@ -24,6 +24,7 @@ buildscript { androidxFragment: 'androidx.fragment:fragment:1.4.0', androidxLifecycle: 'androidx.lifecycle:lifecycle-common:2.4.0', androidxStartup: 'androidx.startup:startup-runtime:1.1.0', + coroutines: 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.0', junit: 'junit:junit:4.13.2', truth: 'com.google.truth:truth:1.1.3', robolectric: 'org.robolectric:robolectric:4.6.1', diff --git a/picasso/build.gradle b/picasso/build.gradle index b328f33ee6..637c939119 100644 --- a/picasso/build.gradle +++ b/picasso/build.gradle @@ -35,6 +35,7 @@ dependencies { implementation deps.androidxAnnotations implementation deps.androidxCore implementation deps.androidxExifInterface + implementation deps.coroutines testImplementation deps.junit testImplementation deps.truth diff --git a/picasso/src/main/java/com/squareup/picasso3/BitmapHunter.kt b/picasso/src/main/java/com/squareup/picasso3/BitmapHunter.kt index e5e6236d80..a83f8d1be4 100644 --- a/picasso/src/main/java/com/squareup/picasso3/BitmapHunter.kt +++ b/picasso/src/main/java/com/squareup/picasso3/BitmapHunter.kt @@ -35,6 +35,7 @@ import java.util.concurrent.CountDownLatch import java.util.concurrent.Future import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicReference +import kotlinx.coroutines.runBlocking internal open class BitmapHunter( val picasso: Picasso, @@ -101,7 +102,7 @@ internal open class BitmapHunter( } if (retryCount == 0) { - data = data.newBuilder().networkPolicy(NetworkPolicy.OFFLINE).build() + data = runBlocking { data.newBuilder().networkPolicy(NetworkPolicy.OFFLINE).build() } } val resultReference = AtomicReference() diff --git a/picasso/src/main/java/com/squareup/picasso3/Dispatcher.kt b/picasso/src/main/java/com/squareup/picasso3/Dispatcher.kt index dfeb2f0cd5..f8d2f89019 100644 --- a/picasso/src/main/java/com/squareup/picasso3/Dispatcher.kt +++ b/picasso/src/main/java/com/squareup/picasso3/Dispatcher.kt @@ -52,6 +52,7 @@ import com.squareup.picasso3.Utils.hasPermission import com.squareup.picasso3.Utils.isAirplaneModeOn import com.squareup.picasso3.Utils.log import java.util.concurrent.ExecutorService +import kotlinx.coroutines.launch internal class Dispatcher internal constructor( private val context: Context, @@ -335,10 +336,12 @@ internal class Dispatcher internal constructor( logId = getLogIdsForHunter(hunter) ) } - if (hunter.exception is ContentLengthException) { - hunter.data = hunter.data.newBuilder().networkPolicy(NO_CACHE).build() + hunter.picasso.scope.launch { + if (hunter.exception is ContentLengthException) { + hunter.data = hunter.data.newBuilder().networkPolicy(NO_CACHE).build() + } + hunter.future = service.submit(hunter) } - hunter.future = service.submit(hunter) } else { performError(hunter) // Mark for replay only if we observe network info changes and support replay. diff --git a/picasso/src/main/java/com/squareup/picasso3/Picasso.kt b/picasso/src/main/java/com/squareup/picasso3/Picasso.kt index b9a5abca68..198d27d119 100644 --- a/picasso/src/main/java/com/squareup/picasso3/Picasso.kt +++ b/picasso/src/main/java/com/squareup/picasso3/Picasso.kt @@ -53,6 +53,11 @@ import okhttp3.OkHttpClient import java.io.File import java.io.IOException import java.util.concurrent.ExecutorService +import kotlin.coroutines.CoroutineContext +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel /** * Image downloading, transformation, and caching manager. @@ -80,7 +85,8 @@ class Picasso internal constructor( * **WARNING:** Enabling this will result in excessive object allocation. This should be only * be used for debugging purposes. Do NOT pass `BuildConfig.DEBUG`. */ - @Volatile var isLoggingEnabled: Boolean + @Volatile var isLoggingEnabled: Boolean, + coroutineContext: CoroutineContext = Dispatchers.Main.immediate, ) : LifecycleObserver { @get:JvmName("-requestTransformers") internal val requestTransformers: List = requestTransformers.toList() @@ -101,6 +107,8 @@ class Picasso internal constructor( @set:JvmName("-shutdown") internal var shutdown = false + internal val scope = CoroutineScope(SupervisorJob() + coroutineContext) + init { // Adjust this and Builder(Picasso) as internal handlers are added or removed. val builtInHandlers = 8 @@ -361,6 +369,7 @@ class Picasso internal constructor( return } cache.clear() + scope.cancel() close() diff --git a/picasso/src/main/java/com/squareup/picasso3/Request.kt b/picasso/src/main/java/com/squareup/picasso3/Request.kt index f1e9d0b4c4..d53ae05153 100644 --- a/picasso/src/main/java/com/squareup/picasso3/Request.kt +++ b/picasso/src/main/java/com/squareup/picasso3/Request.kt @@ -276,6 +276,7 @@ class Request internal constructor(builder: Builder) { var stableKey: String? = null var targetWidth = 0 var targetHeight = 0 + var sizeSpec: SizeSpec = SizeSpec.Unspecified var centerCrop = false var centerCropGravity = 0 var centerInside = false @@ -389,6 +390,10 @@ class Request internal constructor(builder: Builder) { tag = null } + fun sizeSpec(spec: SizeSpec) { + sizeSpec = spec + } + /** * Resize the image to the specified size in pixels. * Use 0 as desired dimension to resize keeping aspect ratio. @@ -555,7 +560,13 @@ class Request internal constructor(builder: Builder) { } /** Create the immutable [Request] object. */ - fun build(): Request { + suspend fun build(): Request { + val size = sizeSpec.resolve() + if (size is SizeSpec.Size.Exact) { + targetWidth = size.width + targetHeight = size.height + } + check(!(centerInside && centerCrop)) { "Center crop and center inside can not be used together." } diff --git a/picasso/src/main/java/com/squareup/picasso3/RequestCreator.kt b/picasso/src/main/java/com/squareup/picasso3/RequestCreator.kt index bbe65ffcda..6705cc61f7 100644 --- a/picasso/src/main/java/com/squareup/picasso3/RequestCreator.kt +++ b/picasso/src/main/java/com/squareup/picasso3/RequestCreator.kt @@ -42,6 +42,8 @@ import com.squareup.picasso3.Utils.checkNotMain import com.squareup.picasso3.Utils.log import java.io.IOException import java.util.concurrent.atomic.AtomicInteger +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking /** Fluent API for building an image download request. */ class RequestCreator internal constructor( @@ -58,6 +60,7 @@ class RequestCreator internal constructor( @DrawableRes private var errorResId = 0 private var placeholderDrawable: Drawable? = null private var errorDrawable: Drawable? = null + private var sizeSpec: SizeSpec = SizeSpec.Unspecified /** Internal use only. Used by [DeferredRequestCreator]. */ @get:JvmName("-tag") @@ -155,6 +158,7 @@ class RequestCreator internal constructor( * *Note:* This method works only when your target is an [ImageView]. */ fun fit(): RequestCreator { + check(sizeSpec == SizeSpec.Unspecified) { "Use only one of fit() or sizeSpec()." } deferred = true return this } @@ -173,6 +177,11 @@ class RequestCreator internal constructor( return this } + fun sizeSpec(spec: SizeSpec) { + check(!deferred) { "Use only one of fit() or sizeSpec()." } + sizeSpec = spec + } + /** * Resize the image to the specified dimension size. * Use 0 as desired dimension to resize keeping aspect ratio. @@ -354,7 +363,7 @@ class RequestCreator internal constructor( return null } - val request = createRequest(started) + val request = runBlocking { createRequest(started) } val action = GetAction(picasso, request) val result = forRequest(picasso, picasso.dispatcher, picasso.cache, action).hunt() ?: return null @@ -387,20 +396,22 @@ class RequestCreator internal constructor( data.priority(Picasso.Priority.LOW) } - val request = createRequest(started) - if (shouldReadFromMemoryCache(request.memoryPolicy)) { - val bitmap = picasso.quickMemoryCacheCheck(request.key) - if (bitmap != null) { - if (picasso.isLoggingEnabled) { - log(OWNER_MAIN, VERB_COMPLETED, request.plainId(), "from " + LoadedFrom.MEMORY) + picasso.scope.launch { + val request = createRequest(started) + if (shouldReadFromMemoryCache(request.memoryPolicy)) { + val bitmap = picasso.quickMemoryCacheCheck(request.key) + if (bitmap != null) { + if (picasso.isLoggingEnabled) { + log(OWNER_MAIN, VERB_COMPLETED, request.plainId(), "from " + LoadedFrom.MEMORY) + } + callback?.onSuccess() + return@launch } - callback?.onSuccess() - return } - } - val action = FetchAction(picasso, request, callback) - picasso.submit(action) + val action = FetchAction(picasso, request, callback) + picasso.submit(action) + } } } @@ -437,19 +448,21 @@ class RequestCreator internal constructor( return } - val request = createRequest(started) - if (shouldReadFromMemoryCache(request.memoryPolicy)) { - val bitmap = picasso.quickMemoryCacheCheck(request.key) - if (bitmap != null) { - picasso.cancelRequest(target) - target.onBitmapLoaded(bitmap, LoadedFrom.MEMORY) - return + picasso.scope.launch { + val request = createRequest(started) + if (shouldReadFromMemoryCache(request.memoryPolicy)) { + val bitmap = picasso.quickMemoryCacheCheck(request.key) + if (bitmap != null) { + picasso.cancelRequest(target) + target.onBitmapLoaded(bitmap, LoadedFrom.MEMORY) + return@launch + } } - } - target.onPrepareLoad(if (setPlaceholder) getPlaceholderDrawable() else null) - val action = BitmapTargetAction(picasso, target, request, errorDrawable, errorResId) - picasso.enqueueAndSubmit(action) + target.onPrepareLoad(if (setPlaceholder) getPlaceholderDrawable() else null) + val action = BitmapTargetAction(picasso, target, request, errorDrawable, errorResId) + picasso.enqueueAndSubmit(action) + } } /** @@ -471,18 +484,20 @@ class RequestCreator internal constructor( "Cannot use placeholder or error drawables with remote views." } - val request = createRequest(started) - val action = NotificationAction( - picasso, - request, - errorResId, - RemoteViewsTarget(remoteViews, viewId), - notificationId, - notification, - notificationTag, - callback - ) - performRemoteViewInto(request, action) + picasso.scope.launch { + val request = createRequest(started) + val action = NotificationAction( + picasso, + request, + errorResId, + RemoteViewsTarget(remoteViews, viewId), + notificationId, + notification, + notificationTag, + callback + ) + performRemoteViewInto(request, action) + } } /** @@ -515,17 +530,19 @@ class RequestCreator internal constructor( "Cannot use placeholder or error drawables with remote views." } - val request = createRequest(started) - val action = AppWidgetAction( - picasso, - request, - errorResId, - RemoteViewsTarget(remoteViews, viewId), - appWidgetIds, - callback - ) - - performRemoteViewInto(request, action) + picasso.scope.launch { + val request = createRequest(started) + val action = AppWidgetAction( + picasso, + request, + errorResId, + RemoteViewsTarget(remoteViews, viewId), + appWidgetIds, + callback + ) + + performRemoteViewInto(request, action) + } } /** @@ -552,50 +569,43 @@ class RequestCreator internal constructor( } if (deferred) { - check(!data.hasSize()) { "Fit cannot be used with resize." } - val width = target.width - val height = target.height - if (width == 0 || height == 0) { - if (setPlaceholder) { - setPlaceholder(target, getPlaceholderDrawable()) - } - picasso.defer(target, DeferredRequestCreator(this, target, callback)) - return - } - data.resize(width, height) + sizeSpec = ImageViewSizeSpec(target) } - val request = createRequest(started) + picasso.scope.launch { + val request = createRequest(started) - if (shouldReadFromMemoryCache(request.memoryPolicy)) { - val bitmap = picasso.quickMemoryCacheCheck(request.key) - if (bitmap != null) { - picasso.cancelRequest(target) - val result: RequestHandler.Result = RequestHandler.Result.Bitmap(bitmap, LoadedFrom.MEMORY) - setResult(target, picasso.context, result, noFade, picasso.indicatorsEnabled) - if (picasso.isLoggingEnabled) { - log(OWNER_MAIN, VERB_COMPLETED, request.plainId(), "from " + LoadedFrom.MEMORY) + if (shouldReadFromMemoryCache(request.memoryPolicy)) { + val bitmap = picasso.quickMemoryCacheCheck(request.key) + if (bitmap != null) { + picasso.cancelRequest(target) + val result: RequestHandler.Result = + RequestHandler.Result.Bitmap(bitmap, LoadedFrom.MEMORY) + setResult(target, picasso.context, result, noFade, picasso.indicatorsEnabled) + if (picasso.isLoggingEnabled) { + log(OWNER_MAIN, VERB_COMPLETED, request.plainId(), "from " + LoadedFrom.MEMORY) + } + callback?.onSuccess() + return@launch } - callback?.onSuccess() - return } - } - - if (setPlaceholder) { - setPlaceholder(target, getPlaceholderDrawable()) - } - val action = ImageViewAction( - picasso, - target, - request, - errorDrawable, - errorResId, - noFade, - callback - ) + if (setPlaceholder) { + setPlaceholder(target, getPlaceholderDrawable()) + } - picasso.enqueueAndSubmit(action) + val action = ImageViewAction( + picasso, + target, + request, + errorDrawable, + errorResId, + noFade, + callback + ) + + picasso.enqueueAndSubmit(action) + } } private fun getPlaceholderDrawable(): Drawable? { @@ -607,7 +617,7 @@ class RequestCreator internal constructor( } /** Create the request optionally passing it through the request transformer. */ - private fun createRequest(started: Long): Request { + private suspend fun createRequest(started: Long): Request { val id = nextId.getAndIncrement() val request = data.build() request.id = id diff --git a/picasso/src/main/java/com/squareup/picasso3/SizeSpec.kt b/picasso/src/main/java/com/squareup/picasso3/SizeSpec.kt new file mode 100644 index 0000000000..da8f0cda4a --- /dev/null +++ b/picasso/src/main/java/com/squareup/picasso3/SizeSpec.kt @@ -0,0 +1,83 @@ +package com.squareup.picasso3 + +import android.view.View +import android.view.ViewTreeObserver +import androidx.annotation.Px +import com.squareup.picasso3.SizeSpec.Size +import kotlin.coroutines.resume +import kotlinx.coroutines.suspendCancellableCoroutine + +fun interface SizeSpec { + + suspend fun resolve(): Size + + sealed interface Size { + object Unspecified : Size + data class Exact(@Px val width: Int, @Px val height: Int) : Size + } + + object Unspecified : SizeSpec { + override suspend fun resolve() = Size.Unspecified + } +} + +internal class ImageViewSizeSpec( + private val view: View, +) : SizeSpec { + + override suspend fun resolve(): Size { + + val startingWidth = view.width + val startingHeight = view.height + + if (startingWidth > 0 && startingHeight > 0) { + return Size.Exact(startingWidth, startingHeight) + } + + return suspendCancellableCoroutine { continuation -> + + val vto = view.viewTreeObserver + lateinit var attachStateChangeListener: View.OnAttachStateChangeListener + + val preDrawListener = object : ViewTreeObserver.OnPreDrawListener { + override fun onPreDraw(): Boolean { + if (!vto.isAlive) return true + + val width = view.width + val height = view.height + + if (width > 0 && height > 0) { + view.removeOnAttachStateChangeListener(attachStateChangeListener) + vto.removeOnPreDrawListener(this) + continuation.resume(Size.Exact(width, height)) + } + + return true + } + } + + attachStateChangeListener = object : View.OnAttachStateChangeListener { + override fun onViewAttachedToWindow(view: View) { + view.viewTreeObserver.addOnPreDrawListener(preDrawListener) + } + + override fun onViewDetachedFromWindow(view: View) { + view.viewTreeObserver.removeOnPreDrawListener(preDrawListener) + } + } + + view.addOnAttachStateChangeListener(attachStateChangeListener) + + // Only add the pre-draw listener if the view is already attached. + // See: https://github.com/square/picasso/issues/1321 + if (view.windowToken != null) { + attachStateChangeListener.onViewAttachedToWindow(view) + } + + continuation.invokeOnCancellation { + view.removeOnAttachStateChangeListener(attachStateChangeListener) + vto.removeOnPreDrawListener(preDrawListener) + } + } + } +}