diff options
| author | 2024-03-12 22:26:04 +0000 | |
|---|---|---|
| committer | 2024-03-12 22:26:04 +0000 | |
| commit | 3f1677e45effe9a4b0a3d7c6b83cd6f6cfc00fcf (patch) | |
| tree | 2d1dee38b8448d603be686c8e46cf9f034f042b7 /java/src | |
| parent | a225b55f2c75447a3a8c59eb8c39651ebe8e69d5 (diff) | |
| parent | 118a77b69fe8ffa8db0dbda7a636b7d7046f4f54 (diff) | |
Merge "Update app targets on payload selection change" into main
Diffstat (limited to 'java/src')
11 files changed, 229 insertions, 57 deletions
diff --git a/java/src/com/android/intentresolver/contentpreview/ShareouselContentPreviewUi.kt b/java/src/com/android/intentresolver/contentpreview/ShareouselContentPreviewUi.kt index 80f7c25a..463da5fa 100644 --- a/java/src/com/android/intentresolver/contentpreview/ShareouselContentPreviewUi.kt +++ b/java/src/com/android/intentresolver/contentpreview/ShareouselContentPreviewUi.kt @@ -31,9 +31,9 @@ import androidx.compose.ui.platform.LocalContext import androidx.lifecycle.viewmodel.compose.viewModel import com.android.intentresolver.R import com.android.intentresolver.contentpreview.ChooserContentPreviewUi.ActionFactory -import com.android.intentresolver.contentpreview.payloadtoggle.app.viewmodel.ShareouselContentPreviewViewModel import com.android.intentresolver.contentpreview.payloadtoggle.ui.composable.Shareousel import com.android.intentresolver.contentpreview.payloadtoggle.ui.viewmodel.ShareouselViewModel +import com.android.intentresolver.v2.ui.viewmodel.ChooserViewModel @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE) class ShareouselContentPreviewUi( @@ -58,8 +58,8 @@ class ShareouselContentPreviewUi( } return ComposeView(parent.context).apply { setContent { - val vm: ShareouselContentPreviewViewModel = viewModel() - val viewModel: ShareouselViewModel = vm.viewModel + val vm: ChooserViewModel = viewModel() + val viewModel: ShareouselViewModel = vm.shareouselViewModel headlineViewParent?.let { LaunchedEffect(viewModel) { bindHeadline(viewModel, headlineViewParent) } diff --git a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/app/viewmodel/ShareouselContentPreviewViewModel.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/app/viewmodel/ShareouselContentPreviewViewModel.kt deleted file mode 100644 index 479f0ec8..00000000 --- a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/app/viewmodel/ShareouselContentPreviewViewModel.kt +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright (C) 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.payloadtoggle.app.viewmodel - -import androidx.lifecycle.ViewModel -import com.android.intentresolver.contentpreview.payloadtoggle.domain.interactor.FetchPreviewsInteractor -import com.android.intentresolver.contentpreview.payloadtoggle.domain.interactor.UpdateTargetIntentInteractor -import com.android.intentresolver.contentpreview.payloadtoggle.ui.viewmodel.ShareouselViewModel -import com.android.intentresolver.inject.Background -import com.android.intentresolver.inject.ViewModelOwned -import dagger.hilt.android.lifecycle.HiltViewModel -import javax.inject.Inject -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch - -/** View-model for [com.android.intentresolver.contentpreview.ShareouselContentPreviewUi]. */ -@HiltViewModel -class ShareouselContentPreviewViewModel -@Inject -constructor( - val viewModel: ShareouselViewModel, - updateTargetIntentInteractor: UpdateTargetIntentInteractor, - fetchPreviewsInteractor: FetchPreviewsInteractor, - @Background private val bgDispatcher: CoroutineDispatcher, - @ViewModelOwned private val scope: CoroutineScope, -) : ViewModel() { - init { - scope.launch(bgDispatcher) { updateTargetIntentInteractor.launch() } - scope.launch(bgDispatcher) { fetchPreviewsInteractor.launch() } - } -} diff --git a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/data/repository/PreviewSelectionsRepository.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/data/repository/PreviewSelectionsRepository.kt index 8035580d..2d849d14 100644 --- a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/data/repository/PreviewSelectionsRepository.kt +++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/data/repository/PreviewSelectionsRepository.kt @@ -16,14 +16,46 @@ package com.android.intentresolver.contentpreview.payloadtoggle.data.repository +import android.util.Log import com.android.intentresolver.contentpreview.payloadtoggle.shared.model.PreviewModel import dagger.hilt.android.scopes.ViewModelScoped import javax.inject.Inject +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.update + +private const val TAG = "PreviewSelectionsRep" /** Stores set of selected previews. */ @ViewModelScoped class PreviewSelectionsRepository @Inject constructor() { /** Set of selected previews. */ - val selections = MutableStateFlow<Set<PreviewModel>>(emptySet()) + private val _selections = MutableStateFlow<Set<PreviewModel>?>(null) + + val selections: Flow<Set<PreviewModel>> = _selections.filterNotNull() + + fun setSelection(selection: Set<PreviewModel>) { + _selections.value = selection + } + + fun select(item: PreviewModel) { + _selections.update { selection -> + selection?.let { it + item } + ?: run { + Log.w(TAG, "Changing selection before it is initialized") + null + } + } + } + + fun unselect(item: PreviewModel) { + _selections.update { selection -> + selection?.let { it - item } + ?: run { + Log.w(TAG, "Changing selection before it is initialized") + null + } + } + } } diff --git a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/FetchPreviewsInteractor.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/FetchPreviewsInteractor.kt index 032692cd..a7749c92 100644 --- a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/FetchPreviewsInteractor.kt +++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/FetchPreviewsInteractor.kt @@ -44,7 +44,7 @@ constructor( suspend fun launch() = coroutineScope { val cursor = async { cursorResolver.getCursor() } val initialPreviewMap: Set<PreviewModel> = getInitialPreviews() - selectionRepository.selections.value = initialPreviewMap + selectionRepository.setSelection(initialPreviewMap) setCursorPreviews.setPreviews( previewsByKey = initialPreviewMap, startIndex = focusedItemIdx, diff --git a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/SelectablePreviewInteractor.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/SelectablePreviewInteractor.kt index d94b1078..0b1038f5 100644 --- a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/SelectablePreviewInteractor.kt +++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/SelectablePreviewInteractor.kt @@ -21,7 +21,6 @@ import com.android.intentresolver.contentpreview.payloadtoggle.data.repository.P import com.android.intentresolver.contentpreview.payloadtoggle.shared.model.PreviewModel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.update /** An individual preview in Shareousel. */ class SelectablePreviewInteractor( @@ -37,9 +36,9 @@ class SelectablePreviewInteractor( /** Sets whether this preview is selected by the user. */ fun setSelected(isSelected: Boolean) { if (isSelected) { - selectionRepo.selections.update { it + key } + selectionRepo.select(key) } else { - selectionRepo.selections.update { it - key } + selectionRepo.unselect(key) } } } diff --git a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/UpdateTargetIntentInteractor.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/UpdateTargetIntentInteractor.kt index e7bdafbc..4619e478 100644 --- a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/UpdateTargetIntentInteractor.kt +++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/UpdateTargetIntentInteractor.kt @@ -30,7 +30,6 @@ import javax.inject.Inject import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.launch diff --git a/java/src/com/android/intentresolver/v2/ChooserActivity.java b/java/src/com/android/intentresolver/v2/ChooserActivity.java index 510e6d14..bda393b8 100644 --- a/java/src/com/android/intentresolver/v2/ChooserActivity.java +++ b/java/src/com/android/intentresolver/v2/ChooserActivity.java @@ -352,6 +352,9 @@ public class ChooserActivity extends Hilt_ChooserActivity implements // Initializer is invoked when this function returns, via Lifecycle. mChooserHelper.setInitializer(this::initializeWith); + if (mChooserServiceFeatureFlags.chooserPayloadToggling()) { + mChooserHelper.setOnChooserRequestChanged(this::onChooserRequestChanged); + } } @Override @@ -513,7 +516,7 @@ public class ChooserActivity extends Hilt_ChooserActivity implements mChooserMultiProfilePagerAdapter = createMultiProfilePagerAdapter( /* context = */ this, mProfilePagerResources, - mViewModel.getRequest().getValue(), + mRequest, mProfiles, mProfileAvailability, mRequest.getInitialIntents(), @@ -657,6 +660,71 @@ public class ChooserActivity extends Hilt_ChooserActivity implements Tracer.INSTANCE.markLaunched(); } + private void onChooserRequestChanged(ChooserRequest chooserRequest) { + if (mRequest == chooserRequest) { + return; + } + mRequest = chooserRequest; + recreatePagerAdapter(); + } + + private void recreatePagerAdapter() { + if (!mChooserServiceFeatureFlags.chooserPayloadToggling()) { + return; + } + destroyProfileRecords(); + createProfileRecords( + new AppPredictorFactory( + this, + Objects.toString(mRequest.getSharedText(), null), + mRequest.getShareTargetFilter(), + mAppPredictionAvailable + ), + mRequest.getShareTargetFilter() + ); + + if (mChooserMultiProfilePagerAdapter != null) { + mChooserMultiProfilePagerAdapter.destroy(); + } + mChooserMultiProfilePagerAdapter = createMultiProfilePagerAdapter( + /* context = */ this, + mProfilePagerResources, + mRequest, + mProfiles, + mProfileAvailability, + mRequest.getInitialIntents(), + mMaxTargetsPerRow, + mFeatureFlags); + mChooserMultiProfilePagerAdapter.setupViewPager( + requireViewById(com.android.internal.R.id.profile_pager)); + if (mPersonalPackageMonitor != null) { + mPersonalPackageMonitor.unregister(); + } + mPersonalPackageMonitor = createPackageMonitor( + mChooserMultiProfilePagerAdapter.getPersonalListAdapter()); + mPersonalPackageMonitor.register( + this, + getMainLooper(), + mProfiles.getPersonalHandle(), + false); + if (mProfiles.getWorkProfilePresent()) { + if (mWorkPackageMonitor != null) { + mWorkPackageMonitor.unregister(); + } + mWorkPackageMonitor = createPackageMonitor( + mChooserMultiProfilePagerAdapter.getWorkListAdapter()); + mWorkPackageMonitor.register( + this, + getMainLooper(), + mProfiles.getWorkHandle(), + false); + } + postRebuildList( + mChooserMultiProfilePagerAdapter.rebuildTabs( + mProfiles.getWorkProfilePresent() + || mProfiles.getPrivateProfilePresent())); + } + @Override protected void onRestoreInstanceState(@NonNull Bundle savedInstanceState) { ViewPager viewPager = findViewById(com.android.internal.R.id.profile_pager); diff --git a/java/src/com/android/intentresolver/v2/ChooserHelper.kt b/java/src/com/android/intentresolver/v2/ChooserHelper.kt index d34e0b36..f2a2726a 100644 --- a/java/src/com/android/intentresolver/v2/ChooserHelper.kt +++ b/java/src/com/android/intentresolver/v2/ChooserHelper.kt @@ -25,6 +25,7 @@ import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle import com.android.intentresolver.contentpreview.payloadtoggle.data.repository.ActivityResultRepository import com.android.intentresolver.inject.Background import com.android.intentresolver.v2.annotation.JavaInterop @@ -36,6 +37,7 @@ import com.android.intentresolver.v2.validation.Invalid import com.android.intentresolver.v2.validation.Valid import com.android.intentresolver.v2.validation.log import dagger.hilt.android.scopes.ActivityScoped +import java.util.function.Consumer import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.filterNotNull @@ -113,6 +115,8 @@ constructor( private lateinit var activityInitializer: ChooserInitializer + var onChooserRequestChanged: Consumer<ChooserRequest> = Consumer {} + init { activity.lifecycle.addObserver(this) } @@ -150,6 +154,12 @@ constructor( activity.setResult(activityResultRepo.activityResult.filterNotNull().first()) activity.finish() } + + activity.lifecycleScope.launch { + activity.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.request.collect { onChooserRequestChanged.accept(it) } + } + } } override fun onStart(owner: LifecycleOwner) { diff --git a/java/src/com/android/intentresolver/v2/domain/interactor/ChooserRequestUpdateInteractor.kt b/java/src/com/android/intentresolver/v2/domain/interactor/ChooserRequestUpdateInteractor.kt new file mode 100644 index 00000000..99da5c81 --- /dev/null +++ b/java/src/com/android/intentresolver/v2/domain/interactor/ChooserRequestUpdateInteractor.kt @@ -0,0 +1,82 @@ +/* + * 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.v2.domain.interactor + +import android.content.Intent +import android.util.Log +import com.android.intentresolver.contentpreview.payloadtoggle.data.repository.TargetIntentRepository +import com.android.intentresolver.inject.ChooserServiceFlags +import com.android.intentresolver.inject.TargetIntent +import com.android.intentresolver.v2.ui.model.ActivityModel +import com.android.intentresolver.v2.ui.model.ChooserRequest +import com.android.intentresolver.v2.ui.viewmodel.readChooserRequest +import com.android.intentresolver.v2.validation.Invalid +import com.android.intentresolver.v2.validation.Valid +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import dagger.hilt.android.scopes.ViewModelScoped +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.filter + +private const val TAG = "ChooserRequestUpdate" + +/** Updates updates ChooserRequest with a new target intent */ +// TODO: make fully injectable +class ChooserRequestUpdateInteractor +@AssistedInject +constructor( + private val activityModel: ActivityModel, + @TargetIntent private val initialIntent: Intent, + private val targetIntentRepository: TargetIntentRepository, + // TODO: replace with a proper repository, when available + @Assisted private val chooserRequestRepository: MutableStateFlow<ChooserRequest>, + private val flags: ChooserServiceFlags, +) { + + suspend fun launch() { + targetIntentRepository.targetIntent + // TODO: maybe find a better way to exclude the initial intent (as here it's compared by + // reference) + .filter { it !== initialIntent } + .collect(::updateTargetIntent) + } + + private fun updateTargetIntent(targetIntent: Intent) { + val updatedActivityModel = activityModel.updateWithTargetIntent(targetIntent) + when (val updatedChooserRequest = readChooserRequest(updatedActivityModel, flags)) { + is Valid -> chooserRequestRepository.value = updatedChooserRequest.value + is Invalid -> Log.w(TAG, "Failed to apply payload selection changes") + } + } + + private fun ActivityModel.updateWithTargetIntent(targetIntent: Intent) = + ActivityModel( + Intent(intent).apply { putExtra(Intent.EXTRA_INTENT, targetIntent) }, + launchedFromUid, + launchedFromPackage, + referrer, + ) +} + +@AssistedFactory +@ViewModelScoped +interface ChooserRequestUpdateInteractorFactory { + fun create( + chooserRequestRepository: MutableStateFlow<ChooserRequest> + ): ChooserRequestUpdateInteractor +} diff --git a/java/src/com/android/intentresolver/v2/profiles/MultiProfilePagerAdapter.java b/java/src/com/android/intentresolver/v2/profiles/MultiProfilePagerAdapter.java index 5d7cf26e..341e7043 100644 --- a/java/src/com/android/intentresolver/v2/profiles/MultiProfilePagerAdapter.java +++ b/java/src/com/android/intentresolver/v2/profiles/MultiProfilePagerAdapter.java @@ -245,6 +245,7 @@ public class MultiProfilePagerAdapter< Runnable onTabChangeListener, OnProfileSelectedListener clientOnProfileSelectedListener) { tabHost.setup(); + tabHost.getTabWidget().removeAllViews(); viewPager.setSaveEnabled(false); for (int pageNumber = 0; pageNumber < getItemCount(); ++pageNumber) { diff --git a/java/src/com/android/intentresolver/v2/ui/viewmodel/ChooserViewModel.kt b/java/src/com/android/intentresolver/v2/ui/viewmodel/ChooserViewModel.kt index 4d87b2cb..4431a545 100644 --- a/java/src/com/android/intentresolver/v2/ui/viewmodel/ChooserViewModel.kt +++ b/java/src/com/android/intentresolver/v2/ui/viewmodel/ChooserViewModel.kt @@ -18,17 +18,26 @@ package com.android.intentresolver.v2.ui.viewmodel import android.util.Log import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.android.intentresolver.contentpreview.payloadtoggle.domain.interactor.FetchPreviewsInteractor +import com.android.intentresolver.contentpreview.payloadtoggle.domain.interactor.UpdateTargetIntentInteractor +import com.android.intentresolver.contentpreview.payloadtoggle.ui.viewmodel.ShareouselViewModel +import com.android.intentresolver.inject.Background import com.android.intentresolver.inject.ChooserServiceFlags +import com.android.intentresolver.v2.domain.interactor.ChooserRequestUpdateInteractorFactory import com.android.intentresolver.v2.ui.model.ActivityModel import com.android.intentresolver.v2.ui.model.ActivityModel.Companion.ACTIVITY_MODEL_KEY import com.android.intentresolver.v2.ui.model.ChooserRequest import com.android.intentresolver.v2.validation.Invalid import com.android.intentresolver.v2.validation.Valid +import dagger.Lazy import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch private const val TAG = "ChooserViewModel" @@ -37,7 +46,12 @@ class ChooserViewModel @Inject constructor( args: SavedStateHandle, - flags: ChooserServiceFlags, + private val shareouselViewModelProvider: Lazy<ShareouselViewModel>, + private val updateTargetIntentInteractor: Lazy<UpdateTargetIntentInteractor>, + private val fetchPreviewsInteractor: Lazy<FetchPreviewsInteractor>, + @Background private val bgDispatcher: CoroutineDispatcher, + private val chooserRequestUpdateInteractorFactory: ChooserRequestUpdateInteractorFactory, + private val flags: ChooserServiceFlags, ) : ViewModel() { /** Parcelable-only references provided from the creating Activity */ @@ -46,6 +60,19 @@ constructor( "ActivityModel missing in SavedStateHandle! ($ACTIVITY_MODEL_KEY)" } + val shareouselViewModel by lazy { + // TODO: consolidate this logic, this would require a consolidated preview view model but + // for now just postpone starting the payload selection preview machinery until it's needed + assert(flags.chooserPayloadToggling()) { + "An attempt to use payload selection preview with the disabled flag" + } + + viewModelScope.launch(bgDispatcher) { updateTargetIntentInteractor.get().launch() } + viewModelScope.launch(bgDispatcher) { fetchPreviewsInteractor.get().launch() } + viewModelScope.launch { chooserRequestUpdateInteractorFactory.create(_request).launch() } + shareouselViewModelProvider.get() + } + /** * Provided only for the express purpose of early exit in the event of an invalid request. * |