summaryrefslogtreecommitdiff
path: root/java/src/com
diff options
context:
space:
mode:
author Andrey Yepin <ayepin@google.com> 2024-10-01 10:14:31 -0700
committer Andrey Yepin <ayepin@google.com> 2024-12-13 15:45:11 -0800
commit319d59d29b4037246a72cc97e1353cc6fbda20d6 (patch)
treeee81575c468d1408d89b64aec354509d6b9d0fea /java/src/com
parenteffd3e6c1bba8d5d5ce8047b9a1e455816fcd4b3 (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')
-rw-r--r--java/src/com/android/intentresolver/ChooserActivity.java80
-rw-r--r--java/src/com/android/intentresolver/ChooserHelper.kt9
-rw-r--r--java/src/com/android/intentresolver/data/model/ChooserRequest.kt2
-rw-r--r--java/src/com/android/intentresolver/data/repository/ChooserRequestRepository.kt5
-rw-r--r--java/src/com/android/intentresolver/interactive/data/repository/InteractiveSessionCallbackRepository.kt54
-rw-r--r--java/src/com/android/intentresolver/interactive/domain/interactor/InteractiveSessionInteractor.kt139
-rw-r--r--java/src/com/android/intentresolver/interactive/domain/interactor/SafeChooserInteractiveSessionCallback.kt43
-rw-r--r--java/src/com/android/intentresolver/interactive/domain/model/ChooserIntentUpdater.kt36
-rw-r--r--java/src/com/android/intentresolver/shared/model/ActivityModel.kt5
-rw-r--r--java/src/com/android/intentresolver/ui/viewmodel/ChooserRequestReader.kt25
-rw-r--r--java/src/com/android/intentresolver/ui/viewmodel/ChooserViewModel.kt9
-rw-r--r--java/src/com/android/intentresolver/widget/ResolverDrawerLayout.java4
-rw-r--r--java/src/com/android/intentresolver/widget/ResolverDrawerLayoutExt.kt51
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
+}