diff options
34 files changed, 402 insertions, 306 deletions
diff --git a/AndroidManifest-lib.xml b/AndroidManifest-lib.xml index b3a43eb3..bdb94232 100644 --- a/AndroidManifest-lib.xml +++ b/AndroidManifest-lib.xml @@ -32,4 +32,6 @@ <uses-permission android:name="android.permission.QUERY_CLONED_APPS" /> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> <uses-permission android:name="android.permission.REPORT_USAGE_STATS" /> + <uses-permission android:name="android.permission.LOG_COMPAT_CHANGE" /> + <uses-permission android:name="android.permission.READ_COMPAT_CHANGE_CONFIG" /> </manifest> diff --git a/java/src/com/android/intentresolver/ChooserActivity.java b/java/src/com/android/intentresolver/ChooserActivity.java index 9b4582df..039fad56 100644 --- a/java/src/com/android/intentresolver/ChooserActivity.java +++ b/java/src/com/android/intentresolver/ChooserActivity.java @@ -318,7 +318,8 @@ public class ChooserActivity extends Hilt_ChooserActivity implements mEnterTransitionAnimationDelegate, new HeadlineGeneratorImpl(this), ContentTypeHint.NONE, - mChooserRequest.getMetadataText() + mChooserRequest.getMetadataText(), + /*isPayloadTogglingEnabled =*/ false ); updateStickyContentPreview(); diff --git a/java/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUi.java index acdf6ec6..6f201ad5 100644 --- a/java/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUi.java +++ b/java/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUi.java @@ -25,7 +25,6 @@ import android.content.ClipData; import android.content.Intent; import android.content.res.Resources; import android.net.Uri; -import android.service.chooser.Flags; import android.text.TextUtils; import android.view.LayoutInflater; import android.view.View; @@ -51,6 +50,7 @@ import kotlinx.coroutines.CoroutineScope; public final class ChooserContentPreviewUi { private final CoroutineScope mScope; + private final boolean mIsPayloadTogglingEnabled; /** * Delegate to build the default system action buttons to display in the preview layout, if/when @@ -103,8 +103,11 @@ public final class ChooserContentPreviewUi { TransitionElementStatusCallback transitionElementStatusCallback, HeadlineGenerator headlineGenerator, ContentTypeHint contentTypeHint, - @Nullable CharSequence metadata) { + @Nullable CharSequence metadata, + // TODO: replace with the FeatureFlag ref when v1 is gone + boolean isPayloadTogglingEnabled) { mScope = scope; + mIsPayloadTogglingEnabled = isPayloadTogglingEnabled; mContentPreviewUi = createContentPreview( previewData, targetIntent, @@ -157,8 +160,7 @@ public final class ChooserContentPreviewUi { return fileContentPreviewUi; } - //TODO: use flags injection - if (previewType == CONTENT_PREVIEW_PAYLOAD_SELECTION && Flags.chooserPayloadToggling()) { + if (previewType == CONTENT_PREVIEW_PAYLOAD_SELECTION && mIsPayloadTogglingEnabled) { transitionElementStatusCallback.onAllTransitionElementsReady(); // TODO return new ShareouselContentPreviewUi(actionFactory); } diff --git a/java/src/com/android/intentresolver/contentpreview/ContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/ContentPreviewUi.java index c35f93b4..b0fb278e 100644 --- a/java/src/com/android/intentresolver/contentpreview/ContentPreviewUi.java +++ b/java/src/com/android/intentresolver/contentpreview/ContentPreviewUi.java @@ -30,12 +30,14 @@ import android.widget.ImageView; import android.widget.TextView; import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; import com.android.intentresolver.R; import com.android.intentresolver.widget.ActionRow; import com.android.intentresolver.widget.ScrollableImagePreviewView; -abstract class ContentPreviewUi { +@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE) +public abstract class ContentPreviewUi { private static final int IMAGE_FADE_IN_MILLIS = 150; static final String TAG = "ChooserPreview"; diff --git a/java/src/com/android/intentresolver/contentpreview/CursorUriReader.kt b/java/src/com/android/intentresolver/contentpreview/CursorUriReader.kt index e9e60040..6a12f56c 100644 --- a/java/src/com/android/intentresolver/contentpreview/CursorUriReader.kt +++ b/java/src/com/android/intentresolver/contentpreview/CursorUriReader.kt @@ -140,7 +140,6 @@ class CursorUriReader( cursor.extras?.getInt(CursorExtraKeys.POSITION, 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 8073cfec..3f306a80 100644 --- a/java/src/com/android/intentresolver/contentpreview/PreviewDataProvider.kt +++ b/java/src/com/android/intentresolver/contentpreview/PreviewDataProvider.kt @@ -155,7 +155,6 @@ constructor( val extraContentUri = additionalContentUri ?: return false return runCatching { val authority = extraContentUri.authority - // TODO: verify that authority is case-sensitive records.firstOrNull { authority == it.uri.authority } == null } .onFailure { diff --git a/java/src/com/android/intentresolver/contentpreview/ShareouselContentPreviewUi.kt b/java/src/com/android/intentresolver/contentpreview/ShareouselContentPreviewUi.kt index cc89f5bf..82c09986 100644 --- a/java/src/com/android/intentresolver/contentpreview/ShareouselContentPreviewUi.kt +++ b/java/src/com/android/intentresolver/contentpreview/ShareouselContentPreviewUi.kt @@ -20,6 +20,7 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.TextView +import androidx.annotation.VisibleForTesting import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height @@ -43,7 +44,8 @@ import com.android.intentresolver.contentpreview.shareousel.ui.composable.Shareo import com.android.intentresolver.contentpreview.shareousel.ui.viewmodel.ShareouselViewModel import com.android.intentresolver.contentpreview.shareousel.ui.viewmodel.toShareouselViewModel -internal class ShareouselContentPreviewUi( +@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE) +class ShareouselContentPreviewUi( private val actionFactory: ActionFactory, ) : ContentPreviewUi() { diff --git a/java/src/com/android/intentresolver/v2/ChooserActionFactory.java b/java/src/com/android/intentresolver/v2/ChooserActionFactory.java index f9de9f4b..9077a18d 100644 --- a/java/src/com/android/intentresolver/v2/ChooserActionFactory.java +++ b/java/src/com/android/intentresolver/v2/ChooserActionFactory.java @@ -131,11 +131,12 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio Callable</* @Nullable */ View> firstVisibleImageQuery, ActionActivityStarter activityStarter, @Nullable ShareResultSender shareResultSender, - Consumer</* @Nullable */ Integer> finishCallback) { + Consumer</* @Nullable */ Integer> finishCallback, + ClipboardManager clipboardManager) { this( context, makeCopyButtonRunnable( - context, + clipboardManager, targetIntent, referrerPackageName, finishCallback, @@ -181,13 +182,12 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio if (mShareResultSender != null) { mEditButtonRunnable = () -> { mShareResultSender.onActionSelected(ShareAction.SYSTEM_EDIT); - mEditButtonRunnable.run(); + editButtonRunnable.run(); }; if (mCopyButtonRunnable != null) { mCopyButtonRunnable = () -> { mShareResultSender.onActionSelected(ShareAction.SYSTEM_COPY); - //noinspection DataFlowIssue - mCopyButtonRunnable.run(); + copyButtonRunnable.run(); }; } } @@ -245,7 +245,7 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio @Nullable private static Runnable makeCopyButtonRunnable( - Context context, + ClipboardManager clipboardManager, Intent targetIntent, String referrerPackageName, Consumer<Integer> finishCallback, @@ -261,8 +261,6 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio return null; } return () -> { - ClipboardManager clipboardManager = (ClipboardManager) context.getSystemService( - Context.CLIPBOARD_SERVICE); clipboardManager.setPrimaryClipAsPackage(clipData, referrerPackageName); log.logActionSelected(EventLog.SELECTION_TYPE_COPY); diff --git a/java/src/com/android/intentresolver/v2/ChooserActivity.java b/java/src/com/android/intentresolver/v2/ChooserActivity.java index cdc05b95..68815067 100644 --- a/java/src/com/android/intentresolver/v2/ChooserActivity.java +++ b/java/src/com/android/intentresolver/v2/ChooserActivity.java @@ -47,6 +47,7 @@ import android.app.prediction.AppPredictor; import android.app.prediction.AppTarget; import android.app.prediction.AppTargetEvent; import android.app.prediction.AppTargetId; +import android.content.ClipboardManager; import android.content.ComponentName; import android.content.ContentResolver; import android.content.Context; @@ -138,8 +139,6 @@ import com.android.intentresolver.model.AppPredictionServiceResolverComparator; import com.android.intentresolver.model.ResolverRankerServiceResolverComparator; import com.android.intentresolver.shortcuts.AppPredictorFactory; import com.android.intentresolver.shortcuts.ShortcutLoader; -import com.android.intentresolver.v2.MultiProfilePagerAdapter.ProfileType; -import com.android.intentresolver.v2.MultiProfilePagerAdapter.TabConfig; import com.android.intentresolver.v2.data.repository.DevicePolicyResources; import com.android.intentresolver.v2.emptystate.NoAppsAvailableEmptyStateProvider; import com.android.intentresolver.v2.emptystate.NoCrossProfileEmptyStateProvider; @@ -148,6 +147,12 @@ import com.android.intentresolver.v2.emptystate.WorkProfilePausedEmptyStateProvi import com.android.intentresolver.v2.platform.AppPredictionAvailable; import com.android.intentresolver.v2.platform.ImageEditor; import com.android.intentresolver.v2.platform.NearbyShare; +import com.android.intentresolver.v2.profiles.ChooserMultiProfilePagerAdapter; +import com.android.intentresolver.v2.profiles.MultiProfilePagerAdapter; +import com.android.intentresolver.v2.profiles.MultiProfilePagerAdapter.ProfileType; +import com.android.intentresolver.v2.profiles.OnProfileSelectedListener; +import com.android.intentresolver.v2.profiles.OnSwitchOnWorkSelectedListener; +import com.android.intentresolver.v2.profiles.TabConfig; import com.android.intentresolver.v2.ui.ActionTitle; import com.android.intentresolver.v2.ui.ShareResultSender; import com.android.intentresolver.v2.ui.ShareResultSenderFactory; @@ -245,7 +250,7 @@ public class ChooserActivity extends Hilt_ChooserActivity implements private ResolverActivity.PickTargetOptionRequest mPickOptionRequest; @Nullable - private MultiProfilePagerAdapter.OnSwitchOnWorkSelectedListener mOnSwitchOnWorkSelectedListener; + private OnSwitchOnWorkSelectedListener mOnSwitchOnWorkSelectedListener; ////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////// @@ -278,6 +283,7 @@ public class ChooserActivity extends Hilt_ChooserActivity implements @Inject public TargetDataLoader mTargetDataLoader; @Inject public DevicePolicyResources mDevicePolicyResources; @Inject public PackageManager mPackageManager; + @Inject public ClipboardManager mClipboardManager; @Inject public IntentForwarding mIntentForwarding; @Inject public ShareResultSenderFactory mShareResultSenderFactory; @Nullable @@ -485,7 +491,7 @@ public class ChooserActivity extends Hilt_ChooserActivity implements .get(BasePreviewViewModel.class); previewViewModel.init( chooserRequest.getTargetIntent(), - getIntent(), + mActivityLaunch.getIntent(), chooserRequest.getAdditionalContentUri(), chooserRequest.getFocusedItemPosition(), mChooserServiceFeatureFlags.chooserPayloadToggling()); @@ -493,7 +499,7 @@ public class ChooserActivity extends Hilt_ChooserActivity implements ChooserContentPreviewUi.ActionFactory actionFactory = chooserActionFactory; if (previewViewModel.getPreviewDataProvider().getPreviewType() == CONTENT_PREVIEW_PAYLOAD_SELECTION - && android.service.chooser.Flags.chooserPayloadToggling()) { + && mChooserServiceFeatureFlags.chooserPayloadToggling()) { PayloadToggleInteractor payloadToggleInteractor = previewViewModel.getPayloadToggleInteractor(); if (payloadToggleInteractor != null) { @@ -515,8 +521,8 @@ public class ChooserActivity extends Hilt_ChooserActivity implements mEnterTransitionAnimationDelegate, new HeadlineGeneratorImpl(this), chooserRequest.getContentTypeHint(), - chooserRequest.getMetadataText() - ); + chooserRequest.getMetadataText(), + mChooserServiceFeatureFlags.chooserPayloadToggling()); updateStickyContentPreview(); if (shouldShowStickyContentPreview() || mChooserMultiProfilePagerAdapter @@ -807,10 +813,6 @@ public class ChooserActivity extends Hilt_ChooserActivity implements mChooserMultiProfilePagerAdapter.getActiveListAdapter().handlePackagesChanged(); } - public boolean super_shouldAutoLaunchSingleChoice(TargetInfo target) { - return !target.isSuspended(); - } - /** Start the activity specified by the {@link TargetInfo}.*/ public final void safelyStartActivity(TargetInfo cti) { // In case cloned apps are present, we would want to start those apps in cloned user @@ -1169,7 +1171,7 @@ public class ChooserActivity extends Hilt_ChooserActivity implements R.layout.resolver_profile_tab_button, com.android.internal.R.id.profile_pager, () -> onProfileTabSelected(viewPager.getCurrentItem()), - new MultiProfilePagerAdapter.OnProfileSelectedListener() { + new OnProfileSelectedListener() { @Override public void onProfilePageSelected(@ProfileType int profileId, int pageNumber) {} @@ -1652,14 +1654,12 @@ public class ChooserActivity extends Hilt_ChooserActivity implements } public boolean shouldAutoLaunchSingleChoice(TargetInfo target) { - // Note that this is only safe because the Intent handled by the ChooserActivity is - // guaranteed to contain no extras unknown to the local ClassLoader. That is why this - // method can not be replaced in the ResolverActivity whole hog. - if (!super_shouldAutoLaunchSingleChoice(target)) { + if (target.isSuspended()) { return false; } - return getIntent().getBooleanExtra(Intent.EXTRA_AUTO_LAUNCH_SINGLE_CHOICE, true); + return mActivityLaunch.getIntent().getBooleanExtra(Intent.EXTRA_AUTO_LAUNCH_SINGLE_CHOICE, + true); } private void showTargetDetails(TargetInfo targetInfo) { @@ -2155,7 +2155,8 @@ public class ChooserActivity extends Hilt_ChooserActivity implements setResult(status); } finish(); - }); + }, + mClipboardManager); } /* diff --git a/java/src/com/android/intentresolver/v2/ResolverActivity.java b/java/src/com/android/intentresolver/v2/ResolverActivity.java index 77d1dbf5..241b6735 100644 --- a/java/src/com/android/intentresolver/v2/ResolverActivity.java +++ b/java/src/com/android/intentresolver/v2/ResolverActivity.java @@ -100,15 +100,18 @@ import com.android.intentresolver.emptystate.EmptyStateProvider; import com.android.intentresolver.icons.DefaultTargetDataLoader; import com.android.intentresolver.icons.TargetDataLoader; import com.android.intentresolver.model.ResolverRankerServiceResolverComparator; -import com.android.intentresolver.v2.MultiProfilePagerAdapter.OnSwitchOnWorkSelectedListener; -import com.android.intentresolver.v2.MultiProfilePagerAdapter.ProfileType; -import com.android.intentresolver.v2.MultiProfilePagerAdapter.TabConfig; import com.android.intentresolver.v2.data.repository.DevicePolicyResources; -import com.android.intentresolver.v2.domain.model.Profile; +import com.android.intentresolver.v2.shared.model.Profile; import com.android.intentresolver.v2.emptystate.NoAppsAvailableEmptyStateProvider; import com.android.intentresolver.v2.emptystate.NoCrossProfileEmptyStateProvider; import com.android.intentresolver.v2.emptystate.NoCrossProfileEmptyStateProvider.DevicePolicyBlockerEmptyState; import com.android.intentresolver.v2.emptystate.WorkProfilePausedEmptyStateProvider; +import com.android.intentresolver.v2.profiles.MultiProfilePagerAdapter; +import com.android.intentresolver.v2.profiles.OnSwitchOnWorkSelectedListener; +import com.android.intentresolver.v2.profiles.MultiProfilePagerAdapter.ProfileType; +import com.android.intentresolver.v2.profiles.OnProfileSelectedListener; +import com.android.intentresolver.v2.profiles.TabConfig; +import com.android.intentresolver.v2.profiles.ResolverMultiProfilePagerAdapter; import com.android.intentresolver.v2.ui.ActionTitle; import com.android.intentresolver.v2.ui.model.ActivityLaunch; import com.android.intentresolver.v2.ui.model.ResolverRequest; @@ -1865,7 +1868,7 @@ public class ResolverActivity extends Hilt_ResolverActivity implements R.layout.resolver_profile_tab_button, com.android.internal.R.id.profile_pager, () -> onProfileTabSelected(viewPager.getCurrentItem()), - new MultiProfilePagerAdapter.OnProfileSelectedListener() { + new OnProfileSelectedListener() { @Override public void onProfilePageSelected(@ProfileType int profileId, int pageNumber) { resetButtonBar(); diff --git a/java/src/com/android/intentresolver/v2/domain/interactor/UserInteractor.kt b/java/src/com/android/intentresolver/v2/domain/interactor/UserInteractor.kt index f12d8197..c8df9684 100644 --- a/java/src/com/android/intentresolver/v2/domain/interactor/UserInteractor.kt +++ b/java/src/com/android/intentresolver/v2/domain/interactor/UserInteractor.kt @@ -19,8 +19,8 @@ package com.android.intentresolver.v2.domain.interactor import android.os.UserHandle import com.android.intentresolver.inject.ApplicationUser import com.android.intentresolver.v2.data.repository.UserRepository -import com.android.intentresolver.v2.domain.model.Profile -import com.android.intentresolver.v2.domain.model.Profile.Type +import com.android.intentresolver.v2.shared.model.Profile +import com.android.intentresolver.v2.shared.model.Profile.Type import com.android.intentresolver.v2.shared.model.User import com.android.intentresolver.v2.shared.model.User.Role import javax.inject.Inject diff --git a/java/src/com/android/intentresolver/v2/ext/IntentExt.kt b/java/src/com/android/intentresolver/v2/ext/IntentExt.kt index 7aa8e036..8c2d7277 100644 --- a/java/src/com/android/intentresolver/v2/ext/IntentExt.kt +++ b/java/src/com/android/intentresolver/v2/ext/IntentExt.kt @@ -32,8 +32,14 @@ inline fun Intent.ifMatch( /** True if the Intent has one of the specified actions. */ fun Intent.hasAction(vararg actions: String): Boolean = action in actions +/** True if the Intent has a specific component target */ +fun Intent.hasComponent(): Boolean = (component != null) + /** True if the Intent has a single matching category. */ fun Intent.hasSingleCategory(category: String) = categories.singleOrNull() == category +/** True if the Intent is a SEND or SEND_MULTIPLE action. */ +fun Intent.hasSendAction() = hasAction(Intent.ACTION_SEND, Intent.ACTION_SEND_MULTIPLE) + /** True if the Intent resolves to the special Home (Launcher) component */ fun Intent.isHomeIntent() = hasAction(Intent.ACTION_MAIN) && hasSingleCategory(Intent.CATEGORY_HOME) diff --git a/java/src/com/android/intentresolver/v2/profiles/AdapterBinder.java b/java/src/com/android/intentresolver/v2/profiles/AdapterBinder.java new file mode 100644 index 00000000..c5b35273 --- /dev/null +++ b/java/src/com/android/intentresolver/v2/profiles/AdapterBinder.java @@ -0,0 +1,31 @@ +/* + * 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.v2.profiles; + +/** + * Delegate to set up a given adapter and page view to be used together. + * + * @param <PageViewT> (as in {@link MultiProfilePagerAdapter}). + * @param <SinglePageAdapterT> (as in {@link MultiProfilePagerAdapter}). + */ +public interface AdapterBinder<PageViewT, SinglePageAdapterT> { + /** + * The given {@code view} will be associated with the given {@code adapter}. Do any work + * necessary to configure them compatibly, introduce them to each other, etc. + */ + void bind(PageViewT view, SinglePageAdapterT adapter); +} diff --git a/java/src/com/android/intentresolver/v2/ChooserMultiProfilePagerAdapter.java b/java/src/com/android/intentresolver/v2/profiles/ChooserMultiProfilePagerAdapter.java index 42eb077b..0ee9d141 100644 --- a/java/src/com/android/intentresolver/v2/ChooserMultiProfilePagerAdapter.java +++ b/java/src/com/android/intentresolver/v2/profiles/ChooserMultiProfilePagerAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019 The Android Open Source Project + * 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.intentresolver.v2; +package com.android.intentresolver.v2.profiles; import android.content.Context; import android.os.UserHandle; diff --git a/java/src/com/android/intentresolver/v2/MultiProfilePagerAdapter.java b/java/src/com/android/intentresolver/v2/profiles/MultiProfilePagerAdapter.java index 79403095..43785db3 100644 --- a/java/src/com/android/intentresolver/v2/MultiProfilePagerAdapter.java +++ b/java/src/com/android/intentresolver/v2/profiles/MultiProfilePagerAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019 The Android Open Source Project + * 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. @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.android.intentresolver.v2; +package com.android.intentresolver.v2.profiles; import android.annotation.IntDef; import android.annotation.Nullable; @@ -32,7 +32,6 @@ import androidx.viewpager.widget.ViewPager; import com.android.intentresolver.ResolverListAdapter; import com.android.intentresolver.emptystate.EmptyState; import com.android.intentresolver.emptystate.EmptyStateProvider; -import com.android.intentresolver.v2.emptystate.EmptyStateUiHelper; import com.android.internal.annotations.VisibleForTesting; import com.google.common.collect.ImmutableList; @@ -48,20 +47,6 @@ import java.util.function.Supplier; /** * Skeletal {@link PagerAdapter} implementation for a UI with per-profile tabs (as in Sharesheet). - * <p> - * TODO: attempt to further restrict visibility/improve encapsulation in the methods we expose. - * <p> - * TODO: deprecate and audit/fix usages of any methods that refer to the "active" or "inactive" - * <p> - * adapters; these were marked {@link VisibleForTesting} and their usage seems like an accident - * waiting to happen since clients seem to make assumptions about which adapter will be "active" in - * a particular context, and more explicit APIs would make sure those were valid. - * <p> - * TODO: consider renaming legacy methods (e.g. why do we know it's a "list", not just a "page"?) - * <p> - * TODO: this is part of an in-progress refactor to merge with `GenericMultiProfilePagerAdapter`. - * As originally noted there, we've reduced explicit references to the `ResolverListAdapter` base - * type and may be able to drop the type constraint. * * @param <PageViewT> the type of the widget that represents the contents of a page in this adapter * @param <SinglePageAdapterT> the type of a "root" adapter class to be instantiated and included in @@ -71,24 +56,11 @@ import java.util.function.Supplier; * be possible to get the list adapter from the page adapter via our * <code>mListAdapterExtractor</code>. */ -class MultiProfilePagerAdapter< +public class MultiProfilePagerAdapter< PageViewT extends ViewGroup, SinglePageAdapterT, ListAdapterT extends ResolverListAdapter> extends PagerAdapter { - /** - * Delegate to set up a given adapter and page view to be used together. - * @param <PageViewT> (as in {@link MultiProfilePagerAdapter}). - * @param <SinglePageAdapterT> (as in {@link MultiProfilePagerAdapter}). - */ - public interface AdapterBinder<PageViewT, SinglePageAdapterT> { - /** - * The given {@code view} will be associated with the given {@code adapter}. Do any work - * necessary to configure them compatibly, introduce them to each other, etc. - */ - void bind(PageViewT view, SinglePageAdapterT adapter); - } - public static final int PROFILE_PERSONAL = 0; public static final int PROFILE_WORK = 1; @@ -110,27 +82,6 @@ class MultiProfilePagerAdapter< private int mCurrentPage; private OnProfileSelectedListener mOnProfileSelectedListener; - public static class TabConfig<PageAdapterT> { - private final @ProfileType int mProfile; - private final String mTabLabel; - private final String mTabAccessibilityLabel; - private final String mTabTag; - private final PageAdapterT mPageAdapter; - - public TabConfig( - @ProfileType int profile, - String tabLabel, - String tabAccessibilityLabel, - String tabTag, - PageAdapterT pageAdapter) { - mProfile = profile; - mTabLabel = tabLabel; - mTabAccessibilityLabel = tabAccessibilityLabel; - mTabTag = tabTag; - mPageAdapter = pageAdapter; - } - } - protected MultiProfilePagerAdapter( Function<SinglePageAdapterT, ListAdapterT> listAdapterExtractor, AdapterBinder<PageViewT, SinglePageAdapterT> adapterBinder, @@ -291,7 +242,7 @@ class MultiProfilePagerAdapter< int tabButtonLayoutResId, int tabPageContentViewId, Runnable onTabChangeListener, - MultiProfilePagerAdapter.OnProfileSelectedListener clientOnProfileSelectedListener) { + OnProfileSelectedListener clientOnProfileSelectedListener) { tabHost.setup(); viewPager.setSaveEnabled(false); @@ -325,7 +276,7 @@ class MultiProfilePagerAdapter< viewPager.setVisibility(View.VISIBLE); tabHost.setCurrentTab(getCurrentPage()); mOnProfileSelectedListener = - new MultiProfilePagerAdapter.OnProfileSelectedListener() { + new OnProfileSelectedListener() { @Override public void onProfilePageSelected(@ProfileType int profileId, int pageNumber) { tabHost.setCurrentTab(pageNumber); @@ -457,7 +408,7 @@ class MultiProfilePagerAdapter< } public final PageViewT getListViewForIndex(int index) { - return getItem(index).mView; + return getItem(index).getView(); } /** @@ -471,7 +422,7 @@ class MultiProfilePagerAdapter< if (!hasPageForIndex(index)) { return null; } - return getItem(index).mAdapter; + return getItem(index).getAdapter(); } /** @@ -692,18 +643,6 @@ class MultiProfilePagerAdapter< showEmptyState(listAdapter, emptyState, clickListener); } - /** - * Class to get user id of the current process - */ - public static class MyUserIdProvider { - /** - * @return user id of the current process - */ - public int getMyUserId() { - return UserHandle.myUserId(); - } - } - private void showEmptyState( ListAdapterT activeListAdapter, EmptyState emptyState, @@ -750,86 +689,4 @@ class MultiProfilePagerAdapter< && mWorkProfileQuietModeChecker.get()); } - // TODO: `ChooserActivity` also has a per-profile record type. Maybe the "multi-profile pager" - // should be the owner of all per-profile data (especially now that the API is generic)? - private static class ProfileDescriptor<PageViewT, SinglePageAdapterT> { - final @ProfileType int mProfile; - final String mTabLabel; - final String mTabAccessibilityLabel; - final String mTabTag; - - final ViewGroup mRootView; - final EmptyStateUiHelper mEmptyStateUi; - - // TODO: post-refactoring, we may not need to retain these ivars directly (since they may - // be encapsulated within the `EmptyStateUiHelper`?). - private final ViewGroup mEmptyStateView; - - private final SinglePageAdapterT mAdapter; - private final PageViewT mView; - - ProfileDescriptor( - @ProfileType int forProfile, - String tabLabel, - String tabAccessibilityLabel, - String tabTag, - ViewGroup rootView, - SinglePageAdapterT adapter, - Supplier<Optional<Integer>> containerBottomPaddingOverrideSupplier) { - mProfile = forProfile; - mTabLabel = tabLabel; - mTabAccessibilityLabel = tabAccessibilityLabel; - mTabTag = tabTag; - mRootView = rootView; - mAdapter = adapter; - mEmptyStateView = rootView.findViewById(com.android.internal.R.id.resolver_empty_state); - mView = (PageViewT) rootView.findViewById(com.android.internal.R.id.resolver_list); - mEmptyStateUi = new EmptyStateUiHelper( - rootView, - com.android.internal.R.id.resolver_list, - containerBottomPaddingOverrideSupplier); - } - - protected ViewGroup getEmptyStateView() { - return mEmptyStateView; - } - - private void setupContainerPadding() { - mEmptyStateUi.setupContainerPadding(); - } - } - - /** Listener interface for changes between the per-profile UI tabs. */ - public interface OnProfileSelectedListener { - /** - * Callback for when the user changes the active tab. - * <p>This callback is only called when the intent resolver or share sheet shows - * more than one profile. - * @param profileId the ID of the newly-selected profile, e.g. {@link #PROFILE_PERSONAL} - * if the personal profile tab was selected or {@link #PROFILE_WORK} if the work profile tab - * was selected. - */ - void onProfilePageSelected(@ProfileType int profileId, int pageNumber); - - - /** - * Callback for when the scroll state changes. Useful for discovering when the user begins - * dragging, when the pager is automatically settling to the current page, or when it is - * fully stopped/idle. - * @param state {@link ViewPager#SCROLL_STATE_IDLE}, {@link ViewPager#SCROLL_STATE_DRAGGING} - * or {@link ViewPager#SCROLL_STATE_SETTLING} - * @see ViewPager.OnPageChangeListener#onPageScrollStateChanged - */ - void onProfilePageStateChanged(int state); - } - - /** - * Listener for when the user switches on the work profile from the work tab. - */ - public interface OnSwitchOnWorkSelectedListener { - /** - * Callback for when the user switches on the work profile from the work tab. - */ - void onSwitchOnWorkSelected(); - } } diff --git a/java/src/com/android/intentresolver/v2/profiles/OnProfileSelectedListener.java b/java/src/com/android/intentresolver/v2/profiles/OnProfileSelectedListener.java new file mode 100644 index 00000000..7bdbec4c --- /dev/null +++ b/java/src/com/android/intentresolver/v2/profiles/OnProfileSelectedListener.java @@ -0,0 +1,46 @@ +/* + * 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.v2.profiles; + +import androidx.viewpager.widget.ViewPager; + +/** Listener interface for changes between the per-profile UI tabs. */ +public interface OnProfileSelectedListener { + /** + * Callback for when the user changes the active tab. + * <p>This callback is only called when the intent resolver or share sheet shows + * more than one profile. + * + * @param profileId the ID of the newly-selected profile, e.g. {@link #PROFILE_PERSONAL} + * if the personal profile tab was selected or {@link #PROFILE_WORK} if the + * work profile tab + * was selected. + */ + void onProfilePageSelected(@MultiProfilePagerAdapter.ProfileType int profileId, int pageNumber); + + + /** + * Callback for when the scroll state changes. Useful for discovering when the user begins + * dragging, when the pager is automatically settling to the current page, or when it is + * fully stopped/idle. + * + * @param state {@link ViewPager#SCROLL_STATE_IDLE}, {@link ViewPager#SCROLL_STATE_DRAGGING} + * or {@link ViewPager#SCROLL_STATE_SETTLING} + * @see ViewPager.OnPageChangeListener#onPageScrollStateChanged + */ + void onProfilePageStateChanged(int state); +} diff --git a/java/src/com/android/intentresolver/v2/profiles/OnSwitchOnWorkSelectedListener.java b/java/src/com/android/intentresolver/v2/profiles/OnSwitchOnWorkSelectedListener.java new file mode 100644 index 00000000..3dbbd4d0 --- /dev/null +++ b/java/src/com/android/intentresolver/v2/profiles/OnSwitchOnWorkSelectedListener.java @@ -0,0 +1,27 @@ +/* + * 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.v2.profiles; + +/** + * Listener for when the user switches on the work profile from the work tab. + */ +public interface OnSwitchOnWorkSelectedListener { + /** + * Callback for when the user switches on the work profile from the work tab. + */ + void onSwitchOnWorkSelected(); +} diff --git a/java/src/com/android/intentresolver/v2/profiles/ProfileDescriptor.java b/java/src/com/android/intentresolver/v2/profiles/ProfileDescriptor.java new file mode 100644 index 00000000..e2e9c19d --- /dev/null +++ b/java/src/com/android/intentresolver/v2/profiles/ProfileDescriptor.java @@ -0,0 +1,82 @@ +/* + * 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.v2.profiles; + +import android.view.ViewGroup; + +import com.android.intentresolver.v2.emptystate.EmptyStateUiHelper; + +import java.util.Optional; +import java.util.function.Supplier; + +// TODO: `ChooserActivity` also has a per-profile record type. Maybe the "multi-profile pager" +// should be the owner of all per-profile data (especially now that the API is generic)? +class ProfileDescriptor<PageViewT, SinglePageAdapterT> { + final @MultiProfilePagerAdapter.ProfileType int mProfile; + final String mTabLabel; + final String mTabAccessibilityLabel; + final String mTabTag; + + final ViewGroup mRootView; + final EmptyStateUiHelper mEmptyStateUi; + + // TODO: post-refactoring, we may not need to retain these ivars directly (since they may + // be encapsulated within the `EmptyStateUiHelper`?). + private final ViewGroup mEmptyStateView; + + private final SinglePageAdapterT mAdapter; + + public SinglePageAdapterT getAdapter() { + return mAdapter; + } + + public PageViewT getView() { + return mView; + } + + private final PageViewT mView; + + ProfileDescriptor( + @MultiProfilePagerAdapter.ProfileType int forProfile, + String tabLabel, + String tabAccessibilityLabel, + String tabTag, + ViewGroup rootView, + SinglePageAdapterT adapter, + Supplier<Optional<Integer>> containerBottomPaddingOverrideSupplier) { + mProfile = forProfile; + mTabLabel = tabLabel; + mTabAccessibilityLabel = tabAccessibilityLabel; + mTabTag = tabTag; + mRootView = rootView; + mAdapter = adapter; + mEmptyStateView = rootView.findViewById(com.android.internal.R.id.resolver_empty_state); + mView = (PageViewT) rootView.findViewById(com.android.internal.R.id.resolver_list); + mEmptyStateUi = new EmptyStateUiHelper( + rootView, + com.android.internal.R.id.resolver_list, + containerBottomPaddingOverrideSupplier); + } + + protected ViewGroup getEmptyStateView() { + return mEmptyStateView; + } + + public void setupContainerPadding() { + mEmptyStateUi.setupContainerPadding(); + } +} diff --git a/java/src/com/android/intentresolver/v2/ResolverMultiProfilePagerAdapter.java b/java/src/com/android/intentresolver/v2/profiles/ResolverMultiProfilePagerAdapter.java index c2e1ae07..e44cf8da 100644 --- a/java/src/com/android/intentresolver/v2/ResolverMultiProfilePagerAdapter.java +++ b/java/src/com/android/intentresolver/v2/profiles/ResolverMultiProfilePagerAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019 The Android Open Source Project + * 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.intentresolver.v2; +package com.android.intentresolver.v2.profiles; import android.content.Context; import android.os.UserHandle; diff --git a/java/src/com/android/intentresolver/v2/profiles/TabConfig.java b/java/src/com/android/intentresolver/v2/profiles/TabConfig.java new file mode 100644 index 00000000..994f8aff --- /dev/null +++ b/java/src/com/android/intentresolver/v2/profiles/TabConfig.java @@ -0,0 +1,38 @@ +/* + * 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.v2.profiles; + +public class TabConfig<PageAdapterT> { + final @MultiProfilePagerAdapter.ProfileType int mProfile; + final String mTabLabel; + final String mTabAccessibilityLabel; + final String mTabTag; + final PageAdapterT mPageAdapter; + + public TabConfig( + @MultiProfilePagerAdapter.ProfileType int profile, + String tabLabel, + String tabAccessibilityLabel, + String tabTag, + PageAdapterT pageAdapter) { + mProfile = profile; + mTabLabel = tabLabel; + mTabAccessibilityLabel = tabAccessibilityLabel; + mTabTag = tabTag; + mPageAdapter = pageAdapter; + } +} diff --git a/java/src/com/android/intentresolver/v2/domain/model/Profile.kt b/java/src/com/android/intentresolver/v2/shared/model/Profile.kt index 46015c7a..6e37174c 100644 --- a/java/src/com/android/intentresolver/v2/domain/model/Profile.kt +++ b/java/src/com/android/intentresolver/v2/shared/model/Profile.kt @@ -14,13 +14,12 @@ * limitations under the License. */ -package com.android.intentresolver.v2.domain.model +package com.android.intentresolver.v2.shared.model -import com.android.intentresolver.v2.domain.model.Profile.Type -import com.android.intentresolver.v2.shared.model.User +import com.android.intentresolver.v2.shared.model.Profile.Type /** - * A domain layer model which associates [users][User] into a [Type] instance. + * Associates [users][User] into a [Type] instance. * * This is a simple abstraction which combines a primary [user][User] with an optional * [cloned apps][User.Role.CLONE] user. This encapsulates the cloned app user id, while still being diff --git a/java/src/com/android/intentresolver/v2/ui/ProfilePagerResources.kt b/java/src/com/android/intentresolver/v2/ui/ProfilePagerResources.kt index 0d31b23e..1cd72ba5 100644 --- a/java/src/com/android/intentresolver/v2/ui/ProfilePagerResources.kt +++ b/java/src/com/android/intentresolver/v2/ui/ProfilePagerResources.kt @@ -19,7 +19,7 @@ package com.android.intentresolver.v2.ui import android.content.res.Resources import com.android.intentresolver.inject.ApplicationOwned import com.android.intentresolver.v2.data.repository.DevicePolicyResources -import com.android.intentresolver.v2.domain.model.Profile +import com.android.intentresolver.v2.shared.model.Profile import javax.inject.Inject import com.android.intentresolver.R diff --git a/java/src/com/android/intentresolver/v2/ui/model/ResolverRequest.kt b/java/src/com/android/intentresolver/v2/ui/model/ResolverRequest.kt index 5abfb602..a4f74ca9 100644 --- a/java/src/com/android/intentresolver/v2/ui/model/ResolverRequest.kt +++ b/java/src/com/android/intentresolver/v2/ui/model/ResolverRequest.kt @@ -19,7 +19,7 @@ package com.android.intentresolver.v2.ui.model import android.content.Intent import android.content.pm.ResolveInfo import android.os.UserHandle -import com.android.intentresolver.v2.domain.model.Profile +import com.android.intentresolver.v2.shared.model.Profile import com.android.intentresolver.v2.ext.isHomeIntent /** All of the things that are consumed from an incoming Intent Resolution request (+Extras). */ diff --git a/java/src/com/android/intentresolver/v2/ui/viewmodel/ChooserRequestReader.kt b/java/src/com/android/intentresolver/v2/ui/viewmodel/ChooserRequestReader.kt index 565d4de1..e32d69b0 100644 --- a/java/src/com/android/intentresolver/v2/ui/viewmodel/ChooserRequestReader.kt +++ b/java/src/com/android/intentresolver/v2/ui/viewmodel/ChooserRequestReader.kt @@ -17,8 +17,6 @@ package com.android.intentresolver.v2.ui.viewmodel import android.content.ComponentName import android.content.Intent -import android.content.Intent.ACTION_SEND -import android.content.Intent.ACTION_SEND_MULTIPLE import android.content.Intent.EXTRA_ALTERNATE_INTENTS import android.content.Intent.EXTRA_CHOOSER_CUSTOM_ACTIONS import android.content.Intent.EXTRA_CHOOSER_MODIFY_SHARE_ACTION @@ -47,7 +45,7 @@ import com.android.intentresolver.ContentTypeHint import com.android.intentresolver.R import com.android.intentresolver.inject.ChooserServiceFlags import com.android.intentresolver.util.hasValidIcon -import com.android.intentresolver.v2.ext.hasAction +import com.android.intentresolver.v2.ext.hasSendAction import com.android.intentresolver.v2.ext.ifMatch import com.android.intentresolver.v2.ui.model.ActivityLaunch import com.android.intentresolver.v2.ui.model.ChooserRequest @@ -60,8 +58,6 @@ import com.android.intentresolver.v2.validation.validateFrom private const val MAX_CHOOSER_ACTIONS = 5 private const val MAX_INITIAL_INTENTS = 2 -private fun Intent.hasSendAction() = hasAction(ACTION_SEND, ACTION_SEND_MULTIPLE) - internal fun Intent.maybeAddSendActionFlags() = ifMatch(Intent::hasSendAction) { addFlags(FLAG_ACTIVITY_NEW_DOCUMENT) @@ -77,7 +73,7 @@ fun readChooserRequest( return validateFrom(extras::get) { val targetIntent = required(IntentOrUri(EXTRA_INTENT)).maybeAddSendActionFlags() - val isSendAction = targetIntent.hasAction(ACTION_SEND, ACTION_SEND_MULTIPLE) + val isSendAction = targetIntent.hasSendAction() val additionalTargets = optional(array<Intent>(EXTRA_ALTERNATE_INTENTS))?.map { it.maybeAddSendActionFlags() } diff --git a/java/src/com/android/intentresolver/v2/ui/viewmodel/ResolverRequestReader.kt b/java/src/com/android/intentresolver/v2/ui/viewmodel/ResolverRequestReader.kt index fc9f1e01..22d76493 100644 --- a/java/src/com/android/intentresolver/v2/ui/viewmodel/ResolverRequestReader.kt +++ b/java/src/com/android/intentresolver/v2/ui/viewmodel/ResolverRequestReader.kt @@ -20,7 +20,7 @@ import android.os.Bundle import android.os.UserHandle import com.android.intentresolver.v2.ResolverActivity.PROFILE_PERSONAL import com.android.intentresolver.v2.ResolverActivity.PROFILE_WORK -import com.android.intentresolver.v2.domain.model.Profile +import com.android.intentresolver.v2.shared.model.Profile import com.android.intentresolver.v2.ui.model.ActivityLaunch import com.android.intentresolver.v2.ui.model.ResolverRequest import com.android.intentresolver.v2.validation.Validation diff --git a/tests/shared/src/com/android/intentresolver/v2/data/repository/FakeUserRepository.kt b/tests/shared/src/com/android/intentresolver/v2/data/repository/FakeUserRepository.kt index 5ed6f506..73d9a084 100644 --- a/tests/shared/src/com/android/intentresolver/v2/data/repository/FakeUserRepository.kt +++ b/tests/shared/src/com/android/intentresolver/v2/data/repository/FakeUserRepository.kt @@ -22,7 +22,7 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.update /** A simple repository which can be initialized from a list and updated. */ -class FakeUserRepository(vararg userList: User) : UserRepository { +class FakeUserRepository(userList: List<User>) : UserRepository { internal data class UserState(val user: User, val available: Boolean) private val userState = MutableStateFlow(userList.map { UserState(it, available = true) }) @@ -47,7 +47,7 @@ class FakeUserRepository(vararg userList: User) : UserRepository { override val availability = userState.map { userStateList -> userStateList.associate { it.user to it.available } } - override suspend fun requestState(user: User, available: Boolean) { + fun updateState(user: User, available: Boolean) { userState.update { userStateList -> userStateList.map { userState -> if (userState.user.id == user.id) { @@ -58,4 +58,8 @@ class FakeUserRepository(vararg userList: User) : UserRepository { } } } + + override suspend fun requestState(user: User, available: Boolean) { + updateState(user, available) + } } diff --git a/tests/unit/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUiTest.kt b/tests/unit/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUiTest.kt index ad53eef4..c7c3c516 100644 --- a/tests/unit/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUiTest.kt +++ b/tests/unit/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUiTest.kt @@ -18,7 +18,6 @@ package com.android.intentresolver.contentpreview import android.content.Intent import android.net.Uri -import android.platform.test.annotations.RequiresFlagsDisabled import android.platform.test.flag.junit.CheckFlagsRule import android.platform.test.flag.junit.DeviceFlagsValueProvider import com.android.intentresolver.ContentTypeHint @@ -49,29 +48,40 @@ class ChooserContentPreviewUiTest { private val actionFactory = object : ActionFactory { override fun getCopyButtonRunnable(): Runnable? = null + override fun getEditButtonRunnable(): Runnable? = null + override fun createCustomActions(): List<ActionRow.Action> = emptyList() + override fun getModifyShareAction(): ActionRow.Action? = null + override fun getExcludeSharedTextAction(): Consumer<Boolean> = Consumer<Boolean> {} } private val transitionCallback = mock<ImagePreviewView.TransitionElementStatusCallback>() @get:Rule val checkFlagsRule: CheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule() + private fun createContentPreviewUi( + targetIntent: Intent, + isPayloadTogglingEnabled: Boolean = false + ) = + ChooserContentPreviewUi( + testScope, + previewData, + targetIntent, + imageLoader, + actionFactory, + transitionCallback, + headlineGenerator, + ContentTypeHint.NONE, + testMetadataText, + isPayloadTogglingEnabled, + ) + @Test fun test_textPreviewType_useTextPreviewUi() { whenever(previewData.previewType).thenReturn(ContentPreviewType.CONTENT_PREVIEW_TEXT) - val testSubject = - ChooserContentPreviewUi( - testScope, - previewData, - Intent(Intent.ACTION_VIEW), - imageLoader, - actionFactory, - transitionCallback, - headlineGenerator, - ContentTypeHint.NONE, - testMetadataText, - ) + val testSubject = createContentPreviewUi(targetIntent = Intent(Intent.ACTION_VIEW)) + assertThat(testSubject.preferredContentPreview) .isEqualTo(ContentPreviewType.CONTENT_PREVIEW_TEXT) assertThat(testSubject.mContentPreviewUi).isInstanceOf(TextContentPreviewUi::class.java) @@ -81,18 +91,7 @@ class ChooserContentPreviewUiTest { @Test fun test_filePreviewType_useFilePreviewUi() { whenever(previewData.previewType).thenReturn(ContentPreviewType.CONTENT_PREVIEW_FILE) - val testSubject = - ChooserContentPreviewUi( - testScope, - previewData, - Intent(Intent.ACTION_SEND), - imageLoader, - actionFactory, - transitionCallback, - headlineGenerator, - ContentTypeHint.NONE, - testMetadataText, - ) + val testSubject = createContentPreviewUi(targetIntent = Intent(Intent.ACTION_SEND)) assertThat(testSubject.preferredContentPreview) .isEqualTo(ContentPreviewType.CONTENT_PREVIEW_FILE) assertThat(testSubject.mContentPreviewUi).isInstanceOf(FileContentPreviewUi::class.java) @@ -108,16 +107,9 @@ class ChooserContentPreviewUiTest { .thenReturn(FileInfo.Builder(uri).withPreviewUri(uri).withMimeType("image/png").build()) whenever(previewData.imagePreviewFileInfoFlow).thenReturn(MutableSharedFlow()) val testSubject = - ChooserContentPreviewUi( - testScope, - previewData, - Intent(Intent.ACTION_SEND).apply { putExtra(Intent.EXTRA_TEXT, "Shared text") }, - imageLoader, - actionFactory, - transitionCallback, - headlineGenerator, - ContentTypeHint.NONE, - testMetadataText, + createContentPreviewUi( + targetIntent = + Intent(Intent.ACTION_SEND).apply { putExtra(Intent.EXTRA_TEXT, "Shared text") } ) assertThat(testSubject.mContentPreviewUi) .isInstanceOf(FilesPlusTextContentPreviewUi::class.java) @@ -133,18 +125,7 @@ class ChooserContentPreviewUiTest { whenever(previewData.firstFileInfo) .thenReturn(FileInfo.Builder(uri).withPreviewUri(uri).withMimeType("image/png").build()) whenever(previewData.imagePreviewFileInfoFlow).thenReturn(MutableSharedFlow()) - val testSubject = - ChooserContentPreviewUi( - testScope, - previewData, - Intent(Intent.ACTION_SEND), - imageLoader, - actionFactory, - transitionCallback, - headlineGenerator, - ContentTypeHint.NONE, - testMetadataText, - ) + val testSubject = createContentPreviewUi(targetIntent = Intent(Intent.ACTION_SEND)) assertThat(testSubject.preferredContentPreview) .isEqualTo(ContentPreviewType.CONTENT_PREVIEW_IMAGE) assertThat(testSubject.mContentPreviewUi).isInstanceOf(UnifiedContentPreviewUi::class.java) @@ -153,8 +134,7 @@ class ChooserContentPreviewUiTest { } @Test - @RequiresFlagsDisabled(android.service.chooser.Flags.FLAG_CHOOSER_PAYLOAD_TOGGLING) - fun test_imagePayloadSelectionType_useImagePreviewUi() { + fun test_imagePayloadSelectionTypeWithEnabledFlag_usePayloadSelectionPreviewUi() { // Event if we returned wrong type due to a bug, we should not use payload selection UI val uri = Uri.parse("content://org.pkg.app/img.png") whenever(previewData.previewType) @@ -164,21 +144,11 @@ class ChooserContentPreviewUiTest { .thenReturn(FileInfo.Builder(uri).withPreviewUri(uri).withMimeType("image/png").build()) whenever(previewData.imagePreviewFileInfoFlow).thenReturn(MutableSharedFlow()) val testSubject = - ChooserContentPreviewUi( - testScope, - previewData, - Intent(Intent.ACTION_SEND), - imageLoader, - actionFactory, - transitionCallback, - headlineGenerator, - ContentTypeHint.NONE, - testMetadataText, + createContentPreviewUi( + targetIntent = Intent(Intent.ACTION_SEND), + isPayloadTogglingEnabled = true ) - assertThat(testSubject.preferredContentPreview) - .isEqualTo(ContentPreviewType.CONTENT_PREVIEW_IMAGE) - assertThat(testSubject.mContentPreviewUi).isInstanceOf(UnifiedContentPreviewUi::class.java) - verify(previewData, times(1)).imagePreviewFileInfoFlow - verify(transitionCallback, never()).onAllTransitionElementsReady() + assertThat(testSubject.mContentPreviewUi) + .isInstanceOf(ShareouselContentPreviewUi::class.java) } } diff --git a/tests/unit/src/com/android/intentresolver/v2/ChooserActionFactoryTest.kt b/tests/unit/src/com/android/intentresolver/v2/ChooserActionFactoryTest.kt index 717d26bd..95e4c377 100644 --- a/tests/unit/src/com/android/intentresolver/v2/ChooserActionFactoryTest.kt +++ b/tests/unit/src/com/android/intentresolver/v2/ChooserActionFactoryTest.kt @@ -31,6 +31,8 @@ import androidx.test.platform.app.InstrumentationRegistry import com.android.intentresolver.ChooserRequestParameters import com.android.intentresolver.logging.EventLog import com.android.intentresolver.mock +import com.android.intentresolver.v2.ui.ShareResultSender +import com.android.intentresolver.v2.ui.model.ShareAction import com.android.intentresolver.whenever import com.google.common.collect.ImmutableList import com.google.common.truth.Truth.assertThat @@ -45,7 +47,9 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentMatchers.eq -import org.mockito.Mockito +import org.mockito.Mockito.eq +import org.mockito.Mockito.times +import org.mockito.Mockito.verify @RunWith(AndroidJUnit4::class) class ChooserActionFactoryTest { @@ -94,7 +98,7 @@ class ChooserActionFactoryTest { // click it customActions[0].onClicked.run() - Mockito.verify(logger).logCustomActionSelected(eq(0)) + verify(logger).logCustomActionSelected(eq(0)) assertEquals(Activity.RESULT_OK, resultConsumer.latestReturn) // Verify the pending intent has been called assertTrue("Timed out waiting for broadcast", countdown.await(2500, TimeUnit.MILLISECONDS)) @@ -114,7 +118,7 @@ class ChooserActionFactoryTest { val action = factory.modifyShareAction ?: error("Modify share action should not be null") action.onClicked.run() - Mockito.verify(logger).logActionSelected(eq(EventLog.SELECTION_TYPE_MODIFY_SHARE)) + verify(logger).logActionSelected(eq(EventLog.SELECTION_TYPE_MODIFY_SHARE)) assertEquals(Activity.RESULT_OK, resultConsumer.latestReturn) // Verify the pending intent has been called assertTrue("Timed out waiting for broadcast", countdown.await(2500, TimeUnit.MILLISECONDS)) @@ -146,6 +150,7 @@ class ChooserActionFactoryTest { /* activityStarter = */ mock(), /* shareResultSender = */ null, /* finishCallback = */ {}, + /* clipboardManager = */ mock(), ) assertThat(testSubject.copyButtonRunnable).isNull() } @@ -173,12 +178,13 @@ class ChooserActionFactoryTest { /* activityStarter = */ mock(), /* shareResultSender = */ null, /* finishCallback = */ {}, + /* clipboardManager = */ mock(), ) assertThat(testSubject.copyButtonRunnable).isNull() } @Test - fun sendActionWithText_nonNullCopyRunnable() { + fun sendActionWithTextCopyRunnable() { val targetIntent = Intent(Intent.ACTION_SEND).apply { putExtra(Intent.EXTRA_TEXT, "Text") } val chooserRequest = @@ -186,6 +192,8 @@ class ChooserActionFactoryTest { whenever(this.targetIntent).thenReturn(targetIntent) whenever(chooserActions).thenReturn(ImmutableList.of()) } + + val resultSender = mock<ShareResultSender>() val testSubject = ChooserActionFactory( /* context = */ context, @@ -198,10 +206,15 @@ class ChooserActionFactoryTest { /* onUpdateSharedTextIsExcluded = */ {}, /* firstVisibleImageQuery = */ { null }, /* activityStarter = */ mock(), - /* shareResultSender = */ null, + /* shareResultSender = */ resultSender, /* finishCallback = */ {}, + /* clipboardManager = */ mock(), ) assertThat(testSubject.copyButtonRunnable).isNotNull() + + testSubject.copyButtonRunnable?.run() + + verify(resultSender, times(1)).onActionSelected(ShareAction.SYSTEM_COPY) } private fun createFactory(includeModifyShare: Boolean = false): ChooserActionFactory { @@ -242,7 +255,8 @@ class ChooserActionFactoryTest { /* firstVisibleImageQuery = */ { null }, /* activityStarter = */ mock(), /* shareResultSender = */ null, - /* finishCallback = */ resultConsumer + /* finishCallback = */ resultConsumer, + /* clipboardManager = */ mock(), ) } } diff --git a/tests/unit/src/com/android/intentresolver/v2/ChooserMutableActionFactoryTest.kt b/tests/unit/src/com/android/intentresolver/v2/ChooserMutableActionFactoryTest.kt index 42702cef..ec2b807d 100644 --- a/tests/unit/src/com/android/intentresolver/v2/ChooserMutableActionFactoryTest.kt +++ b/tests/unit/src/com/android/intentresolver/v2/ChooserMutableActionFactoryTest.kt @@ -110,7 +110,8 @@ class ChooserMutableActionFactoryTest { /* firstVisibleImageQuery = */ { null }, /* activityStarter = */ mock(), /* shareResultSender = */ null, - /* finishCallback = */ resultConsumer + /* finishCallback = */ resultConsumer, + mock() ) } diff --git a/tests/unit/src/com/android/intentresolver/v2/data/repository/FakeUserRepositoryTest.kt b/tests/unit/src/com/android/intentresolver/v2/data/repository/FakeUserRepositoryTest.kt index 334f31ad..d10ea8d0 100644 --- a/tests/unit/src/com/android/intentresolver/v2/data/repository/FakeUserRepositoryTest.kt +++ b/tests/unit/src/com/android/intentresolver/v2/data/repository/FakeUserRepositoryTest.kt @@ -33,7 +33,7 @@ class FakeUserRepositoryTest { @Test fun init() = runTest { - val repo = FakeUserRepository(personalUser, workUser, privateUser) + val repo = FakeUserRepository(listOf(personalUser, workUser, privateUser)) val users by collectLastValue(repo.users) assertThat(users).containsExactly(personalUser, workUser, privateUser) @@ -41,7 +41,7 @@ class FakeUserRepositoryTest { @Test fun addUser() = runTest { - val repo = FakeUserRepository() + val repo = FakeUserRepository(emptyList()) val users by collectLastValue(repo.users) assertThat(users).isEmpty() @@ -55,7 +55,7 @@ class FakeUserRepositoryTest { @Test fun removeUser() = runTest { - val repo = FakeUserRepository(personalUser, workUser) + val repo = FakeUserRepository(listOf(personalUser, workUser)) val users by collectLastValue(repo.users) repo.removeUser(workUser) @@ -67,7 +67,7 @@ class FakeUserRepositoryTest { @Test fun isAvailable_defaultValue() = runTest { - val repo = FakeUserRepository(personalUser, workUser) + val repo = FakeUserRepository(listOf(personalUser, workUser)) val available by collectLastValue(repo.availability) @@ -80,7 +80,7 @@ class FakeUserRepositoryTest { @Test fun isAvailable() = runTest { - val repo = FakeUserRepository(personalUser, workUser) + val repo = FakeUserRepository(listOf(personalUser, workUser)) val available by collectLastValue(repo.availability) assertThat(available!![workUser]).isTrue() @@ -94,7 +94,7 @@ class FakeUserRepositoryTest { @Test fun isAvailable_addRemove() = runTest { - val repo = FakeUserRepository(personalUser, workUser) + val repo = FakeUserRepository(listOf(personalUser, workUser)) val available by collectLastValue(repo.availability) assertThat(available!![workUser]).isTrue() diff --git a/tests/unit/src/com/android/intentresolver/v2/domain/interactor/UserInteractorTest.kt b/tests/unit/src/com/android/intentresolver/v2/domain/interactor/UserInteractorTest.kt index 4d246b9a..b66fabfd 100644 --- a/tests/unit/src/com/android/intentresolver/v2/domain/interactor/UserInteractorTest.kt +++ b/tests/unit/src/com/android/intentresolver/v2/domain/interactor/UserInteractorTest.kt @@ -18,10 +18,10 @@ package com.android.intentresolver.v2.domain.interactor import com.android.intentresolver.v2.coroutines.collectLastValue import com.android.intentresolver.v2.data.repository.FakeUserRepository -import com.android.intentresolver.v2.domain.model.Profile -import com.android.intentresolver.v2.domain.model.Profile.Type.PERSONAL -import com.android.intentresolver.v2.domain.model.Profile.Type.PRIVATE -import com.android.intentresolver.v2.domain.model.Profile.Type.WORK +import com.android.intentresolver.v2.shared.model.Profile +import com.android.intentresolver.v2.shared.model.Profile.Type.PERSONAL +import com.android.intentresolver.v2.shared.model.Profile.Type.PRIVATE +import com.android.intentresolver.v2.shared.model.Profile.Type.WORK import com.android.intentresolver.v2.shared.model.User import com.android.intentresolver.v2.shared.model.User.Role import com.google.common.truth.Truth.assertThat @@ -45,7 +45,7 @@ class UserInteractorTest { fun launchedByProfile(): Unit = runTest { val profileInteractor = UserInteractor( - userRepository = FakeUserRepository(personalUser, cloneUser), + userRepository = FakeUserRepository(listOf(personalUser, cloneUser)), launchedAs = personalUser.handle ) @@ -58,7 +58,7 @@ class UserInteractorTest { fun launchedByProfile_asClone(): Unit = runTest { val profileInteractor = UserInteractor( - userRepository = FakeUserRepository(personalUser, cloneUser), + userRepository = FakeUserRepository(listOf(personalUser, cloneUser)), launchedAs = cloneUser.handle ) val profiles by collectLastValue(profileInteractor.launchedAsProfile) @@ -70,7 +70,7 @@ class UserInteractorTest { fun profiles_withPersonal(): Unit = runTest { val profileInteractor = UserInteractor( - userRepository = FakeUserRepository(personalUser), + userRepository = FakeUserRepository(listOf(personalUser)), launchedAs = personalUser.handle ) @@ -81,7 +81,7 @@ class UserInteractorTest { @Test fun profiles_addClone(): Unit = runTest { - val fakeUserRepo = FakeUserRepository(personalUser) + val fakeUserRepo = FakeUserRepository(listOf(personalUser)) val profileInteractor = UserInteractor(userRepository = fakeUserRepo, launchedAs = personalUser.handle) @@ -96,7 +96,7 @@ class UserInteractorTest { fun profiles_withPersonalAndClone(): Unit = runTest { val profileInteractor = UserInteractor( - userRepository = FakeUserRepository(personalUser, cloneUser), + userRepository = FakeUserRepository(listOf(personalUser, cloneUser)), launchedAs = personalUser.handle ) val profiles by collectLastValue(profileInteractor.profiles) @@ -108,7 +108,8 @@ class UserInteractorTest { fun profiles_withAllSupportedTypes(): Unit = runTest { val profileInteractor = UserInteractor( - userRepository = FakeUserRepository(personalUser, cloneUser, workUser, privateUser), + userRepository = + FakeUserRepository(listOf(personalUser, cloneUser, workUser, privateUser)), launchedAs = personalUser.handle ) val profiles by collectLastValue(profileInteractor.profiles) @@ -125,7 +126,8 @@ class UserInteractorTest { fun profiles_preservesIterationOrder(): Unit = runTest { val profileInteractor = UserInteractor( - userRepository = FakeUserRepository(workUser, cloneUser, privateUser, personalUser), + userRepository = + FakeUserRepository(listOf(workUser, cloneUser, privateUser, personalUser)), launchedAs = personalUser.handle ) @@ -141,7 +143,7 @@ class UserInteractorTest { @Test fun isAvailable_defaultValue() = runTest { - val userRepo = FakeUserRepository(personalUser) + val userRepo = FakeUserRepository(listOf(personalUser)) userRepo.addUser(workUser, false) val profileInteractor = @@ -156,7 +158,7 @@ class UserInteractorTest { @Test fun isAvailable() = runTest { - val userRepo = FakeUserRepository(workUser, personalUser) + val userRepo = FakeUserRepository(listOf(workUser, personalUser)) val profileInteractor = UserInteractor(userRepository = userRepo, launchedAs = personalUser.handle) val workAvailable by collectLastValue(profileInteractor.isAvailable(WORK)) @@ -183,7 +185,7 @@ class UserInteractorTest { */ @Test fun updateState() = runTest { - val userRepo = FakeUserRepository(workUser, personalUser) + val userRepo = FakeUserRepository(listOf(workUser, personalUser)) val userInteractor = UserInteractor(userRepository = userRepo, launchedAs = personalUser.handle) val workProfile = Profile(Profile.Type.WORK, workUser) diff --git a/tests/unit/src/com/android/intentresolver/v2/ext/IntentExtTest.kt b/tests/unit/src/com/android/intentresolver/v2/ext/IntentExtTest.kt index 6a16168c..2ccd548a 100644 --- a/tests/unit/src/com/android/intentresolver/v2/ext/IntentExtTest.kt +++ b/tests/unit/src/com/android/intentresolver/v2/ext/IntentExtTest.kt @@ -15,6 +15,7 @@ */ package com.android.intentresolver.v2.ext +import android.content.ComponentName import android.content.Intent import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertWithMessage @@ -36,6 +37,20 @@ class IntentExtTest { } @Test + fun hasComponent() { + assertThat(Intent().hasComponent()).isFalse() + assertThat(Intent().setComponent(ComponentName("A", "B")).hasComponent()).isTrue() + } + + @Test + fun hasSendAction() { + assertThat(Intent(Intent.ACTION_SEND).hasSendAction()).isTrue() + assertThat(Intent(Intent.ACTION_SEND_MULTIPLE).hasSendAction()).isTrue() + assertThat(Intent(Intent.ACTION_SENDTO).hasSendAction()).isFalse() + assertThat(Intent(Intent.ACTION_VIEW).hasSendAction()).isFalse() + } + + @Test fun hasSingleCategory() { val intent = Intent().addCategory(Intent.CATEGORY_HOME) assertThat(intent.hasSingleCategory(Intent.CATEGORY_HOME)).isTrue() diff --git a/tests/unit/src/com/android/intentresolver/v2/MultiProfilePagerAdapterTest.kt b/tests/unit/src/com/android/intentresolver/v2/profiles/MultiProfilePagerAdapterTest.kt index 8e5f00ac..5b6b5d99 100644 --- a/tests/unit/src/com/android/intentresolver/v2/MultiProfilePagerAdapterTest.kt +++ b/tests/unit/src/com/android/intentresolver/v2/profiles/MultiProfilePagerAdapterTest.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 The Android Open Source Project + * 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.intentresolver.v2 +package com.android.intentresolver.v2.profiles import android.os.UserHandle import android.view.LayoutInflater @@ -28,7 +28,6 @@ import com.android.intentresolver.R import com.android.intentresolver.ResolverListAdapter import com.android.intentresolver.emptystate.EmptyStateProvider import com.android.intentresolver.mock -import com.android.intentresolver.v2.MultiProfilePagerAdapter.TabConfig import com.android.intentresolver.whenever import com.google.common.collect.ImmutableList import com.google.common.truth.Truth.assertThat diff --git a/tests/unit/src/com/android/intentresolver/v2/ui/viewmodel/ResolverRequestTest.kt b/tests/unit/src/com/android/intentresolver/v2/ui/viewmodel/ResolverRequestTest.kt index a5acb0d3..e88c46f5 100644 --- a/tests/unit/src/com/android/intentresolver/v2/ui/viewmodel/ResolverRequestTest.kt +++ b/tests/unit/src/com/android/intentresolver/v2/ui/viewmodel/ResolverRequestTest.kt @@ -22,7 +22,7 @@ import android.os.UserHandle import androidx.core.net.toUri import androidx.core.os.bundleOf import com.android.intentresolver.v2.ResolverActivity.PROFILE_WORK -import com.android.intentresolver.v2.domain.model.Profile.Type.WORK +import com.android.intentresolver.v2.shared.model.Profile.Type.WORK import com.android.intentresolver.v2.ui.model.ActivityLaunch import com.android.intentresolver.v2.ui.model.ResolverRequest import com.android.intentresolver.v2.validation.UncaughtException |