diff options
Diffstat (limited to 'java/src')
8 files changed, 203 insertions, 31 deletions
diff --git a/java/src/com/android/intentresolver/ChooserActivity.java b/java/src/com/android/intentresolver/ChooserActivity.java index 843ae809..9b4582df 100644 --- a/java/src/com/android/intentresolver/ChooserActivity.java +++ b/java/src/com/android/intentresolver/ChooserActivity.java @@ -303,9 +303,15 @@ public class ChooserActivity extends Hilt_ChooserActivity implements BasePreviewViewModel previewViewModel = new ViewModelProvider(this, createPreviewViewModelFactory()) .get(BasePreviewViewModel.class); + previewViewModel.init( + mChooserRequest.getTargetIntent(), + getIntent(), + /*additionalContentUri = */ null, + /*focusedItemIdx = */ 0, + /*isPayloadTogglingEnabled = */ false); mChooserContentPreviewUi = new ChooserContentPreviewUi( getCoroutineScope(getLifecycle()), - previewViewModel.createOrReuseProvider(mChooserRequest.getTargetIntent()), + previewViewModel.getPreviewDataProvider(), mChooserRequest.getTargetIntent(), previewViewModel.getImageLoader(), createChooserActionFactory(), diff --git a/java/src/com/android/intentresolver/contentpreview/BasePreviewViewModel.kt b/java/src/com/android/intentresolver/contentpreview/BasePreviewViewModel.kt index 3b20a45c..21c909ea 100644 --- a/java/src/com/android/intentresolver/contentpreview/BasePreviewViewModel.kt +++ b/java/src/com/android/intentresolver/contentpreview/BasePreviewViewModel.kt @@ -17,14 +17,22 @@ package com.android.intentresolver.contentpreview import android.content.Intent +import android.net.Uri import androidx.annotation.MainThread import androidx.lifecycle.ViewModel /** A contract for the preview view model. Added for testing. */ abstract class BasePreviewViewModel : ViewModel() { - @MainThread abstract fun createOrReuseProvider(targetIntent: Intent): PreviewDataProvider - - abstract val imageLoader: ImageLoader - + @get:MainThread abstract val previewDataProvider: PreviewDataProvider + @get:MainThread abstract val imageLoader: ImageLoader abstract val payloadToggleInteractor: PayloadToggleInteractor? + + @MainThread + abstract fun init( + targetIntent: Intent, + chooserIntent: Intent, + additionalContentUri: Uri?, + focusedItemIdx: Int, + isPayloadTogglingEnabled: Boolean, + ) } diff --git a/java/src/com/android/intentresolver/contentpreview/CursorUriReader.kt b/java/src/com/android/intentresolver/contentpreview/CursorUriReader.kt index dbf27a88..7ff3b49e 100644 --- a/java/src/com/android/intentresolver/contentpreview/CursorUriReader.kt +++ b/java/src/com/android/intentresolver/contentpreview/CursorUriReader.kt @@ -16,10 +16,22 @@ package com.android.intentresolver.contentpreview +import android.content.ContentInterface +import android.content.Intent import android.database.Cursor +import android.database.MatrixCursor import android.net.Uri +import android.os.Bundle +import android.os.CancellationSignal import android.util.Log import android.util.SparseArray +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.coroutineScope + +// TODO: replace with the new API AdditionalContentContract$Columns#URI +private const val ColumnUri = "uri" +// TODO: replace with the new API AdditionalContentContract$CursorExtraKeys#POSITION +private const val ExtraPosition = "position" private const val TAG = ContentPreviewUi.TAG @@ -98,4 +110,38 @@ class CursorUriReader( override fun close() { cursor.close() } + + companion object { + suspend fun createCursorReader( + contentResolver: ContentInterface, + uri: Uri, + chooserIntent: Intent + ): CursorUriReader { + val cancellationSignal = CancellationSignal() + val cursor = + try { + coroutineScope { + runCatching { + contentResolver.query( + uri, + arrayOf(ColumnUri), + Bundle().apply { + putParcelable(Intent.EXTRA_INTENT, chooserIntent) + }, + cancellationSignal + ) + } + .getOrNull() + ?: MatrixCursor(arrayOf(ColumnUri)) + } + } catch (e: CancellationException) { + cancellationSignal.cancel() + throw e + } + return CursorUriReader(cursor, cursor.extras?.getInt(ExtraPosition, 0) ?: 0, 128) { + // TODO: check that authority is case-sensitive for resolution reasons + it.authority != uri.authority + } + } + } } diff --git a/java/src/com/android/intentresolver/contentpreview/PreviewDataProvider.kt b/java/src/com/android/intentresolver/contentpreview/PreviewDataProvider.kt index 38918d79..659f7dc9 100644 --- a/java/src/com/android/intentresolver/contentpreview/PreviewDataProvider.kt +++ b/java/src/com/android/intentresolver/contentpreview/PreviewDataProvider.kt @@ -100,6 +100,9 @@ constructor( open val uriCount: Int get() = records.size + val uris: List<Uri> + get() = records.map { it.uri } + /** * Returns a [Flow] of [FileInfo], for each shared URI in order, with [FileInfo.mimeType] and * [FileInfo.previewUri] set (a data projection tailored for the image preview UI). diff --git a/java/src/com/android/intentresolver/contentpreview/PreviewViewModel.kt b/java/src/com/android/intentresolver/contentpreview/PreviewViewModel.kt index 77cf0ac9..7369fa0f 100644 --- a/java/src/com/android/intentresolver/contentpreview/PreviewViewModel.kt +++ b/java/src/com/android/intentresolver/contentpreview/PreviewViewModel.kt @@ -17,7 +17,9 @@ package com.android.intentresolver.contentpreview import android.app.Application +import android.content.ContentResolver import android.content.Intent +import android.net.Uri import androidx.annotation.MainThread import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider @@ -26,46 +28,90 @@ import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewmodel.CreationExtras import com.android.intentresolver.R import com.android.intentresolver.inject.Background -import dagger.hilt.android.lifecycle.HiltViewModel -import javax.inject.Inject +import java.util.concurrent.Executors import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.asCoroutineDispatcher import kotlinx.coroutines.plus -/** A trivial view model to keep a [PreviewDataProvider] instance over a configuration change */ -@HiltViewModel -class PreviewViewModel -@Inject -constructor( - private val application: Application, +/** A view model for the preview logic */ +class PreviewViewModel( + private val contentResolver: ContentResolver, + // TODO: inject ImageLoader instead + private val thumbnailSize: Int, @Background private val dispatcher: CoroutineDispatcher = Dispatchers.IO, ) : BasePreviewViewModel() { - private var previewDataProvider: PreviewDataProvider? = null + private var targetIntent: Intent? = null + private var chooserIntent: Intent? = null + private var additionalContentUri: Uri? = null + private var focusedItemIdx: Int = 0 + private var isPayloadTogglingEnabled = false - @MainThread - override fun createOrReuseProvider(targetIntent: Intent): PreviewDataProvider = - previewDataProvider - ?: PreviewDataProvider( - viewModelScope + dispatcher, - targetIntent, - application.contentResolver - ) - .also { previewDataProvider = it } + override val previewDataProvider by lazy { + val targetIntent = requireNotNull(this.targetIntent) { "Not initialized" } + PreviewDataProvider(viewModelScope + dispatcher, targetIntent, contentResolver) + } override val imageLoader by lazy { ImagePreviewImageLoader( viewModelScope + dispatcher, - thumbnailSize = - application.resources.getDimensionPixelSize( - R.dimen.chooser_preview_image_max_dimen - ), - application.contentResolver, + thumbnailSize, + contentResolver, cacheSize = 16 ) } override val payloadToggleInteractor: PayloadToggleInteractor? by lazy { - null // TODO: initialize PayloadToggleInteractor() + val targetIntent = requireNotNull(targetIntent) { "Not initialized" } + // TODO: replace with flags injection + if (!isPayloadTogglingEnabled) return@lazy null + createPayloadToggleInteractor( + additionalContentUri ?: return@lazy null, + targetIntent, + chooserIntent ?: return@lazy null, + ) + .apply { start() } + } + + // TODO: make the view model injectable and inject these dependencies instead + @MainThread + override fun init( + targetIntent: Intent, + chooserIntent: Intent, + additionalContentUri: Uri?, + focusedItemIdx: Int, + isPayloadTogglingEnabled: Boolean, + ) { + if (this.targetIntent != null) return + this.targetIntent = targetIntent + this.chooserIntent = chooserIntent + this.additionalContentUri = additionalContentUri + this.focusedItemIdx = focusedItemIdx + this.isPayloadTogglingEnabled = isPayloadTogglingEnabled + } + + private fun createPayloadToggleInteractor( + contentProviderUri: Uri, + targetIntent: Intent, + chooserIntent: Intent, + ): PayloadToggleInteractor { + return PayloadToggleInteractor( + // TODO: update PayloadToggleInteractor to support multiple threads + viewModelScope + Executors.newSingleThreadScheduledExecutor().asCoroutineDispatcher(), + previewDataProvider.uris, + maxOf(0, minOf(focusedItemIdx, previewDataProvider.uriCount - 1)), + DefaultMimeTypeClassifier, + { + CursorUriReader.createCursorReader( + contentResolver, + contentProviderUri, + chooserIntent + ) + }, + UriMetadataReader(contentResolver, DefaultMimeTypeClassifier), + TargetIntentModifier(targetIntent, getUri = { uri }, getMimeType = { mimeType }), + SelectionChangeCallback(contentProviderUri, chooserIntent, contentResolver) + ) } companion object { @@ -75,7 +121,16 @@ constructor( override fun <T : ViewModel> create( modelClass: Class<T>, extras: CreationExtras - ): T = PreviewViewModel(checkNotNull(extras[APPLICATION_KEY])) as T + ): T { + val application: Application = checkNotNull(extras[APPLICATION_KEY]) + return PreviewViewModel( + application.contentResolver, + application.resources.getDimensionPixelSize( + R.dimen.chooser_preview_image_max_dimen + ) + ) + as T + } } } } diff --git a/java/src/com/android/intentresolver/contentpreview/TargetIntentModifier.kt b/java/src/com/android/intentresolver/contentpreview/TargetIntentModifier.kt index 99cfc0f8..d7e04920 100644 --- a/java/src/com/android/intentresolver/contentpreview/TargetIntentModifier.kt +++ b/java/src/com/android/intentresolver/contentpreview/TargetIntentModifier.kt @@ -16,6 +16,7 @@ package com.android.intentresolver.contentpreview +import android.content.ClipData import android.content.ClipDescription.compareMimeTypes import android.content.Intent import android.content.Intent.ACTION_SEND @@ -45,6 +46,12 @@ class TargetIntentModifier<Item>( } else { putParcelableArrayListExtra(EXTRA_STREAM, uris) } + clipData = + ClipData("", arrayOf(targetMimeType), ClipData.Item(uris[0])).also { + for (i in 1 until uris.size) { + it.addItem(ClipData.Item(uris[i])) + } + } } } diff --git a/java/src/com/android/intentresolver/contentpreview/UriMetadataReader.kt b/java/src/com/android/intentresolver/contentpreview/UriMetadataReader.kt new file mode 100644 index 00000000..784cefa0 --- /dev/null +++ b/java/src/com/android/intentresolver/contentpreview/UriMetadataReader.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2024 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 android.content.ContentResolver +import android.net.Uri + +// TODO: share this logic with PreviewDataProvider +class UriMetadataReader( + private val contentResolver: ContentResolver, + private val mimeTypeClassifier: MimeTypeClassifier, +) : (Uri) -> FileInfo { + fun getMetadata(uri: Uri): FileInfo = + FileInfo.Builder(uri) + .apply { + runCatching { + withMimeType(contentResolver.getType(uri)) + if (mimeTypeClassifier.isImageType(mimeType)) { + withPreviewUri(uri) + } + } + } + .build() + + override fun invoke(uri: Uri): FileInfo = getMetadata(uri) +} diff --git a/java/src/com/android/intentresolver/v2/ChooserActivity.java b/java/src/com/android/intentresolver/v2/ChooserActivity.java index 30845818..2ffd31d8 100644 --- a/java/src/com/android/intentresolver/v2/ChooserActivity.java +++ b/java/src/com/android/intentresolver/v2/ChooserActivity.java @@ -268,6 +268,7 @@ public class ChooserActivity extends Hilt_ChooserActivity implements @Inject public ActivityLaunch mActivityLaunch; @Inject public FeatureFlags mFeatureFlags; + @Inject public android.service.chooser.FeatureFlags mChooserServiceFeatureFlags; @Inject public EventLog mEventLog; @Inject @AppPredictionAvailable public boolean mAppPredictionAvailable; @Inject @ImageEditor public Optional<ComponentName> mImageEditor; @@ -480,9 +481,15 @@ public class ChooserActivity extends Hilt_ChooserActivity implements BasePreviewViewModel previewViewModel = new ViewModelProvider(this, createPreviewViewModelFactory()) .get(BasePreviewViewModel.class); + previewViewModel.init( + chooserRequest.getTargetIntent(), + getIntent(), + chooserRequest.getAdditionalContentUri(), + chooserRequest.getFocusedItemPosition(), + mChooserServiceFeatureFlags.chooserPayloadToggling()); mChooserContentPreviewUi = new ChooserContentPreviewUi( getCoroutineScope(getLifecycle()), - previewViewModel.createOrReuseProvider(chooserRequest.getTargetIntent()), + previewViewModel.getPreviewDataProvider(), chooserRequest.getTargetIntent(), previewViewModel.getImageLoader(), createChooserActionFactory(), |