diff options
author | 2023-05-12 16:57:29 +0000 | |
---|---|---|
committer | 2023-05-12 16:57:29 +0000 | |
commit | f368862ad60cfaf677d117d67caae94d7d0d1977 (patch) | |
tree | 25cd762006dd88fe3d9628e7de50ec79f4ed5b24 /java/src | |
parent | 4b19dbc551e361d6407aca7e146bda0b6a3a30fb (diff) | |
parent | 6df7212d1017517efd372845411eebee3d8c8580 (diff) |
Merge "Retain the image loader through configuration change" into udc-dev am: 6df7212d10
Original change: https://googleplex-android-review.googlesource.com/c/platform/packages/modules/IntentResolver/+/23192997
Change-Id: Idb07640d3a041e7c873796c6660835155f024a36
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
Diffstat (limited to 'java/src')
-rw-r--r-- | java/src/com/android/intentresolver/ChooserActivity.java | 21 | ||||
-rw-r--r-- | java/src/com/android/intentresolver/contentpreview/BasePreviewViewModel.kt | 31 | ||||
-rw-r--r-- | java/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUi.java | 4 | ||||
-rw-r--r-- | java/src/com/android/intentresolver/contentpreview/FilesPlusTextContentPreviewUi.java | 21 | ||||
-rw-r--r-- | java/src/com/android/intentresolver/contentpreview/ImageLoader.kt | 19 | ||||
-rw-r--r-- | java/src/com/android/intentresolver/contentpreview/ImagePreviewImageLoader.kt (renamed from java/src/com/android/intentresolver/ImagePreviewImageLoader.kt) | 88 | ||||
-rw-r--r-- | java/src/com/android/intentresolver/contentpreview/PreviewViewModel.kt | 40 | ||||
-rw-r--r-- | java/src/com/android/intentresolver/contentpreview/TextContentPreviewUi.java | 5 |
8 files changed, 139 insertions, 90 deletions
diff --git a/java/src/com/android/intentresolver/ChooserActivity.java b/java/src/com/android/intentresolver/ChooserActivity.java index 0786e088..014aa2a2 100644 --- a/java/src/com/android/intentresolver/ChooserActivity.java +++ b/java/src/com/android/intentresolver/ChooserActivity.java @@ -83,9 +83,9 @@ import com.android.intentresolver.NoCrossProfileEmptyStateProvider.DevicePolicyB import com.android.intentresolver.chooser.DisplayResolveInfo; import com.android.intentresolver.chooser.MultiDisplayResolveInfo; import com.android.intentresolver.chooser.TargetInfo; +import com.android.intentresolver.contentpreview.BasePreviewViewModel; import com.android.intentresolver.contentpreview.ChooserContentPreviewUi; import com.android.intentresolver.contentpreview.HeadlineGeneratorImpl; -import com.android.intentresolver.contentpreview.ImageLoader; import com.android.intentresolver.contentpreview.PreviewViewModel; import com.android.intentresolver.flags.FeatureFlagRepository; import com.android.intentresolver.flags.FeatureFlagRepositoryFactory; @@ -272,13 +272,14 @@ public class ChooserActivity extends ResolverActivity implements } }); + BasePreviewViewModel previewViewModel = + new ViewModelProvider(this, createPreviewViewModelFactory()) + .get(BasePreviewViewModel.class); mChooserContentPreviewUi = new ChooserContentPreviewUi( getLifecycle(), - new ViewModelProvider(this, PreviewViewModel.Companion.getFactory()) - .get(PreviewViewModel.class) - .createOrReuseProvider(mChooserRequest), + previewViewModel.createOrReuseProvider(mChooserRequest), mChooserRequest.getTargetIntent(), - createPreviewImageLoader(), + previewViewModel.createOrReuseImageLoader(), createChooserActionFactory(), mEnterTransitionAnimationDelegate, new HeadlineGeneratorImpl(this)); @@ -1314,14 +1315,8 @@ public class ChooserActivity extends ResolverActivity implements } @VisibleForTesting - protected ImageLoader createPreviewImageLoader() { - final int cacheSize; - float chooserWidth = getResources().getDimension(R.dimen.chooser_width); - // imageWidth = imagePreviewHeight * minAspectRatio (see ScrollableImagePreviewView) - float imageWidth = - getResources().getDimension(R.dimen.chooser_preview_image_height_tall) * 2 / 5; - cacheSize = (int) (Math.ceil(chooserWidth / imageWidth) + 2); - return new ImagePreviewImageLoader(this, getLifecycle(), cacheSize); + protected ViewModelProvider.Factory createPreviewViewModelFactory() { + return PreviewViewModel.Companion.getFactory(); } private ChooserActionFactory createChooserActionFactory() { diff --git a/java/src/com/android/intentresolver/contentpreview/BasePreviewViewModel.kt b/java/src/com/android/intentresolver/contentpreview/BasePreviewViewModel.kt new file mode 100644 index 00000000..103e8bf4 --- /dev/null +++ b/java/src/com/android/intentresolver/contentpreview/BasePreviewViewModel.kt @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.intentresolver.contentpreview + +import androidx.annotation.MainThread +import androidx.lifecycle.ViewModel +import com.android.intentresolver.ChooserRequestParameters + +/** A contract for the preview view model. Added for testing. */ +abstract class BasePreviewViewModel : ViewModel() { + @MainThread + abstract fun createOrReuseProvider( + chooserRequest: ChooserRequestParameters + ): PreviewDataProvider + + @MainThread abstract fun createOrReuseImageLoader(): ImageLoader +} diff --git a/java/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUi.java index 787af95f..9100b392 100644 --- a/java/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUi.java +++ b/java/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUi.java @@ -118,6 +118,7 @@ public final class ChooserContentPreviewUi { int previewType = previewData.getPreviewType(); if (previewType == CONTENT_PREVIEW_TEXT) { return createTextPreview( + mLifecycle, targetIntent, actionFactory, imageLoader, @@ -140,6 +141,7 @@ public final class ChooserContentPreviewUi { if (!TextUtils.isEmpty(text)) { FilesPlusTextContentPreviewUi previewUi = new FilesPlusTextContentPreviewUi( + mLifecycle, isSingleImageShare, previewData.getUriCount(), targetIntent.getCharSequenceExtra(Intent.EXTRA_TEXT), @@ -180,6 +182,7 @@ public final class ChooserContentPreviewUi { } private static TextContentPreviewUi createTextPreview( + Lifecycle lifecycle, Intent targetIntent, ChooserContentPreviewUi.ActionFactory actionFactory, ImageLoader imageLoader, @@ -195,6 +198,7 @@ public final class ChooserContentPreviewUi { } } return new TextContentPreviewUi( + lifecycle, sharingText, previewTitle, previewThumbnail, diff --git a/java/src/com/android/intentresolver/contentpreview/FilesPlusTextContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/FilesPlusTextContentPreviewUi.java index 860423c4..e4e33839 100644 --- a/java/src/com/android/intentresolver/contentpreview/FilesPlusTextContentPreviewUi.java +++ b/java/src/com/android/intentresolver/contentpreview/FilesPlusTextContentPreviewUi.java @@ -31,6 +31,7 @@ import android.widget.ImageView; import android.widget.TextView; import androidx.annotation.Nullable; +import androidx.lifecycle.Lifecycle; import com.android.intentresolver.R; import com.android.intentresolver.widget.ActionRow; @@ -48,6 +49,7 @@ import java.util.function.Consumer; * file content). */ class FilesPlusTextContentPreviewUi extends ContentPreviewUi { + private final Lifecycle mLifecycle; private final CharSequence mText; private final ChooserContentPreviewUi.ActionFactory mActionFactory; private final ImageLoader mImageLoader; @@ -63,6 +65,7 @@ class FilesPlusTextContentPreviewUi extends ContentPreviewUi { private boolean mAllVideos; FilesPlusTextContentPreviewUi( + Lifecycle lifecycle, boolean isSingleImage, int fileCount, CharSequence text, @@ -70,6 +73,7 @@ class FilesPlusTextContentPreviewUi extends ContentPreviewUi { ImageLoader imageLoader, MimeTypeClassifier typeClassifier, HeadlineGenerator headlineGenerator) { + mLifecycle = lifecycle; if (isSingleImage && fileCount != 1) { throw new IllegalArgumentException( "fileCount = " + fileCount + " and isSingleImage = true"); @@ -155,13 +159,16 @@ class FilesPlusTextContentPreviewUi extends ContentPreviewUi { ImageView imagePreview = mContentPreviewView.requireViewById(R.id.image_view); if (mIsSingleImage && mFirstFilePreviewUri != null) { - mImageLoader.loadImage(mFirstFilePreviewUri, bitmap -> { - if (bitmap == null) { - imagePreview.setVisibility(View.GONE); - } else { - imagePreview.setImageBitmap(bitmap); - } - }); + mImageLoader.loadImage( + mLifecycle, + mFirstFilePreviewUri, + bitmap -> { + if (bitmap == null) { + imagePreview.setVisibility(View.GONE); + } else { + imagePreview.setImageBitmap(bitmap); + } + }); } else { imagePreview.setVisibility(View.GONE); } diff --git a/java/src/com/android/intentresolver/contentpreview/ImageLoader.kt b/java/src/com/android/intentresolver/contentpreview/ImageLoader.kt index 225807ee..8d0fb84b 100644 --- a/java/src/com/android/intentresolver/contentpreview/ImageLoader.kt +++ b/java/src/com/android/intentresolver/contentpreview/ImageLoader.kt @@ -18,32 +18,29 @@ package com.android.intentresolver.contentpreview import android.graphics.Bitmap import android.net.Uri +import androidx.lifecycle.Lifecycle import java.util.function.Consumer -/** - * A content preview image loader. - */ +/** A content preview image loader. */ interface ImageLoader : suspend (Uri) -> Bitmap?, suspend (Uri, Boolean) -> Bitmap? { /** * Load preview image asynchronously; caching is allowed. + * * @param uri content URI * @param callback a callback that will be invoked with the loaded image or null if loading has - * failed. + * failed. */ - fun loadImage(uri: Uri, callback: Consumer<Bitmap?>) + fun loadImage(callerLifecycle: Lifecycle, uri: Uri, callback: Consumer<Bitmap?>) - /** - * Prepopulate the image loader cache. - */ + /** Prepopulate the image loader cache. */ fun prePopulate(uris: List<Uri>) - /** - * Load preview image; caching is allowed. - */ + /** Load preview image; caching is allowed. */ override suspend fun invoke(uri: Uri) = invoke(uri, true) /** * Load preview image. + * * @param uri content URI * @param caching indicates if the loaded image could be cached. */ diff --git a/java/src/com/android/intentresolver/ImagePreviewImageLoader.kt b/java/src/com/android/intentresolver/contentpreview/ImagePreviewImageLoader.kt index c97efdd1..89b79a0a 100644 --- a/java/src/com/android/intentresolver/ImagePreviewImageLoader.kt +++ b/java/src/com/android/intentresolver/contentpreview/ImagePreviewImageLoader.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 The Android Open Source Project + * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,9 +14,9 @@ * limitations under the License. */ -package com.android.intentresolver +package com.android.intentresolver.contentpreview -import android.content.Context +import android.content.ContentResolver import android.graphics.Bitmap import android.net.Uri import android.util.Log @@ -26,12 +26,10 @@ import androidx.annotation.VisibleForTesting import androidx.collection.LruCache import androidx.lifecycle.Lifecycle import androidx.lifecycle.coroutineScope -import com.android.intentresolver.contentpreview.ImageLoader import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CompletableDeferred -import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Deferred -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import java.util.function.Consumer @@ -42,29 +40,24 @@ private const val TAG = "ImagePreviewImageLoader" * Implements preview image loading for the content preview UI. Provides requests deduplication and * image caching. */ -@VisibleForTesting -class ImagePreviewImageLoader @JvmOverloads constructor( - private val context: Context, - private val lifecycle: Lifecycle, +@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE) +class ImagePreviewImageLoader( + private val scope: CoroutineScope, + thumbnailSize: Int, + private val contentResolver: ContentResolver, cacheSize: Int, - private val dispatcher: CoroutineDispatcher = Dispatchers.IO ) : ImageLoader { - private val thumbnailSize: Size = - context.resources.getDimensionPixelSize(R.dimen.chooser_preview_image_max_dimen).let { - Size(it, it) - } + private val thumbnailSize: Size = Size(thumbnailSize, thumbnailSize) private val lock = Any() - @GuardedBy("lock") - private val cache = LruCache<Uri, RequestRecord>(cacheSize) - @GuardedBy("lock") - private val runningRequests = HashMap<Uri, RequestRecord>() + @GuardedBy("lock") private val cache = LruCache<Uri, RequestRecord>(cacheSize) + @GuardedBy("lock") private val runningRequests = HashMap<Uri, RequestRecord>() override suspend fun invoke(uri: Uri, caching: Boolean): Bitmap? = loadImageAsync(uri, caching) - override fun loadImage(uri: Uri, callback: Consumer<Bitmap?>) { - lifecycle.coroutineScope.launch { + override fun loadImage(callerLifecycle: Lifecycle, uri: Uri, callback: Consumer<Bitmap?>) { + callerLifecycle.coroutineScope.launch { val image = loadImageAsync(uri, caching = true) if (isActive) { callback.accept(image) @@ -74,28 +67,26 @@ class ImagePreviewImageLoader @JvmOverloads constructor( override fun prePopulate(uris: List<Uri>) { uris.asSequence().take(cache.maxSize()).forEach { uri -> - lifecycle.coroutineScope.launch { - loadImageAsync(uri, caching = true) - } + scope.launch { loadImageAsync(uri, caching = true) } } } private suspend fun loadImageAsync(uri: Uri, caching: Boolean): Bitmap? { - return getRequestDeferred(uri, caching) - .await() + return getRequestDeferred(uri, caching).await() } private fun getRequestDeferred(uri: Uri, caching: Boolean): Deferred<Bitmap?> { var shouldLaunchImageLoading = false - val request = synchronized(lock) { - cache[uri] - ?: runningRequests.getOrPut(uri) { - shouldLaunchImageLoading = true - RequestRecord(uri, CompletableDeferred(), caching) - }.apply { - this.caching = this.caching || caching - } - } + val request = + synchronized(lock) { + cache[uri] + ?: runningRequests + .getOrPut(uri) { + shouldLaunchImageLoading = true + RequestRecord(uri, CompletableDeferred(), caching) + } + .apply { this.caching = this.caching || caching } + } if (shouldLaunchImageLoading) { request.loadBitmapAsync() } @@ -103,22 +94,23 @@ class ImagePreviewImageLoader @JvmOverloads constructor( } private fun RequestRecord.loadBitmapAsync() { - lifecycle.coroutineScope.launch(dispatcher) { - loadBitmap() - }.invokeOnCompletion { cause -> - if (cause is CancellationException) { - cancel() + scope + .launch { loadBitmap() } + .invokeOnCompletion { cause -> + if (cause is CancellationException) { + cancel() + } } - } } private fun RequestRecord.loadBitmap() { - val bitmap = try { - context.contentResolver.loadThumbnail(uri, thumbnailSize, null) - } catch (t: Throwable) { - Log.d(TAG, "failed to load $uri preview", t) - null - } + val bitmap = + try { + contentResolver.loadThumbnail(uri, thumbnailSize, null) + } catch (t: Throwable) { + Log.d(TAG, "failed to load $uri preview", t) + null + } complete(bitmap) } @@ -144,4 +136,4 @@ class ImagePreviewImageLoader @JvmOverloads constructor( val deferred: CompletableDeferred<Bitmap?>, @GuardedBy("lock") var caching: Boolean ) -} +}
\ No newline at end of file diff --git a/java/src/com/android/intentresolver/contentpreview/PreviewViewModel.kt b/java/src/com/android/intentresolver/contentpreview/PreviewViewModel.kt index 2f4b0211..331b0cb6 100644 --- a/java/src/com/android/intentresolver/contentpreview/PreviewViewModel.kt +++ b/java/src/com/android/intentresolver/contentpreview/PreviewViewModel.kt @@ -16,24 +16,45 @@ package com.android.intentresolver.contentpreview -import android.content.ContentResolver -import android.content.Context +import android.app.Application +import androidx.annotation.MainThread import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY +import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewmodel.CreationExtras import com.android.intentresolver.ChooserRequestParameters +import com.android.intentresolver.R +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.plus /** A trivial view model to keep a [PreviewDataProvider] instance over a configuration change */ -class PreviewViewModel(private val contentResolver: ContentResolver) : ViewModel() { +class PreviewViewModel(private val application: Application) : BasePreviewViewModel() { private var previewDataProvider: PreviewDataProvider? = null + private var imageLoader: ImagePreviewImageLoader? = null - fun createOrReuseProvider(chooserRequest: ChooserRequestParameters): PreviewDataProvider { - return previewDataProvider - ?: PreviewDataProvider(chooserRequest.targetIntent, contentResolver).also { + @MainThread + override fun createOrReuseProvider( + chooserRequest: ChooserRequestParameters + ): PreviewDataProvider = + previewDataProvider + ?: PreviewDataProvider(chooserRequest.targetIntent, application.contentResolver).also { previewDataProvider = it } - } + + @MainThread + override fun createOrReuseImageLoader(): ImageLoader = + imageLoader + ?: ImagePreviewImageLoader( + viewModelScope + Dispatchers.IO, + thumbnailSize = + application.resources.getDimensionPixelSize( + R.dimen.chooser_preview_image_max_dimen + ), + application.contentResolver, + cacheSize = 16 + ) + .also { imageLoader = it } companion object { val Factory: ViewModelProvider.Factory = @@ -42,10 +63,7 @@ class PreviewViewModel(private val contentResolver: ContentResolver) : ViewModel override fun <T : ViewModel> create( modelClass: Class<T>, extras: CreationExtras - ): T = - PreviewViewModel( - (checkNotNull(extras[APPLICATION_KEY]) as Context).contentResolver - ) as T + ): T = PreviewViewModel(checkNotNull(extras[APPLICATION_KEY])) as T } } } diff --git a/java/src/com/android/intentresolver/contentpreview/TextContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/TextContentPreviewUi.java index 3c8a6e48..19fd3bb4 100644 --- a/java/src/com/android/intentresolver/contentpreview/TextContentPreviewUi.java +++ b/java/src/com/android/intentresolver/contentpreview/TextContentPreviewUi.java @@ -28,6 +28,7 @@ import android.widget.ImageView; import android.widget.TextView; import androidx.annotation.Nullable; +import androidx.lifecycle.Lifecycle; import com.android.intentresolver.R; import com.android.intentresolver.widget.ActionRow; @@ -36,6 +37,7 @@ import java.util.ArrayList; import java.util.List; class TextContentPreviewUi extends ContentPreviewUi { + private final Lifecycle mLifecycle; @Nullable private final CharSequence mSharingText; @Nullable @@ -47,12 +49,14 @@ class TextContentPreviewUi extends ContentPreviewUi { private final HeadlineGenerator mHeadlineGenerator; TextContentPreviewUi( + Lifecycle lifecycle, @Nullable CharSequence sharingText, @Nullable CharSequence previewTitle, @Nullable Uri previewThumbnail, ChooserContentPreviewUi.ActionFactory actionFactory, ImageLoader imageLoader, HeadlineGenerator headlineGenerator) { + mLifecycle = lifecycle; mSharingText = sharingText; mPreviewTitle = previewTitle; mPreviewThumbnail = previewThumbnail; @@ -117,6 +121,7 @@ class TextContentPreviewUi extends ContentPreviewUi { previewThumbnailView.setVisibility(View.GONE); } else { mImageLoader.loadImage( + mLifecycle, mPreviewThumbnail, (bitmap) -> updateViewWithImage( contentPreviewLayout.findViewById( |