diff options
author | 2024-10-01 10:14:31 -0700 | |
---|---|---|
committer | 2024-12-13 15:45:11 -0800 | |
commit | 319d59d29b4037246a72cc97e1353cc6fbda20d6 (patch) | |
tree | ee81575c468d1408d89b64aec354509d6b9d0fea /java/src/com | |
parent | effd3e6c1bba8d5d5ce8047b9a1e455816fcd4b3 (diff) |
An experimental interactive Chooser session implementation
Bug: 378493324
Test: manual testing with a test app
Test: atest IntentResolver-tests-unit
Test: atest IntentResolver-tests-activity
Flag: com.android.intentresolver.interactive_session
Change-Id: I15b303dd3912c63538930d39b7743e290adb480c
Diffstat (limited to 'java/src/com')
13 files changed, 451 insertions, 11 deletions
diff --git a/java/src/com/android/intentresolver/ChooserActivity.java b/java/src/com/android/intentresolver/ChooserActivity.java index 92f366ea..d81adfba 100644 --- a/java/src/com/android/intentresolver/ChooserActivity.java +++ b/java/src/com/android/intentresolver/ChooserActivity.java @@ -24,6 +24,7 @@ import static androidx.lifecycle.LifecycleKt.getCoroutineScope; import static com.android.intentresolver.ChooserActionFactory.EDIT_SOURCE; import static com.android.intentresolver.Flags.fixShortcutsFlashing; +import static com.android.intentresolver.Flags.interactiveSession; import static com.android.intentresolver.Flags.keyboardNavigationFix; import static com.android.intentresolver.Flags.rebuildAdaptersOnTargetPinning; import static com.android.intentresolver.Flags.refineSystemActions; @@ -60,6 +61,7 @@ import android.content.pm.ShortcutInfo; import android.content.res.Configuration; import android.database.Cursor; import android.graphics.Insets; +import android.graphics.Rect; import android.net.Uri; import android.os.Bundle; import android.os.StrictMode; @@ -143,6 +145,7 @@ import com.android.intentresolver.widget.ActionRow; import com.android.intentresolver.widget.ChooserNestedScrollView; import com.android.intentresolver.widget.ImagePreviewView; import com.android.intentresolver.widget.ResolverDrawerLayout; +import com.android.intentresolver.widget.ResolverDrawerLayoutExt; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.content.PackageMonitor; import com.android.internal.logging.MetricsLogger; @@ -463,6 +466,9 @@ public class ChooserActivity extends Hilt_ChooserActivity implements if (isFinishing()) { mLatencyTracker.onActionCancel(ACTION_LOAD_SHARE_SHEET); + if (interactiveSession() && mViewModel != null) { + mViewModel.getInteractiveSessionInteractor().endSession(); + } } mBackgroundThreadPoolExecutor.shutdownNow(); @@ -682,6 +688,11 @@ public class ChooserActivity extends Hilt_ChooserActivity implements mEnterTransitionAnimationDelegate.postponeTransition(); mInitialProfile = findSelectedProfile(); Tracer.INSTANCE.markLaunched(); + + if (isInteractiveSession()) { + configureInteractiveSessionWindow(); + updateInteractiveArea(); + } } private void maybeDisableRecentsScreenshot( @@ -721,6 +732,45 @@ public class ChooserActivity extends Hilt_ChooserActivity implements mChooserMultiProfilePagerAdapter.setTargetsEnabled(hasSelections); } + private void configureInteractiveSessionWindow() { + if (!isInteractiveSession()) { + Log.wtf(TAG, "Unexpected user of the method; should be an interactive session"); + return; + } + final Window window = getWindow(); + if (window == null) { + return; + } + window.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND); + window.addPrivateFlags(WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY); + } + + private void updateInteractiveArea() { + if (!isInteractiveSession()) { + Log.wtf(TAG, "Unexpected user of the method; should be an interactive session"); + return; + } + final View contentView = findViewById(android.R.id.content); + final ResolverDrawerLayout rdl = mResolverDrawerLayout; + if (contentView == null || rdl == null) { + return; + } + final Rect rect = new Rect(); + contentView.getViewTreeObserver().addOnComputeInternalInsetsListener((info) -> { + int oldTop = rect.top; + rdl.getBoundsInWindow(rect, true); + int left = rect.left; + int top = rect.top; + ResolverDrawerLayoutExt.getVisibleDrawerRect(rdl, rect); + rect.offset(left, top); + if (oldTop != rect.top) { + mViewModel.getInteractiveSessionInteractor().sendTopDrawerTopOffsetChange(rect.top); + } + info.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION); + info.touchableRegion.set(new Rect(rect)); + }); + } + private void onAppTargetsLoaded(ResolverListAdapter listAdapter) { Log.d(TAG, "onAppTargetsLoaded(" + "listAdapter.userHandle=" + listAdapter.getUserHandle() + ")"); @@ -964,6 +1014,9 @@ public class ChooserActivity extends Hilt_ChooserActivity implements * @return {@code true} if a resolved target is autolaunched, otherwise {@code false} */ private boolean maybeAutolaunchActivity() { + if (isInteractiveSession()) { + return false; + } int numberOfProfiles = mChooserMultiProfilePagerAdapter.getItemCount(); // TODO(b/280988288): If the ChooserActivity is shown we should consider showing the // correct intent-picker UIs (e.g., mini-resolver) if it was launched without @@ -1562,8 +1615,12 @@ public class ChooserActivity extends Hilt_ChooserActivity implements mChooserMultiProfilePagerAdapter.getActiveListAdapter().handlePackagesChanged(); if (mSystemWindowInsets != null) { - mResolverDrawerLayout.setPadding(mSystemWindowInsets.left, mSystemWindowInsets.top, - mSystemWindowInsets.right, 0); + int topSpacing = isInteractiveSession() ? getInteractiveSessionTopSpacing() : 0; + mResolverDrawerLayout.setPadding( + mSystemWindowInsets.left, + mSystemWindowInsets.top + topSpacing, + mSystemWindowInsets.right, + 0); } if (mViewPager.isLayoutRtl()) { mChooserMultiProfilePagerAdapter.setupViewPager(mViewPager); @@ -2574,7 +2631,7 @@ public class ChooserActivity extends Hilt_ChooserActivity implements } private boolean shouldShowStickyContentPreviewNoOrientationCheck() { - if (!shouldShowContentPreview()) { + if (isInteractiveSession() || !shouldShowContentPreview()) { return false; } ResolverListAdapter adapter = mChooserMultiProfilePagerAdapter.getListAdapterForUserHandle( @@ -2667,13 +2724,26 @@ public class ChooserActivity extends Hilt_ChooserActivity implements } } + private int getInteractiveSessionTopSpacing() { + return getResources().getDimensionPixelSize(R.dimen.chooser_preview_image_height_tall); + } + + private boolean isInteractiveSession() { + return interactiveSession() && mRequest.getInteractiveSessionCallback() != null + && !isTaskRoot(); + } + protected WindowInsets onApplyWindowInsets(View v, WindowInsets insets) { mSystemWindowInsets = insets.getInsets(WindowInsets.Type.systemBars()); mChooserMultiProfilePagerAdapter .setEmptyStateBottomOffset(mSystemWindowInsets.bottom); - mResolverDrawerLayout.setPadding(mSystemWindowInsets.left, mSystemWindowInsets.top, - mSystemWindowInsets.right, 0); + final int topSpacing = isInteractiveSession() ? getInteractiveSessionTopSpacing() : 0; + mResolverDrawerLayout.setPadding( + mSystemWindowInsets.left, + mSystemWindowInsets.top + topSpacing, + mSystemWindowInsets.right, + 0); // Need extra padding so the list can fully scroll up // To accommodate for window insets diff --git a/java/src/com/android/intentresolver/ChooserHelper.kt b/java/src/com/android/intentresolver/ChooserHelper.kt index c26dd77c..2d015128 100644 --- a/java/src/com/android/intentresolver/ChooserHelper.kt +++ b/java/src/com/android/intentresolver/ChooserHelper.kt @@ -27,6 +27,7 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle +import com.android.intentresolver.Flags.interactiveSession import com.android.intentresolver.Flags.unselectFinalItem import com.android.intentresolver.annotation.JavaInterop import com.android.intentresolver.contentpreview.ContentPreviewType.CONTENT_PREVIEW_PAYLOAD_SELECTION @@ -188,6 +189,14 @@ constructor( .collect { onChooserRequestChanged.accept(it) } } } + + if (interactiveSession()) { + activity.lifecycleScope.launch { + viewModel.interactiveSessionInteractor.isSessionActive + .filter { !it } + .collect { activity.finish() } + } + } } override fun onStart(owner: LifecycleOwner) { diff --git a/java/src/com/android/intentresolver/data/model/ChooserRequest.kt b/java/src/com/android/intentresolver/data/model/ChooserRequest.kt index c4aa2b98..60cc9e05 100644 --- a/java/src/com/android/intentresolver/data/model/ChooserRequest.kt +++ b/java/src/com/android/intentresolver/data/model/ChooserRequest.kt @@ -28,6 +28,7 @@ import android.service.chooser.ChooserAction import android.service.chooser.ChooserTarget import androidx.annotation.StringRes import com.android.intentresolver.ContentTypeHint +import com.android.intentresolver.IChooserInteractiveSessionCallback import com.android.intentresolver.ext.hasAction const val ANDROID_APP_SCHEME = "android-app" @@ -182,6 +183,7 @@ data class ChooserRequest( * Specified by the [Intent.EXTRA_METADATA_TEXT] */ val metadataText: CharSequence? = null, + val interactiveSessionCallback: IChooserInteractiveSessionCallback? = null, ) { val referrerPackage = referrer?.takeIf { it.scheme == ANDROID_APP_SCHEME }?.authority diff --git a/java/src/com/android/intentresolver/data/repository/ChooserRequestRepository.kt b/java/src/com/android/intentresolver/data/repository/ChooserRequestRepository.kt index 14177b1b..8b7885c9 100644 --- a/java/src/com/android/intentresolver/data/repository/ChooserRequestRepository.kt +++ b/java/src/com/android/intentresolver/data/repository/ChooserRequestRepository.kt @@ -25,10 +25,7 @@ import kotlinx.coroutines.flow.MutableStateFlow @ViewModelScoped class ChooserRequestRepository @Inject -constructor( - initialRequest: ChooserRequest, - initialActions: List<CustomActionModel>, -) { +constructor(val initialRequest: ChooserRequest, initialActions: List<CustomActionModel>) { /** All information from the sharing application pertaining to the chooser. */ val chooserRequest: MutableStateFlow<ChooserRequest> = MutableStateFlow(initialRequest) diff --git a/java/src/com/android/intentresolver/interactive/data/repository/InteractiveSessionCallbackRepository.kt b/java/src/com/android/intentresolver/interactive/data/repository/InteractiveSessionCallbackRepository.kt new file mode 100644 index 00000000..f8894de5 --- /dev/null +++ b/java/src/com/android/intentresolver/interactive/data/repository/InteractiveSessionCallbackRepository.kt @@ -0,0 +1,54 @@ +/* + * 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 + * + * https://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.interactive.data.repository + +import android.os.Bundle +import androidx.lifecycle.SavedStateHandle +import com.android.intentresolver.IChooserController +import com.android.intentresolver.interactive.domain.model.ChooserIntentUpdater +import dagger.hilt.android.scopes.ViewModelScoped +import java.util.concurrent.atomic.AtomicReference +import javax.inject.Inject + +private const val INTERACTIVE_SESSION_CALLBACK_KEY = "interactive-session-callback" + +@ViewModelScoped +class InteractiveSessionCallbackRepository @Inject constructor(savedStateHandle: SavedStateHandle) { + private val intentUpdaterRef = + AtomicReference<ChooserIntentUpdater?>( + savedStateHandle + .get<Bundle>(INTERACTIVE_SESSION_CALLBACK_KEY) + ?.let { it.getBinder(INTERACTIVE_SESSION_CALLBACK_KEY) } + ?.let { binder -> + binder.queryLocalInterface(IChooserController.DESCRIPTOR) + as? ChooserIntentUpdater + } + ) + + val intentUpdater: ChooserIntentUpdater? + get() = intentUpdaterRef.get() + + init { + savedStateHandle.setSavedStateProvider(INTERACTIVE_SESSION_CALLBACK_KEY) { + Bundle().apply { putBinder(INTERACTIVE_SESSION_CALLBACK_KEY, intentUpdater) } + } + } + + fun setChooserIntentUpdater(intentUpdater: ChooserIntentUpdater) { + intentUpdaterRef.compareAndSet(null, intentUpdater) + } +} diff --git a/java/src/com/android/intentresolver/interactive/domain/interactor/InteractiveSessionInteractor.kt b/java/src/com/android/intentresolver/interactive/domain/interactor/InteractiveSessionInteractor.kt new file mode 100644 index 00000000..09b79985 --- /dev/null +++ b/java/src/com/android/intentresolver/interactive/domain/interactor/InteractiveSessionInteractor.kt @@ -0,0 +1,139 @@ +/* + * 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 + * + * https://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.interactive.domain.interactor + +import android.content.Intent +import android.os.Bundle +import android.os.IBinder +import com.android.intentresolver.contentpreview.payloadtoggle.data.repository.PendingSelectionCallbackRepository +import com.android.intentresolver.data.model.ChooserRequest +import com.android.intentresolver.data.repository.ActivityModelRepository +import com.android.intentresolver.data.repository.ChooserRequestRepository +import com.android.intentresolver.interactive.data.repository.InteractiveSessionCallbackRepository +import com.android.intentresolver.interactive.domain.model.ChooserIntentUpdater +import com.android.intentresolver.ui.viewmodel.readChooserRequest +import com.android.intentresolver.validation.Invalid +import com.android.intentresolver.validation.Valid +import com.android.intentresolver.validation.log +import dagger.hilt.android.scopes.ViewModelScoped +import javax.inject.Inject +import kotlinx.coroutines.awaitCancellation +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch + +private const val TAG = "ChooserSession" + +@ViewModelScoped +class InteractiveSessionInteractor +@Inject +constructor( + activityModelRepo: ActivityModelRepository, + private val chooserRequestRepository: ChooserRequestRepository, + private val pendingSelectionCallbackRepo: PendingSelectionCallbackRepository, + private val interactiveCallbackRepo: InteractiveSessionCallbackRepository, +) { + private val activityModel = activityModelRepo.value + private val sessionCallback = + chooserRequestRepository.initialRequest.interactiveSessionCallback?.let { + SafeChooserInteractiveSessionCallback(it) + } + val isSessionActive = MutableStateFlow(true) + + suspend fun activate() = coroutineScope { + if (sessionCallback == null || activityModel.isTaskRoot) { + sessionCallback?.registerChooserController(null) + return@coroutineScope + } + launch { + val callbackBinder: IBinder = sessionCallback.asBinder() + if (callbackBinder.isBinderAlive) { + val deathRecipient = IBinder.DeathRecipient { isSessionActive.value = false } + callbackBinder.linkToDeath(deathRecipient, 0) + try { + awaitCancellation() + } finally { + runCatching { sessionCallback.asBinder().unlinkToDeath(deathRecipient, 0) } + } + } else { + isSessionActive.value = false + } + } + val chooserIntentUpdater = + interactiveCallbackRepo.intentUpdater + ?: ChooserIntentUpdater().also { + interactiveCallbackRepo.setChooserIntentUpdater(it) + sessionCallback.registerChooserController(it) + } + chooserIntentUpdater.chooserIntent.collect { onIntentUpdated(it) } + } + + fun sendTopDrawerTopOffsetChange(offset: Int) { + sessionCallback?.onDrawerVerticalOffsetChanged(offset) + } + + fun endSession() { + sessionCallback?.registerChooserController(null) + } + + private fun onIntentUpdated(chooserIntent: Intent?) { + if (chooserIntent == null) { + isSessionActive.value = false + return + } + + val result = + readChooserRequest( + chooserIntent.extras ?: Bundle(), + activityModel.launchedFromPackage, + activityModel.referrer, + ) + when (result) { + is Valid<ChooserRequest> -> { + val newRequest = result.value + pendingSelectionCallbackRepo.pendingTargetIntent.compareAndSet( + null, + result.value.targetIntent, + ) + chooserRequestRepository.chooserRequest.update { + it.copy( + targetIntent = newRequest.targetIntent, + targetAction = newRequest.targetAction, + isSendActionTarget = newRequest.isSendActionTarget, + targetType = newRequest.targetType, + filteredComponentNames = newRequest.filteredComponentNames, + callerChooserTargets = newRequest.callerChooserTargets, + additionalTargets = newRequest.additionalTargets, + replacementExtras = newRequest.replacementExtras, + initialIntents = newRequest.initialIntents, + shareTargetFilter = newRequest.shareTargetFilter, + chosenComponentSender = newRequest.chosenComponentSender, + refinementIntentSender = newRequest.refinementIntentSender, + ) + } + pendingSelectionCallbackRepo.pendingTargetIntent.compareAndSet( + result.value.targetIntent, + null, + ) + } + is Invalid -> { + result.errors.forEach { it.log(TAG) } + } + } + } +} diff --git a/java/src/com/android/intentresolver/interactive/domain/interactor/SafeChooserInteractiveSessionCallback.kt b/java/src/com/android/intentresolver/interactive/domain/interactor/SafeChooserInteractiveSessionCallback.kt new file mode 100644 index 00000000..d746a3b5 --- /dev/null +++ b/java/src/com/android/intentresolver/interactive/domain/interactor/SafeChooserInteractiveSessionCallback.kt @@ -0,0 +1,43 @@ +/* + * 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 + * + * https://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.interactive.domain.interactor + +import android.util.Log +import com.android.intentresolver.IChooserController +import com.android.intentresolver.IChooserInteractiveSessionCallback + +private const val TAG = "SessionCallback" + +class SafeChooserInteractiveSessionCallback( + private val delegate: IChooserInteractiveSessionCallback +) : IChooserInteractiveSessionCallback by delegate { + + override fun registerChooserController(updater: IChooserController?) { + if (!isAlive) return + runCatching { delegate.registerChooserController(updater) } + .onFailure { Log.e(TAG, "Failed to invoke registerChooserController", it) } + } + + override fun onDrawerVerticalOffsetChanged(offset: Int) { + if (!isAlive) return + runCatching { delegate.onDrawerVerticalOffsetChanged(offset) } + .onFailure { Log.e(TAG, "Failed to invoke onDrawerVerticalOffsetChanged", it) } + } + + private val isAlive: Boolean + get() = delegate.asBinder().isBinderAlive +} diff --git a/java/src/com/android/intentresolver/interactive/domain/model/ChooserIntentUpdater.kt b/java/src/com/android/intentresolver/interactive/domain/model/ChooserIntentUpdater.kt new file mode 100644 index 00000000..5466a95d --- /dev/null +++ b/java/src/com/android/intentresolver/interactive/domain/model/ChooserIntentUpdater.kt @@ -0,0 +1,36 @@ +/* + * 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 + * + * https://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.interactive.domain.model + +import android.content.Intent +import com.android.intentresolver.IChooserController +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.filter + +private val NotSet = Intent() + +class ChooserIntentUpdater : IChooserController.Stub() { + private val updates = MutableStateFlow<Intent?>(NotSet) + + val chooserIntent: Flow<Intent?> + get() = updates.filter { it !== NotSet } + + override fun updateIntent(chooserIntent: Intent?) { + updates.value = chooserIntent + } +} diff --git a/java/src/com/android/intentresolver/shared/model/ActivityModel.kt b/java/src/com/android/intentresolver/shared/model/ActivityModel.kt index c5efdeba..1a57759d 100644 --- a/java/src/com/android/intentresolver/shared/model/ActivityModel.kt +++ b/java/src/com/android/intentresolver/shared/model/ActivityModel.kt @@ -35,6 +35,8 @@ data class ActivityModel( val launchedFromPackage: String, /** The referrer as supplied to the activity. */ val referrer: Uri?, + /** True if the activity is the first activity in the task */ + val isTaskRoot: Boolean, ) : Parcelable { constructor( source: Parcel @@ -43,6 +45,7 @@ data class ActivityModel( launchedFromUid = source.readInt(), launchedFromPackage = requireNotNull(source.readString()), referrer = source.readParcelable(), + isTaskRoot = source.readBoolean(), ) /** A package name from referrer, if it is an android-app URI */ @@ -55,6 +58,7 @@ data class ActivityModel( dest.writeInt(launchedFromUid) dest.writeString(launchedFromPackage) dest.writeParcelable(referrer, flags) + dest.writeBoolean(isTaskRoot) } companion object { @@ -74,6 +78,7 @@ data class ActivityModel( activity.launchedFromUid, Objects.requireNonNull<String>(activity.launchedFromPackage), activity.referrer, + activity.isTaskRoot, ) } } diff --git a/java/src/com/android/intentresolver/ui/viewmodel/ChooserRequestReader.kt b/java/src/com/android/intentresolver/ui/viewmodel/ChooserRequestReader.kt index 13de84b2..cb4bdcc1 100644 --- a/java/src/com/android/intentresolver/ui/viewmodel/ChooserRequestReader.kt +++ b/java/src/com/android/intentresolver/ui/viewmodel/ChooserRequestReader.kt @@ -40,9 +40,11 @@ import android.content.IntentSender import android.net.Uri import android.os.Bundle import android.service.chooser.ChooserAction +import android.service.chooser.ChooserSession import android.service.chooser.ChooserTarget import com.android.intentresolver.ChooserActivity import com.android.intentresolver.ContentTypeHint +import com.android.intentresolver.Flags.interactiveSession import com.android.intentresolver.R import com.android.intentresolver.data.model.ChooserRequest import com.android.intentresolver.ext.hasSendAction @@ -58,6 +60,8 @@ import com.android.intentresolver.validation.validateFrom private const val MAX_CHOOSER_ACTIONS = 5 private const val MAX_INITIAL_INTENTS = 2 +private const val EXTRA_CHOOSER_INTERACTIVE_CALLBACK = + "com.android.extra.EXTRA_CHOOSER_INTERACTIVE_CALLBACK" internal fun Intent.maybeAddSendActionFlags() = ifMatch(Intent::hasSendAction) { @@ -69,6 +73,14 @@ fun readChooserRequest( model: ActivityModel, savedState: Bundle = model.intent.extras ?: Bundle(), ): ValidationResult<ChooserRequest> { + return readChooserRequest(savedState, model.launchedFromPackage, model.referrer) +} + +fun readChooserRequest( + savedState: Bundle, + launchedFromPackage: String, + referrer: Uri?, +): ValidationResult<ChooserRequest> { @Suppress("DEPRECATION") return validateFrom(savedState::get) { val targetIntent = required(IntentOrUri(EXTRA_INTENT)).maybeAddSendActionFlags() @@ -139,18 +151,26 @@ fun readChooserRequest( val metadataText = optional(value<CharSequence>(EXTRA_METADATA_TEXT)) + val interactiveSessionCallback = + if (interactiveSession()) { + optional(value<ChooserSession>(EXTRA_CHOOSER_INTERACTIVE_CALLBACK)) + ?.sessionCallbackBinder + } else { + null + } + ChooserRequest( targetIntent = targetIntent, targetAction = targetIntent.action, isSendActionTarget = isSendAction, targetType = targetIntent.type, launchedFromPackage = - requireNotNull(model.launchedFromPackage) { + requireNotNull(launchedFromPackage) { "launch.fromPackage was null, See Activity.getLaunchedFromPackage()" }, title = customTitle, defaultTitleResource = defaultTitleResource, - referrer = model.referrer, + referrer = referrer, filteredComponentNames = filteredComponents, callerChooserTargets = callerChooserTargets, chooserActions = chooserActions, @@ -168,6 +188,7 @@ fun readChooserRequest( focusedItemPosition = focusedItemPos, contentTypeHint = contentTypeHint, metadataText = metadataText, + interactiveSessionCallback = interactiveSessionCallback, ) } } diff --git a/java/src/com/android/intentresolver/ui/viewmodel/ChooserViewModel.kt b/java/src/com/android/intentresolver/ui/viewmodel/ChooserViewModel.kt index 8597d802..7bc811c0 100644 --- a/java/src/com/android/intentresolver/ui/viewmodel/ChooserViewModel.kt +++ b/java/src/com/android/intentresolver/ui/viewmodel/ChooserViewModel.kt @@ -21,6 +21,7 @@ import android.util.Log import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.android.intentresolver.Flags.interactiveSession import com.android.intentresolver.Flags.saveShareouselState import com.android.intentresolver.contentpreview.ImageLoader import com.android.intentresolver.contentpreview.PreviewDataProvider @@ -32,6 +33,7 @@ import com.android.intentresolver.data.repository.ActivityModelRepository import com.android.intentresolver.data.repository.ChooserRequestRepository import com.android.intentresolver.domain.saveUpdates import com.android.intentresolver.inject.Background +import com.android.intentresolver.interactive.domain.interactor.InteractiveSessionInteractor import com.android.intentresolver.shared.model.ActivityModel import com.android.intentresolver.validation.Invalid import com.android.intentresolver.validation.Valid @@ -67,6 +69,7 @@ constructor( private val chooserRequestRepository: Lazy<ChooserRequestRepository>, private val contentResolver: ContentInterface, val imageLoader: ImageLoader, + private val interactiveSessionInteractorLazy: Lazy<InteractiveSessionInteractor>, ) : ViewModel() { /** Parcelable-only references provided from the creating Activity */ @@ -98,6 +101,9 @@ constructor( ) } + val interactiveSessionInteractor: InteractiveSessionInteractor + get() = interactiveSessionInteractorLazy.get() + init { when (initialRequest) { is Invalid -> { @@ -116,6 +122,9 @@ constructor( } } } + if (interactiveSession()) { + viewModelScope.launch(bgDispatcher) { interactiveSessionInteractor.activate() } + } } } } diff --git a/java/src/com/android/intentresolver/widget/ResolverDrawerLayout.java b/java/src/com/android/intentresolver/widget/ResolverDrawerLayout.java index 07693b25..4895a2cd 100644 --- a/java/src/com/android/intentresolver/widget/ResolverDrawerLayout.java +++ b/java/src/com/android/intentresolver/widget/ResolverDrawerLayout.java @@ -278,6 +278,10 @@ public class ResolverDrawerLayout extends ViewGroup { mDismissLocked = locked; } + int getTopOffset() { + return mTopOffset; + } + private boolean isMoving() { return mIsDragging || !mScroller.isFinished(); } diff --git a/java/src/com/android/intentresolver/widget/ResolverDrawerLayoutExt.kt b/java/src/com/android/intentresolver/widget/ResolverDrawerLayoutExt.kt new file mode 100644 index 00000000..0c537a12 --- /dev/null +++ b/java/src/com/android/intentresolver/widget/ResolverDrawerLayoutExt.kt @@ -0,0 +1,51 @@ +/* + * 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 + * + * https://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. + */ + +@file:JvmName("ResolverDrawerLayoutExt") + +package com.android.intentresolver.widget + +import android.graphics.Rect +import android.view.View +import android.view.ViewGroup.MarginLayoutParams + +fun ResolverDrawerLayout.getVisibleDrawerRect(outRect: Rect) { + if (!isLaidOut) { + outRect.set(0, 0, 0, 0) + return + } + val firstChild = firstNonGoneChild() + val lp = firstChild?.layoutParams as? MarginLayoutParams + val margin = lp?.topMargin ?: 0 + val top = maxOf(paddingTop, topOffset + margin) + val leftEdge = paddingLeft + val rightEdge = width - paddingRight + val widthAvailable = rightEdge - leftEdge + val childWidth = firstChild?.width ?: 0 + val left = leftEdge + (widthAvailable - childWidth) / 2 + val right = left + childWidth + outRect.set(left, top, right, height - paddingBottom) +} + +fun ResolverDrawerLayout.firstNonGoneChild(): View? { + for (i in 0 until childCount) { + val view = getChildAt(i) + if (view.visibility != View.GONE) { + return view + } + } + return null +} |