diff options
Diffstat (limited to 'java')
13 files changed, 1371 insertions, 508 deletions
diff --git a/java/src/com/android/intentresolver/AbstractMultiProfilePagerAdapter.java b/java/src/com/android/intentresolver/AbstractMultiProfilePagerAdapter.java index 0f0d1797..3e1084f4 100644 --- a/java/src/com/android/intentresolver/AbstractMultiProfilePagerAdapter.java +++ b/java/src/com/android/intentresolver/AbstractMultiProfilePagerAdapter.java @@ -17,18 +17,15 @@ package com.android.intentresolver; import android.annotation.IntDef; import android.annotation.Nullable; +import android.annotation.NonNull; +import android.annotation.UserIdInt; import android.app.AppGlobals; -import android.app.admin.DevicePolicyEventLogger; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.pm.IPackageManager; -import android.content.pm.ResolveInfo; -import android.os.AsyncTask; import android.os.Trace; import android.os.UserHandle; -import android.os.UserManager; -import android.stats.devicepolicy.DevicePolicyEnums; import android.view.View; import android.view.ViewGroup; import android.widget.Button; @@ -60,73 +57,32 @@ public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter { private final Context mContext; private int mCurrentPage; private OnProfileSelectedListener mOnProfileSelectedListener; - private OnSwitchOnWorkSelectedListener mOnSwitchOnWorkSelectedListener; + private Set<Integer> mLoadedPages; - private final UserHandle mPersonalProfileUserHandle; + private final EmptyStateProvider mEmptyStateProvider; private final UserHandle mWorkProfileUserHandle; - private Injector mInjector; - private boolean mIsWaitingToEnableWorkProfile; + private final QuietModeManager mQuietModeManager; AbstractMultiProfilePagerAdapter(Context context, int currentPage, - UserHandle personalProfileUserHandle, + EmptyStateProvider emptyStateProvider, + QuietModeManager quietModeManager, UserHandle workProfileUserHandle) { mContext = Objects.requireNonNull(context); mCurrentPage = currentPage; mLoadedPages = new HashSet<>(); - mPersonalProfileUserHandle = personalProfileUserHandle; mWorkProfileUserHandle = workProfileUserHandle; - UserManager userManager = context.getSystemService(UserManager.class); - mInjector = new Injector() { - @Override - public boolean hasCrossProfileIntents(List<Intent> intents, int sourceUserId, - int targetUserId) { - return AbstractMultiProfilePagerAdapter.this - .hasCrossProfileIntents(intents, sourceUserId, targetUserId); - } - - @Override - public boolean isQuietModeEnabled(UserHandle workProfileUserHandle) { - return userManager.isQuietModeEnabled(workProfileUserHandle); - } - - @Override - public void requestQuietModeEnabled(boolean enabled, UserHandle workProfileUserHandle) { - AsyncTask.THREAD_POOL_EXECUTOR.execute(() -> { - userManager.requestQuietModeEnabled(enabled, workProfileUserHandle); - }); - mIsWaitingToEnableWorkProfile = true; - } - }; - } - - protected void markWorkProfileEnabledBroadcastReceived() { - mIsWaitingToEnableWorkProfile = false; - } - - protected boolean isWaitingToEnableWorkProfile() { - return mIsWaitingToEnableWorkProfile; - } - - /** - * Overrides the default {@link Injector} for testing purposes. - */ - @VisibleForTesting - public void setInjector(Injector injector) { - mInjector = injector; + mEmptyStateProvider = emptyStateProvider; + mQuietModeManager = quietModeManager; } - protected boolean isQuietModeEnabled(UserHandle workProfileUserHandle) { - return mInjector.isQuietModeEnabled(workProfileUserHandle); + private boolean isQuietModeEnabled(UserHandle workProfileUserHandle) { + return mQuietModeManager.isQuietModeEnabled(workProfileUserHandle); } void setOnProfileSelectedListener(OnProfileSelectedListener listener) { mOnProfileSelectedListener = listener; } - void setOnSwitchOnWorkSelectedListener(OnSwitchOnWorkSelectedListener listener) { - mOnSwitchOnWorkSelectedListener = listener; - } - Context getContext() { return mContext; } @@ -280,8 +236,6 @@ public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter { abstract @Nullable ViewGroup getInactiveAdapterView(); - abstract String getMetricsCategory(); - /** * Rebuilds the tab that is currently visible to the user. * <p>Returns {@code true} if rebuild has completed. @@ -317,41 +271,18 @@ public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter { } private boolean rebuildTab(ResolverListAdapter activeListAdapter, boolean doPostProcessing) { - if (shouldShowNoCrossProfileIntentsEmptyState(activeListAdapter)) { + if (shouldSkipRebuild(activeListAdapter)) { activeListAdapter.postListReadyRunnable(doPostProcessing, /* rebuildCompleted */ true); return false; } return activeListAdapter.rebuildList(doPostProcessing); } - private boolean shouldShowNoCrossProfileIntentsEmptyState( - ResolverListAdapter activeListAdapter) { - UserHandle listUserHandle = activeListAdapter.getUserHandle(); - return UserHandle.myUserId() != listUserHandle.getIdentifier() - && allowShowNoCrossProfileIntentsEmptyState() - && !mInjector.hasCrossProfileIntents(activeListAdapter.getIntents(), - UserHandle.myUserId(), listUserHandle.getIdentifier()); + private boolean shouldSkipRebuild(ResolverListAdapter activeListAdapter) { + EmptyState emptyState = mEmptyStateProvider.getEmptyState(activeListAdapter); + return emptyState != null && emptyState.shouldSkipDataRebuild(); } - boolean allowShowNoCrossProfileIntentsEmptyState() { - return true; - } - - protected abstract void showWorkProfileOffEmptyState( - ResolverListAdapter activeListAdapter, View.OnClickListener listener); - - protected abstract void showNoPersonalToWorkIntentsEmptyState( - ResolverListAdapter activeListAdapter); - - protected abstract void showNoPersonalAppsAvailableEmptyState( - ResolverListAdapter activeListAdapter); - - protected abstract void showNoWorkAppsAvailableEmptyState( - ResolverListAdapter activeListAdapter); - - protected abstract void showNoWorkToPersonalIntentsEmptyState( - ResolverListAdapter activeListAdapter); - /** * The empty state screens are shown according to their priority: * <ol> @@ -366,103 +297,88 @@ public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter { * anyway. */ void showEmptyResolverListEmptyState(ResolverListAdapter listAdapter) { - if (maybeShowNoCrossProfileIntentsEmptyState(listAdapter)) { + final EmptyState emptyState = mEmptyStateProvider.getEmptyState(listAdapter); + + if (emptyState == null) { return; } - if (maybeShowWorkProfileOffEmptyState(listAdapter)) { - return; + + emptyState.onEmptyStateShown(); + + View.OnClickListener clickListener = null; + + if (emptyState.getButtonClickListener() != null) { + clickListener = v -> emptyState.getButtonClickListener().onClick(() -> { + ProfileDescriptor descriptor = getItem( + userHandleToPageIndex(listAdapter.getUserHandle())); + AbstractMultiProfilePagerAdapter.this.showSpinner(descriptor.getEmptyStateView()); + }); } - maybeShowNoAppsAvailableEmptyState(listAdapter); + + showEmptyState(listAdapter, emptyState, clickListener); } - private boolean maybeShowNoCrossProfileIntentsEmptyState(ResolverListAdapter listAdapter) { - if (!shouldShowNoCrossProfileIntentsEmptyState(listAdapter)) { - return false; - } - if (listAdapter.getUserHandle().equals(mPersonalProfileUserHandle)) { - DevicePolicyEventLogger.createEvent( - DevicePolicyEnums.RESOLVER_EMPTY_STATE_NO_SHARING_TO_PERSONAL) - .setStrings(getMetricsCategory()) - .write(); - showNoWorkToPersonalIntentsEmptyState(listAdapter); - } else { - DevicePolicyEventLogger.createEvent( - DevicePolicyEnums.RESOLVER_EMPTY_STATE_NO_SHARING_TO_WORK) - .setStrings(getMetricsCategory()) - .write(); - showNoPersonalToWorkIntentsEmptyState(listAdapter); + /** + * 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(); } - return true; } /** - * Returns {@code true} if the work profile off empty state screen is shown. + * Utility class to check if there are cross profile intents, it is in a separate class so + * it could be mocked in tests */ - private boolean maybeShowWorkProfileOffEmptyState(ResolverListAdapter listAdapter) { - UserHandle listUserHandle = listAdapter.getUserHandle(); - if (!listUserHandle.equals(mWorkProfileUserHandle) - || !mInjector.isQuietModeEnabled(mWorkProfileUserHandle) - || listAdapter.getCount() == 0) { - return false; - } - DevicePolicyEventLogger - .createEvent(DevicePolicyEnums.RESOLVER_EMPTY_STATE_WORK_APPS_DISABLED) - .setStrings(getMetricsCategory()) - .write(); - showWorkProfileOffEmptyState(listAdapter, - v -> { - ProfileDescriptor descriptor = getItem( - userHandleToPageIndex(listAdapter.getUserHandle())); - showSpinner(descriptor.getEmptyStateView()); - if (mOnSwitchOnWorkSelectedListener != null) { - mOnSwitchOnWorkSelectedListener.onSwitchOnWorkSelected(); - } - mInjector.requestQuietModeEnabled(false, mWorkProfileUserHandle); - }); - return true; - } - - private void maybeShowNoAppsAvailableEmptyState(ResolverListAdapter listAdapter) { - UserHandle listUserHandle = listAdapter.getUserHandle(); - if (mWorkProfileUserHandle != null - && (UserHandle.myUserId() == listUserHandle.getIdentifier() - || !hasAppsInOtherProfile(listAdapter))) { - DevicePolicyEventLogger.createEvent( - DevicePolicyEnums.RESOLVER_EMPTY_STATE_NO_APPS_RESOLVED) - .setStrings(getMetricsCategory()) - .setBoolean(/*isPersonalProfile*/ listUserHandle == mPersonalProfileUserHandle) - .write(); - if (listUserHandle == mPersonalProfileUserHandle) { - showNoPersonalAppsAvailableEmptyState(listAdapter); - } else { - showNoWorkAppsAvailableEmptyState(listAdapter); - } - } else if (mWorkProfileUserHandle == null) { - showConsumerUserNoAppsAvailableEmptyState(listAdapter); + public static class CrossProfileIntentsChecker { + + private final ContentResolver mContentResolver; + + public CrossProfileIntentsChecker(@NonNull ContentResolver contentResolver) { + mContentResolver = contentResolver; } - } - protected void showEmptyState(ResolverListAdapter activeListAdapter, String title, - String subtitle) { - showEmptyState(activeListAdapter, title, subtitle, /* buttonOnClick */ null); + /** + * Returns {@code true} if at least one of the provided {@code intents} can be forwarded + * from {@code source} (user id) to {@code target} (user id). + */ + public boolean hasCrossProfileIntents(List<Intent> intents, @UserIdInt int source, + @UserIdInt int target) { + IPackageManager packageManager = AppGlobals.getPackageManager(); + + return intents.stream().anyMatch(intent -> + null != IntentForwarderActivity.canForward(intent, source, target, + packageManager, mContentResolver)); + } } - protected void showEmptyState(ResolverListAdapter activeListAdapter, - String title, String subtitle, View.OnClickListener buttonOnClick) { + protected void showEmptyState(ResolverListAdapter activeListAdapter, EmptyState emptyState, + View.OnClickListener buttonOnClick) { ProfileDescriptor descriptor = getItem( userHandleToPageIndex(activeListAdapter.getUserHandle())); descriptor.rootView.findViewById(com.android.internal.R.id.resolver_list).setVisibility(View.GONE); ViewGroup emptyStateView = descriptor.getEmptyStateView(); - resetViewVisibilitiesForWorkProfileEmptyState(emptyStateView); + resetViewVisibilitiesForEmptyState(emptyStateView); emptyStateView.setVisibility(View.VISIBLE); View container = emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_container); setupContainerPadding(container); TextView titleView = emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_title); - titleView.setText(title); + String title = emptyState.getTitle(); + if (title != null) { + titleView.setVisibility(View.VISIBLE); + titleView.setText(title); + } else { + titleView.setVisibility(View.GONE); + } TextView subtitleView = emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_subtitle); + String subtitle = emptyState.getSubtitle(); if (subtitle != null) { subtitleView.setVisibility(View.VISIBLE); subtitleView.setText(subtitle); @@ -470,6 +386,9 @@ public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter { subtitleView.setVisibility(View.GONE); } + View defaultEmptyText = emptyStateView.findViewById(com.android.internal.R.id.empty); + defaultEmptyText.setVisibility(emptyState.useDefaultEmptyView() ? View.VISIBLE : View.GONE); + Button button = emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_button); button.setVisibility(buttonOnClick != null ? View.VISIBLE : View.GONE); button.setOnClickListener(buttonOnClick); @@ -483,22 +402,6 @@ public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter { */ protected void setupContainerPadding(View container) {} - private void showConsumerUserNoAppsAvailableEmptyState(ResolverListAdapter activeListAdapter) { - ProfileDescriptor descriptor = getItem( - userHandleToPageIndex(activeListAdapter.getUserHandle())); - descriptor.rootView.findViewById(com.android.internal.R.id.resolver_list).setVisibility(View.GONE); - View emptyStateView = descriptor.getEmptyStateView(); - resetViewVisibilitiesForConsumerUserEmptyState(emptyStateView); - emptyStateView.setVisibility(View.VISIBLE); - - activeListAdapter.markTabLoaded(); - } - - private boolean isSpinnerShowing(View emptyStateView) { - return emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_progress).getVisibility() - == View.VISIBLE; - } - private void showSpinner(View emptyStateView) { emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_title).setVisibility(View.INVISIBLE); emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_button).setVisibility(View.INVISIBLE); @@ -506,7 +409,7 @@ public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter { emptyStateView.findViewById(com.android.internal.R.id.empty).setVisibility(View.GONE); } - private void resetViewVisibilitiesForWorkProfileEmptyState(View emptyStateView) { + private void resetViewVisibilitiesForEmptyState(View emptyStateView) { emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_title).setVisibility(View.VISIBLE); emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_subtitle).setVisibility(View.VISIBLE); emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_button).setVisibility(View.INVISIBLE); @@ -514,14 +417,6 @@ public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter { emptyStateView.findViewById(com.android.internal.R.id.empty).setVisibility(View.GONE); } - private void resetViewVisibilitiesForConsumerUserEmptyState(View emptyStateView) { - emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_title).setVisibility(View.GONE); - emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_subtitle).setVisibility(View.GONE); - emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_button).setVisibility(View.GONE); - emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_progress).setVisibility(View.GONE); - emptyStateView.findViewById(com.android.internal.R.id.empty).setVisibility(View.VISIBLE); - } - protected void showListView(ResolverListAdapter activeListAdapter) { ProfileDescriptor descriptor = getItem( userHandleToPageIndex(activeListAdapter.getUserHandle())); @@ -530,33 +425,6 @@ public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter { emptyStateView.setVisibility(View.GONE); } - private boolean hasCrossProfileIntents(List<Intent> intents, int source, int target) { - IPackageManager packageManager = AppGlobals.getPackageManager(); - ContentResolver contentResolver = mContext.getContentResolver(); - for (Intent intent : intents) { - if (IntentForwarderActivity.canForward(intent, source, target, packageManager, - contentResolver) != null) { - return true; - } - } - return false; - } - - private boolean hasAppsInOtherProfile(ResolverListAdapter adapter) { - if (mWorkProfileUserHandle == null) { - return false; - } - List<ResolverActivity.ResolvedComponentInfo> resolversForIntent = - adapter.getResolversForUser(UserHandle.of(UserHandle.myUserId())); - for (ResolverActivity.ResolvedComponentInfo info : resolversForIntent) { - ResolveInfo resolveInfo = info.getResolveInfoAt(0); - if (resolveInfo.targetUserId != UserHandle.USER_CURRENT) { - return true; - } - } - return false; - } - boolean shouldShowEmptyStateScreen(ResolverListAdapter listAdapter) { int count = listAdapter.getUnfilteredCount(); return (count == 0 && listAdapter.getPlaceholderCount() == 0) @@ -600,6 +468,99 @@ public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter { } /** + * Returns an empty state to show for the current profile page (tab) if necessary. + * This could be used e.g. to show a blocker on a tab if device management policy doesn't + * allow to use it or there are no apps available. + */ + public interface EmptyStateProvider { + /** + * When a non-null empty state is returned the corresponding profile page will show + * this empty state + * @param resolverListAdapter the current adapter + */ + @Nullable + default EmptyState getEmptyState(ResolverListAdapter resolverListAdapter) { + return null; + } + } + + /** + * Empty state provider that combines multiple providers. Providers earlier in the list have + * priority, that is if there is a provider that returns non-null empty state then all further + * providers will be ignored. + */ + public static class CompositeEmptyStateProvider implements EmptyStateProvider { + + private final EmptyStateProvider[] mProviders; + + public CompositeEmptyStateProvider(EmptyStateProvider... providers) { + mProviders = providers; + } + + @Nullable + @Override + public EmptyState getEmptyState(ResolverListAdapter resolverListAdapter) { + for (EmptyStateProvider provider : mProviders) { + EmptyState emptyState = provider.getEmptyState(resolverListAdapter); + if (emptyState != null) { + return emptyState; + } + } + return null; + } + } + + /** + * Describes how the blocked empty state should look like for a profile tab + */ + public interface EmptyState { + /** + * Title that will be shown on the empty state + */ + @Nullable + default String getTitle() { return null; } + + /** + * Subtitle that will be shown underneath the title on the empty state + */ + @Nullable + default String getSubtitle() { return null; } + + /** + * If non-null then a button will be shown and this listener will be called + * when the button is clicked + */ + @Nullable + default ClickListener getButtonClickListener() { return null; } + + /** + * If true then default text ('No apps can perform this action') and style for the empty + * state will be applied, title and subtitle will be ignored. + */ + default boolean useDefaultEmptyView() { return false; } + + /** + * Returns true if for this empty state we should skip rebuilding of the apps list + * for this tab. + */ + default boolean shouldSkipDataRebuild() { return false; } + + /** + * Called when empty state is shown, could be used e.g. to track analytics events + */ + default void onEmptyStateShown() {} + + interface ClickListener { + void onClick(TabControl currentTab); + } + + interface TabControl { + void showSpinner(); + } + } + + + /** * Listener for when the user switches on the work profile from the work tab. */ interface OnSwitchOnWorkSelectedListener { @@ -612,14 +573,7 @@ public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter { /** * Describes an injector to be used for cross profile functionality. Overridable for testing. */ - @VisibleForTesting - public interface Injector { - /** - * Returns {@code true} if at least one of the provided {@code intents} can be forwarded - * from {@code sourceUserId} to {@code targetUserId}. - */ - boolean hasCrossProfileIntents(List<Intent> intents, int sourceUserId, int targetUserId); - + public interface QuietModeManager { /** * Returns whether the given profile is in quiet mode or not. */ @@ -629,5 +583,15 @@ public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter { * Enables or disables quiet mode for a managed profile. */ void requestQuietModeEnabled(boolean enabled, UserHandle workProfileUserHandle); + + /** + * Should be called when the work profile enabled broadcast received + */ + void markWorkProfileEnabledBroadcastReceived(); + + /** + * Returns true if enabling of work profile is in progress + */ + boolean isWaitingToEnableWorkProfile(); } } diff --git a/java/src/com/android/intentresolver/ChooserActivity.java b/java/src/com/android/intentresolver/ChooserActivity.java index d5a0c32c..776d34a9 100644 --- a/java/src/com/android/intentresolver/ChooserActivity.java +++ b/java/src/com/android/intentresolver/ChooserActivity.java @@ -16,6 +16,14 @@ package com.android.intentresolver; +import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_CANT_ACCESS_PERSONAL; +import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_CANT_ACCESS_WORK; +import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_CANT_SHARE_WITH_PERSONAL; +import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_CANT_SHARE_WITH_WORK; +import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_CROSS_PROFILE_BLOCKED_TITLE; +import static android.stats.devicepolicy.nano.DevicePolicyEnums.RESOLVER_EMPTY_STATE_NO_SHARING_TO_PERSONAL; +import static android.stats.devicepolicy.nano.DevicePolicyEnums.RESOLVER_EMPTY_STATE_NO_SHARING_TO_WORK; + import static com.android.internal.util.LatencyTracker.ACTION_LOAD_SHARE_SHEET; import android.animation.Animator; @@ -99,6 +107,9 @@ import androidx.recyclerview.widget.GridLayoutManager; import androidx.recyclerview.widget.RecyclerView; import androidx.viewpager.widget.ViewPager; +import com.android.intentresolver.AbstractMultiProfilePagerAdapter.EmptyState; +import com.android.intentresolver.AbstractMultiProfilePagerAdapter.EmptyStateProvider; +import com.android.intentresolver.NoCrossProfileEmptyStateProvider.DevicePolicyBlockerEmptyState; import com.android.intentresolver.ResolverListAdapter.ViewHolder; import com.android.intentresolver.chooser.DisplayResolveInfo; import com.android.intentresolver.chooser.MultiDisplayResolveInfo; @@ -594,6 +605,41 @@ public class ChooserActivity extends ResolverActivity implements return mChooserMultiProfilePagerAdapter; } + @Override + protected EmptyStateProvider createBlockerEmptyStateProvider() { + final boolean isSendAction = isSendAction(getTargetIntent()); + + final EmptyState noWorkToPersonalEmptyState = + new DevicePolicyBlockerEmptyState( + /* context= */ this, + /* devicePolicyStringTitleId= */ RESOLVER_CROSS_PROFILE_BLOCKED_TITLE, + /* defaultTitleResource= */ R.string.resolver_cross_profile_blocked, + /* devicePolicyStringSubtitleId= */ + isSendAction ? RESOLVER_CANT_SHARE_WITH_PERSONAL : RESOLVER_CANT_ACCESS_PERSONAL, + /* defaultSubtitleResource= */ + isSendAction ? R.string.resolver_cant_share_with_personal_apps_explanation + : R.string.resolver_cant_access_personal_apps_explanation, + /* devicePolicyEventId= */ RESOLVER_EMPTY_STATE_NO_SHARING_TO_PERSONAL, + /* devicePolicyEventCategory= */ ResolverActivity.METRICS_CATEGORY_CHOOSER); + + final EmptyState noPersonalToWorkEmptyState = + new DevicePolicyBlockerEmptyState( + /* context= */ this, + /* devicePolicyStringTitleId= */ RESOLVER_CROSS_PROFILE_BLOCKED_TITLE, + /* defaultTitleResource= */ R.string.resolver_cross_profile_blocked, + /* devicePolicyStringSubtitleId= */ + isSendAction ? RESOLVER_CANT_SHARE_WITH_WORK : RESOLVER_CANT_ACCESS_WORK, + /* defaultSubtitleResource= */ + isSendAction ? R.string.resolver_cant_share_with_work_apps_explanation + : R.string.resolver_cant_access_work_apps_explanation, + /* devicePolicyEventId= */ RESOLVER_EMPTY_STATE_NO_SHARING_TO_WORK, + /* devicePolicyEventCategory= */ ResolverActivity.METRICS_CATEGORY_CHOOSER); + + return new NoCrossProfileEmptyStateProvider(getPersonalProfileUserHandle(), + noWorkToPersonalEmptyState, noPersonalToWorkEmptyState, + createCrossProfileIntentsChecker(), createMyUserIdProvider()); + } + private ChooserMultiProfilePagerAdapter createChooserMultiProfilePagerAdapterForOneProfile( Intent[] initialIntents, List<ResolveInfo> rList, @@ -608,9 +654,10 @@ public class ChooserActivity extends ResolverActivity implements return new ChooserMultiProfilePagerAdapter( /* context */ this, adapter, - getPersonalProfileUserHandle(), + createEmptyStateProvider(/* workProfileUserHandle= */ null), + mQuietModeManager, /* workProfileUserHandle= */ null, - isSendAction(getTargetIntent()), mMaxTargetsPerRow); + mMaxTargetsPerRow); } private ChooserMultiProfilePagerAdapter createChooserMultiProfilePagerAdapterForTwoProfiles( @@ -636,10 +683,11 @@ public class ChooserActivity extends ResolverActivity implements /* context */ this, personalAdapter, workAdapter, + createEmptyStateProvider(/* workProfileUserHandle= */ getWorkProfileUserHandle()), + mQuietModeManager, selectedProfile, - getPersonalProfileUserHandle(), getWorkProfileUserHandle(), - isSendAction(getTargetIntent()), mMaxTargetsPerRow); + mMaxTargetsPerRow); } private int findSelectedProfile() { diff --git a/java/src/com/android/intentresolver/ChooserMultiProfilePagerAdapter.java b/java/src/com/android/intentresolver/ChooserMultiProfilePagerAdapter.java index 62c14866..d0463fff 100644 --- a/java/src/com/android/intentresolver/ChooserMultiProfilePagerAdapter.java +++ b/java/src/com/android/intentresolver/ChooserMultiProfilePagerAdapter.java @@ -16,17 +16,7 @@ package com.android.intentresolver; -import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_CANT_ACCESS_PERSONAL; -import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_CANT_ACCESS_WORK; -import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_CANT_SHARE_WITH_PERSONAL; -import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_CANT_SHARE_WITH_WORK; -import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_CROSS_PROFILE_BLOCKED_TITLE; -import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_NO_PERSONAL_APPS; -import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_NO_WORK_APPS; -import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_WORK_PAUSED_TITLE; - import android.annotation.Nullable; -import android.app.admin.DevicePolicyManager; import android.content.Context; import android.os.UserHandle; import android.view.LayoutInflater; @@ -47,37 +37,37 @@ public class ChooserMultiProfilePagerAdapter extends AbstractMultiProfilePagerAd private static final int SINGLE_CELL_SPAN_SIZE = 1; private final ChooserProfileDescriptor[] mItems; - private final boolean mIsSendAction; private int mBottomOffset; private int mMaxTargetsPerRow; ChooserMultiProfilePagerAdapter(Context context, ChooserActivity.ChooserGridAdapter adapter, - UserHandle personalProfileUserHandle, + EmptyStateProvider emptyStateProvider, + QuietModeManager quietModeManager, UserHandle workProfileUserHandle, - boolean isSendAction, int maxTargetsPerRow) { - super(context, /* currentPage */ 0, personalProfileUserHandle, workProfileUserHandle); + int maxTargetsPerRow) { + super(context, /* currentPage */ 0, emptyStateProvider, quietModeManager, + workProfileUserHandle); mItems = new ChooserProfileDescriptor[] { createProfileDescriptor(adapter) }; - mIsSendAction = isSendAction; mMaxTargetsPerRow = maxTargetsPerRow; } ChooserMultiProfilePagerAdapter(Context context, ChooserActivity.ChooserGridAdapter personalAdapter, ChooserActivity.ChooserGridAdapter workAdapter, + EmptyStateProvider emptyStateProvider, + QuietModeManager quietModeManager, @Profile int defaultProfile, - UserHandle personalProfileUserHandle, UserHandle workProfileUserHandle, - boolean isSendAction, int maxTargetsPerRow) { - super(context, /* currentPage */ defaultProfile, personalProfileUserHandle, - workProfileUserHandle); + int maxTargetsPerRow) { + super(context, /* currentPage */ defaultProfile, emptyStateProvider, + quietModeManager, workProfileUserHandle); mItems = new ChooserProfileDescriptor[] { createProfileDescriptor(personalAdapter), createProfileDescriptor(workAdapter) }; - mIsSendAction = isSendAction; mMaxTargetsPerRow = maxTargetsPerRow; } @@ -192,112 +182,6 @@ public class ChooserMultiProfilePagerAdapter extends AbstractMultiProfilePagerAd return getListViewForIndex(1 - getCurrentPage()); } - @Override - String getMetricsCategory() { - return ResolverActivity.METRICS_CATEGORY_CHOOSER; - } - - @Override - protected void showWorkProfileOffEmptyState(ResolverListAdapter activeListAdapter, - View.OnClickListener listener) { - showEmptyState(activeListAdapter, - getWorkAppPausedTitle(), - /* subtitle = */ null, - listener); - } - - @Override - protected void showNoPersonalToWorkIntentsEmptyState(ResolverListAdapter activeListAdapter) { - if (mIsSendAction) { - showEmptyState(activeListAdapter, - getCrossProfileBlockedTitle(), - getCantShareWithWorkMessage()); - } else { - showEmptyState(activeListAdapter, - getCrossProfileBlockedTitle(), - getCantAccessWorkMessage()); - } - } - - @Override - protected void showNoWorkToPersonalIntentsEmptyState(ResolverListAdapter activeListAdapter) { - if (mIsSendAction) { - showEmptyState(activeListAdapter, - getCrossProfileBlockedTitle(), - getCantShareWithPersonalMessage()); - } else { - showEmptyState(activeListAdapter, - getCrossProfileBlockedTitle(), - getCantAccessPersonalMessage()); - } - } - - @Override - protected void showNoPersonalAppsAvailableEmptyState(ResolverListAdapter listAdapter) { - showEmptyState(listAdapter, getNoPersonalAppsAvailableMessage(), /* subtitle= */ null); - - } - - @Override - protected void showNoWorkAppsAvailableEmptyState(ResolverListAdapter listAdapter) { - showEmptyState(listAdapter, getNoWorkAppsAvailableMessage(), /* subtitle = */ null); - } - - private String getWorkAppPausedTitle() { - return getContext().getSystemService(DevicePolicyManager.class).getResources().getString( - RESOLVER_WORK_PAUSED_TITLE, - () -> getContext().getString(R.string.resolver_turn_on_work_apps)); - } - - private String getCrossProfileBlockedTitle() { - return getContext().getSystemService(DevicePolicyManager.class).getResources().getString( - RESOLVER_CROSS_PROFILE_BLOCKED_TITLE, - () -> getContext().getString(R.string.resolver_cross_profile_blocked)); - } - - private String getCantShareWithWorkMessage() { - return getContext().getSystemService(DevicePolicyManager.class).getResources().getString( - RESOLVER_CANT_SHARE_WITH_WORK, - () -> getContext().getString( - R.string.resolver_cant_share_with_work_apps_explanation)); - } - - private String getCantShareWithPersonalMessage() { - return getContext().getSystemService(DevicePolicyManager.class).getResources().getString( - RESOLVER_CANT_SHARE_WITH_PERSONAL, - () -> getContext().getString( - R.string.resolver_cant_share_with_personal_apps_explanation)); - } - - private String getCantAccessWorkMessage() { - return getContext().getSystemService(DevicePolicyManager.class).getResources().getString( - RESOLVER_CANT_ACCESS_WORK, - () -> getContext().getString( - R.string.resolver_cant_access_work_apps_explanation)); - } - - private String getCantAccessPersonalMessage() { - return getContext().getSystemService(DevicePolicyManager.class).getResources().getString( - RESOLVER_CANT_ACCESS_PERSONAL, - () -> getContext().getString( - R.string.resolver_cant_access_personal_apps_explanation)); - } - - private String getNoWorkAppsAvailableMessage() { - return getContext().getSystemService(DevicePolicyManager.class).getResources().getString( - RESOLVER_NO_WORK_APPS, - () -> getContext().getString( - R.string.resolver_no_work_apps_available)); - } - - private String getNoPersonalAppsAvailableMessage() { - return getContext().getSystemService(DevicePolicyManager.class).getResources().getString( - RESOLVER_NO_PERSONAL_APPS, - () -> getContext().getString( - R.string.resolver_no_personal_apps_available)); - } - - void setEmptyStateBottomOffset(int bottomOffset) { mBottomOffset = bottomOffset; } diff --git a/java/src/com/android/intentresolver/NoAppsAvailableEmptyStateProvider.java b/java/src/com/android/intentresolver/NoAppsAvailableEmptyStateProvider.java new file mode 100644 index 00000000..5bf994d6 --- /dev/null +++ b/java/src/com/android/intentresolver/NoAppsAvailableEmptyStateProvider.java @@ -0,0 +1,154 @@ +/* + * Copyright (C) 2022 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; + +import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_NO_PERSONAL_APPS; +import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_NO_WORK_APPS; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.admin.DevicePolicyEventLogger; +import android.app.admin.DevicePolicyManager; +import android.content.Context; +import android.content.pm.ResolveInfo; +import android.os.UserHandle; +import android.stats.devicepolicy.nano.DevicePolicyEnums; + +import com.android.intentresolver.AbstractMultiProfilePagerAdapter.EmptyState; +import com.android.intentresolver.AbstractMultiProfilePagerAdapter.EmptyStateProvider; +import com.android.intentresolver.AbstractMultiProfilePagerAdapter.MyUserIdProvider; +import com.android.internal.R; + +import java.util.List; + +/** + * Chooser/ResolverActivity empty state provider that returns empty state which is shown when + * there are no apps available. + */ +public class NoAppsAvailableEmptyStateProvider implements EmptyStateProvider { + + @NonNull + private final Context mContext; + @Nullable + private final UserHandle mWorkProfileUserHandle; + @Nullable + private final UserHandle mPersonalProfileUserHandle; + @NonNull + private final String mMetricsCategory; + @NonNull + private final MyUserIdProvider mMyUserIdProvider; + + public NoAppsAvailableEmptyStateProvider(Context context, UserHandle workProfileUserHandle, + UserHandle personalProfileUserHandle, String metricsCategory, + MyUserIdProvider myUserIdProvider) { + mContext = context; + mWorkProfileUserHandle = workProfileUserHandle; + mPersonalProfileUserHandle = personalProfileUserHandle; + mMetricsCategory = metricsCategory; + mMyUserIdProvider = myUserIdProvider; + } + + @Nullable + @Override + @SuppressWarnings("ReferenceEquality") + public EmptyState getEmptyState(ResolverListAdapter resolverListAdapter) { + UserHandle listUserHandle = resolverListAdapter.getUserHandle(); + + if (mWorkProfileUserHandle != null + && (mMyUserIdProvider.getMyUserId() == listUserHandle.getIdentifier() + || !hasAppsInOtherProfile(resolverListAdapter))) { + + String title; + if (listUserHandle == mPersonalProfileUserHandle) { + title = mContext.getSystemService( + DevicePolicyManager.class).getResources().getString( + RESOLVER_NO_PERSONAL_APPS, + () -> mContext.getString(R.string.resolver_no_personal_apps_available)); + } else { + title = mContext.getSystemService( + DevicePolicyManager.class).getResources().getString( + RESOLVER_NO_WORK_APPS, + () -> mContext.getString(R.string.resolver_no_work_apps_available)); + } + + return new NoAppsAvailableEmptyState( + title, mMetricsCategory, + /* isPersonalProfile= */ listUserHandle == mPersonalProfileUserHandle + ); + } else if (mWorkProfileUserHandle == null) { + // Return default empty state without tracking + return new DefaultEmptyState(); + } + + return null; + } + + private boolean hasAppsInOtherProfile(ResolverListAdapter adapter) { + if (mWorkProfileUserHandle == null) { + return false; + } + List<ResolverActivity.ResolvedComponentInfo> resolversForIntent = + adapter.getResolversForUser(UserHandle.of(mMyUserIdProvider.getMyUserId())); + for (ResolverActivity.ResolvedComponentInfo info : resolversForIntent) { + ResolveInfo resolveInfo = info.getResolveInfoAt(0); + if (resolveInfo.targetUserId != UserHandle.USER_CURRENT) { + return true; + } + } + return false; + } + + public static class DefaultEmptyState implements EmptyState { + @Override + public boolean useDefaultEmptyView() { + return true; + } + } + + public static class NoAppsAvailableEmptyState implements EmptyState { + + @NonNull + private String mTitle; + + @NonNull + private String mMetricsCategory; + + private boolean mIsPersonalProfile; + + public NoAppsAvailableEmptyState(String title, String metricsCategory, + boolean isPersonalProfile) { + mTitle = title; + mMetricsCategory = metricsCategory; + mIsPersonalProfile = isPersonalProfile; + } + + @Nullable + @Override + public String getTitle() { + return mTitle; + } + + @Override + public void onEmptyStateShown() { + DevicePolicyEventLogger.createEvent( + DevicePolicyEnums.RESOLVER_EMPTY_STATE_NO_APPS_RESOLVED) + .setStrings(mMetricsCategory) + .setBoolean(/*isPersonalProfile*/ mIsPersonalProfile) + .write(); + } + } +}
\ No newline at end of file diff --git a/java/src/com/android/intentresolver/NoCrossProfileEmptyStateProvider.java b/java/src/com/android/intentresolver/NoCrossProfileEmptyStateProvider.java new file mode 100644 index 00000000..420d26c5 --- /dev/null +++ b/java/src/com/android/intentresolver/NoCrossProfileEmptyStateProvider.java @@ -0,0 +1,137 @@ +/* + * Copyright (C) 2022 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; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.StringRes; +import android.app.admin.DevicePolicyEventLogger; +import android.app.admin.DevicePolicyManager; +import android.content.Context; +import android.os.UserHandle; + +import com.android.intentresolver.AbstractMultiProfilePagerAdapter.CrossProfileIntentsChecker; +import com.android.intentresolver.AbstractMultiProfilePagerAdapter.EmptyState; +import com.android.intentresolver.AbstractMultiProfilePagerAdapter.EmptyStateProvider; +import com.android.intentresolver.AbstractMultiProfilePagerAdapter.MyUserIdProvider; + +/** + * Empty state provider that does not allow cross profile sharing, it will return a blocker + * in case if the profile of the current tab is not the same as the profile of the calling app. + */ +public class NoCrossProfileEmptyStateProvider implements EmptyStateProvider { + + private final UserHandle mPersonalProfileUserHandle; + private final EmptyState mNoWorkToPersonalEmptyState; + private final EmptyState mNoPersonalToWorkEmptyState; + private final CrossProfileIntentsChecker mCrossProfileIntentsChecker; + private final MyUserIdProvider mUserIdProvider; + + public NoCrossProfileEmptyStateProvider(UserHandle personalUserHandle, + EmptyState noWorkToPersonalEmptyState, + EmptyState noPersonalToWorkEmptyState, + CrossProfileIntentsChecker crossProfileIntentsChecker, + MyUserIdProvider myUserIdProvider) { + mPersonalProfileUserHandle = personalUserHandle; + mNoWorkToPersonalEmptyState = noWorkToPersonalEmptyState; + mNoPersonalToWorkEmptyState = noPersonalToWorkEmptyState; + mCrossProfileIntentsChecker = crossProfileIntentsChecker; + mUserIdProvider = myUserIdProvider; + } + + @Nullable + @Override + public EmptyState getEmptyState(ResolverListAdapter resolverListAdapter) { + boolean shouldShowBlocker = + mUserIdProvider.getMyUserId() != resolverListAdapter.getUserHandle().getIdentifier() + && !mCrossProfileIntentsChecker + .hasCrossProfileIntents(resolverListAdapter.getIntents(), + mUserIdProvider.getMyUserId(), + resolverListAdapter.getUserHandle().getIdentifier()); + + if (!shouldShowBlocker) { + return null; + } + + if (resolverListAdapter.getUserHandle().equals(mPersonalProfileUserHandle)) { + return mNoWorkToPersonalEmptyState; + } else { + return mNoPersonalToWorkEmptyState; + } + } + + + /** + * Empty state that gets strings from the device policy manager and tracks events into + * event logger of the device policy events. + */ + public static class DevicePolicyBlockerEmptyState implements EmptyState { + + @NonNull + private final Context mContext; + private final String mDevicePolicyStringTitleId; + @StringRes + private final int mDefaultTitleResource; + private final String mDevicePolicyStringSubtitleId; + @StringRes + private final int mDefaultSubtitleResource; + private final int mEventId; + @NonNull + private final String mEventCategory; + + public DevicePolicyBlockerEmptyState(Context context, String devicePolicyStringTitleId, + @StringRes int defaultTitleResource, String devicePolicyStringSubtitleId, + @StringRes int defaultSubtitleResource, + int devicePolicyEventId, String devicePolicyEventCategory) { + mContext = context; + mDevicePolicyStringTitleId = devicePolicyStringTitleId; + mDefaultTitleResource = defaultTitleResource; + mDevicePolicyStringSubtitleId = devicePolicyStringSubtitleId; + mDefaultSubtitleResource = defaultSubtitleResource; + mEventId = devicePolicyEventId; + mEventCategory = devicePolicyEventCategory; + } + + @Nullable + @Override + public String getTitle() { + return mContext.getSystemService(DevicePolicyManager.class).getResources().getString( + mDevicePolicyStringTitleId, + () -> mContext.getString(mDefaultTitleResource)); + } + + @Nullable + @Override + public String getSubtitle() { + return mContext.getSystemService(DevicePolicyManager.class).getResources().getString( + mDevicePolicyStringSubtitleId, + () -> mContext.getString(mDefaultSubtitleResource)); + } + + @Override + public void onEmptyStateShown() { + DevicePolicyEventLogger.createEvent(mEventId) + .setStrings(mEventCategory) + .write(); + } + + @Override + public boolean shouldSkipDataRebuild() { + return true; + } + } +} diff --git a/java/src/com/android/intentresolver/ResolverActivity.java b/java/src/com/android/intentresolver/ResolverActivity.java index 46a41b50..fece8d3d 100644 --- a/java/src/com/android/intentresolver/ResolverActivity.java +++ b/java/src/com/android/intentresolver/ResolverActivity.java @@ -19,6 +19,9 @@ package com.android.intentresolver; import static android.Manifest.permission.INTERACT_ACROSS_PROFILES; import static android.app.admin.DevicePolicyResources.Strings.Core.FORWARD_INTENT_TO_PERSONAL; import static android.app.admin.DevicePolicyResources.Strings.Core.FORWARD_INTENT_TO_WORK; +import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_CANT_ACCESS_PERSONAL; +import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_CANT_ACCESS_WORK; +import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_CROSS_PROFILE_BLOCKED_TITLE; import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_PERSONAL_TAB; import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_PERSONAL_TAB_ACCESSIBILITY; import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_WORK_PROFILE_NOT_SUPPORTED; @@ -26,6 +29,8 @@ import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_WORK import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_WORK_TAB_ACCESSIBILITY; import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; import static android.content.PermissionChecker.PID_UNKNOWN; +import static android.stats.devicepolicy.nano.DevicePolicyEnums.RESOLVER_EMPTY_STATE_NO_SHARING_TO_PERSONAL; +import static android.stats.devicepolicy.nano.DevicePolicyEnums.RESOLVER_EMPTY_STATE_NO_SHARING_TO_WORK; import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS; import android.annotation.Nullable; @@ -56,6 +61,7 @@ import android.content.res.TypedArray; import android.graphics.Insets; import android.graphics.drawable.Drawable; import android.net.Uri; +import android.os.AsyncTask; import android.os.Build; import android.os.Bundle; import android.os.PatternMatcher; @@ -93,7 +99,15 @@ import android.widget.Toast; import androidx.fragment.app.FragmentActivity; import androidx.viewpager.widget.ViewPager; +import com.android.intentresolver.AbstractMultiProfilePagerAdapter.CompositeEmptyStateProvider; +import com.android.intentresolver.AbstractMultiProfilePagerAdapter.CrossProfileIntentsChecker; +import com.android.intentresolver.AbstractMultiProfilePagerAdapter.EmptyStateProvider; +import com.android.intentresolver.AbstractMultiProfilePagerAdapter.MyUserIdProvider; +import com.android.intentresolver.AbstractMultiProfilePagerAdapter.OnSwitchOnWorkSelectedListener; import com.android.intentresolver.AbstractMultiProfilePagerAdapter.Profile; +import com.android.intentresolver.AbstractMultiProfilePagerAdapter.QuietModeManager; +import com.android.intentresolver.NoCrossProfileEmptyStateProvider.DevicePolicyBlockerEmptyState; +import com.android.intentresolver.chooser.ChooserTargetInfo; import com.android.intentresolver.chooser.DisplayResolveInfo; import com.android.intentresolver.chooser.TargetInfo; import com.android.intentresolver.widget.ResolverDrawerLayout; @@ -178,6 +192,8 @@ public class ResolverActivity extends FragmentActivity implements @VisibleForTesting protected AbstractMultiProfilePagerAdapter mMultiProfilePagerAdapter; + protected QuietModeManager mQuietModeManager; + // Intent extra for connected audio devices public static final String EXTRA_IS_AUDIO_CAPTURE_DEVICE = "is_audio_capture_device"; @@ -209,6 +225,9 @@ public class ResolverActivity extends FragmentActivity implements private UserHandle mWorkProfileUserHandle; + @Nullable + private OnSwitchOnWorkSelectedListener mOnSwitchOnWorkSelectedListener; + protected final LatencyTracker mLatencyTracker = getLatencyTracker(); private LatencyTracker getLatencyTracker() { @@ -366,6 +385,8 @@ public class ResolverActivity extends FragmentActivity implements setTheme(appliedThemeResId()); super.onCreate(savedInstanceState); + mQuietModeManager = createQuietModeManager(); + // Determine whether we should show that intent is forwarded // from managed profile to owner or other way around. setProfileSwitchMessage(intent.getContentUserHint()); @@ -466,6 +487,111 @@ public class ResolverActivity extends FragmentActivity implements return resolverMultiProfilePagerAdapter; } + @VisibleForTesting + protected MyUserIdProvider createMyUserIdProvider() { + return new MyUserIdProvider(); + } + + @VisibleForTesting + protected CrossProfileIntentsChecker createCrossProfileIntentsChecker() { + return new CrossProfileIntentsChecker(getContentResolver()); + } + + @VisibleForTesting + protected QuietModeManager createQuietModeManager() { + UserManager userManager = getSystemService(UserManager.class); + return new QuietModeManager() { + + private boolean mIsWaitingToEnableWorkProfile = false; + + @Override + public boolean isQuietModeEnabled(UserHandle workProfileUserHandle) { + return userManager.isQuietModeEnabled(workProfileUserHandle); + } + + @Override + public void requestQuietModeEnabled(boolean enabled, UserHandle workProfileUserHandle) { + AsyncTask.THREAD_POOL_EXECUTOR.execute(() -> { + userManager.requestQuietModeEnabled(enabled, workProfileUserHandle); + }); + mIsWaitingToEnableWorkProfile = true; + } + + @Override + public void markWorkProfileEnabledBroadcastReceived() { + mIsWaitingToEnableWorkProfile = false; + } + + @Override + public boolean isWaitingToEnableWorkProfile() { + return mIsWaitingToEnableWorkProfile; + } + }; + } + + protected EmptyStateProvider createBlockerEmptyStateProvider() { + final boolean shouldShowNoCrossProfileIntentsEmptyState = getUser().equals(getIntentUser()); + + if (!shouldShowNoCrossProfileIntentsEmptyState) { + // Implementation that doesn't show any blockers + return new EmptyStateProvider() {}; + } + + final AbstractMultiProfilePagerAdapter.EmptyState + noWorkToPersonalEmptyState = + new DevicePolicyBlockerEmptyState(/* context= */ this, + /* devicePolicyStringTitleId= */ RESOLVER_CROSS_PROFILE_BLOCKED_TITLE, + /* defaultTitleResource= */ R.string.resolver_cross_profile_blocked, + /* devicePolicyStringSubtitleId= */ RESOLVER_CANT_ACCESS_PERSONAL, + /* defaultSubtitleResource= */ + R.string.resolver_cant_access_personal_apps_explanation, + /* devicePolicyEventId= */ RESOLVER_EMPTY_STATE_NO_SHARING_TO_PERSONAL, + /* devicePolicyEventCategory= */ ResolverActivity.METRICS_CATEGORY_RESOLVER); + + final AbstractMultiProfilePagerAdapter.EmptyState noPersonalToWorkEmptyState = + new DevicePolicyBlockerEmptyState(/* context= */ this, + /* devicePolicyStringTitleId= */ RESOLVER_CROSS_PROFILE_BLOCKED_TITLE, + /* defaultTitleResource= */ R.string.resolver_cross_profile_blocked, + /* devicePolicyStringSubtitleId= */ RESOLVER_CANT_ACCESS_WORK, + /* defaultSubtitleResource= */ + R.string.resolver_cant_access_work_apps_explanation, + /* devicePolicyEventId= */ RESOLVER_EMPTY_STATE_NO_SHARING_TO_WORK, + /* devicePolicyEventCategory= */ ResolverActivity.METRICS_CATEGORY_RESOLVER); + + return new NoCrossProfileEmptyStateProvider(getPersonalProfileUserHandle(), + noWorkToPersonalEmptyState, noPersonalToWorkEmptyState, + createCrossProfileIntentsChecker(), createMyUserIdProvider()); + } + + protected EmptyStateProvider createEmptyStateProvider( + @Nullable UserHandle workProfileUserHandle) { + final EmptyStateProvider blockerEmptyStateProvider = createBlockerEmptyStateProvider(); + + final EmptyStateProvider workProfileOffEmptyStateProvider = + new WorkProfilePausedEmptyStateProvider(this, workProfileUserHandle, + mQuietModeManager, + /* onSwitchOnWorkSelectedListener= */ + () -> { if (mOnSwitchOnWorkSelectedListener != null) { + mOnSwitchOnWorkSelectedListener.onSwitchOnWorkSelected(); + }}, + getMetricsCategory()); + + final EmptyStateProvider noAppsEmptyStateProvider = new NoAppsAvailableEmptyStateProvider( + this, + workProfileUserHandle, + getPersonalProfileUserHandle(), + getMetricsCategory(), + createMyUserIdProvider() + ); + + // Return composite provider, the order matters (the higher, the more priority) + return new CompositeEmptyStateProvider( + blockerEmptyStateProvider, + workProfileOffEmptyStateProvider, + noAppsEmptyStateProvider + ); + } + private ResolverMultiProfilePagerAdapter createResolverMultiProfilePagerAdapterForOneProfile( Intent[] initialIntents, List<ResolveInfo> rList, boolean filterLastUsed) { @@ -476,13 +602,21 @@ public class ResolverActivity extends FragmentActivity implements rList, filterLastUsed, /* userHandle */ UserHandle.of(UserHandle.myUserId())); + QuietModeManager quietModeManager = createQuietModeManager(); return new ResolverMultiProfilePagerAdapter( /* context */ this, adapter, - getPersonalProfileUserHandle(), + createEmptyStateProvider(/* workProfileUserHandle= */ null), + quietModeManager, /* workProfileUserHandle= */ null); } + private UserHandle getIntentUser() { + return getIntent().hasExtra(EXTRA_CALLING_USER) + ? getIntent().getParcelableExtra(EXTRA_CALLING_USER) + : getUser(); + } + private ResolverMultiProfilePagerAdapter createResolverMultiProfilePagerAdapterForTwoProfiles( Intent[] initialIntents, List<ResolveInfo> rList, @@ -491,9 +625,7 @@ public class ResolverActivity extends FragmentActivity implements // the intent resolver is started in the other profile. Since this is the only case when // this happens, we check for it here and set the current profile's tab. int selectedProfile = getCurrentProfile(); - UserHandle intentUser = getIntent().hasExtra(EXTRA_CALLING_USER) - ? getIntent().getParcelableExtra(EXTRA_CALLING_USER) - : getUser(); + UserHandle intentUser = getIntentUser(); if (!getUser().equals(intentUser)) { if (getPersonalProfileUserHandle().equals(intentUser)) { selectedProfile = PROFILE_PERSONAL; @@ -526,14 +658,15 @@ public class ResolverActivity extends FragmentActivity implements (filterLastUsed && UserHandle.myUserId() == workProfileUserHandle.getIdentifier()), /* userHandle */ workProfileUserHandle); + QuietModeManager quietModeManager = createQuietModeManager(); return new ResolverMultiProfilePagerAdapter( /* context */ this, personalAdapter, workAdapter, + createEmptyStateProvider(getWorkProfileUserHandle()), + quietModeManager, selectedProfile, - getPersonalProfileUserHandle(), - getWorkProfileUserHandle(), - /* shouldShowNoCrossProfileIntentsEmptyState= */ getUser().equals(intentUser)); + getWorkProfileUserHandle()); } protected int appliedThemeResId() { @@ -840,9 +973,9 @@ public class ResolverActivity extends FragmentActivity implements } mRegistered = true; } - if (shouldShowTabs() && mMultiProfilePagerAdapter.isWaitingToEnableWorkProfile()) { - if (mMultiProfilePagerAdapter.isQuietModeEnabled(getWorkProfileUserHandle())) { - mMultiProfilePagerAdapter.markWorkProfileEnabledBroadcastReceived(); + if (shouldShowTabs() && mQuietModeManager.isWaitingToEnableWorkProfile()) { + if (mQuietModeManager.isQuietModeEnabled(getWorkProfileUserHandle())) { + mQuietModeManager.markWorkProfileEnabledBroadcastReceived(); } } mMultiProfilePagerAdapter.getActiveListAdapter().handlePackagesChanged(); @@ -1799,13 +1932,12 @@ public class ResolverActivity extends FragmentActivity implements onHorizontalSwipeStateChanged(state); } }); - mMultiProfilePagerAdapter.setOnSwitchOnWorkSelectedListener( - () -> { - final View workTab = tabHost.getTabWidget().getChildAt(1); - workTab.setFocusable(true); - workTab.setFocusableInTouchMode(true); - workTab.requestFocus(); - }); + mOnSwitchOnWorkSelectedListener = () -> { + final View workTab = tabHost.getTabWidget().getChildAt(1); + workTab.setFocusable(true); + workTab.setFocusableInTouchMode(true); + workTab.requestFocus(); + }; } private String getPersonalTabLabel() { @@ -2066,7 +2198,7 @@ public class ResolverActivity extends FragmentActivity implements public void onHandlePackagesChanged(ResolverListAdapter listAdapter) { if (listAdapter == mMultiProfilePagerAdapter.getActiveListAdapter()) { if (listAdapter.getUserHandle().equals(getWorkProfileUserHandle()) - && mMultiProfilePagerAdapter.isWaitingToEnableWorkProfile()) { + && mQuietModeManager.isWaitingToEnableWorkProfile()) { // We have just turned on the work profile and entered the pass code to start it, // now we are waiting to receive the ACTION_USER_UNLOCKED broadcast. There is no // point in reloading the list now, since the work profile user is still @@ -2118,7 +2250,7 @@ public class ResolverActivity extends FragmentActivity implements } mWorkProfileHasBeenEnabled = true; - mMultiProfilePagerAdapter.markWorkProfileEnabledBroadcastReceived(); + mQuietModeManager.markWorkProfileEnabledBroadcastReceived(); } else { // Must be an UNAVAILABLE broadcast, so we watch for the next availability mWorkProfileHasBeenEnabled = false; @@ -2134,7 +2266,6 @@ public class ResolverActivity extends FragmentActivity implements }; } - @VisibleForTesting public static final class ResolvedComponentInfo { public final ComponentName name; private final List<Intent> mIntents = new ArrayList<>(); diff --git a/java/src/com/android/intentresolver/ResolverListAdapter.java b/java/src/com/android/intentresolver/ResolverListAdapter.java index f74c33c0..d97191c6 100644 --- a/java/src/com/android/intentresolver/ResolverListAdapter.java +++ b/java/src/com/android/intentresolver/ResolverListAdapter.java @@ -764,7 +764,6 @@ public class ResolverListAdapter extends BaseAdapter { } } - @VisibleForTesting public UserHandle getUserHandle() { return mResolverListController.getUserHandle(); } diff --git a/java/src/com/android/intentresolver/ResolverMultiProfilePagerAdapter.java b/java/src/com/android/intentresolver/ResolverMultiProfilePagerAdapter.java index 7cd38a7e..8cf65529 100644 --- a/java/src/com/android/intentresolver/ResolverMultiProfilePagerAdapter.java +++ b/java/src/com/android/intentresolver/ResolverMultiProfilePagerAdapter.java @@ -16,15 +16,7 @@ package com.android.intentresolver; -import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_CANT_ACCESS_PERSONAL; -import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_CANT_ACCESS_WORK; -import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_CROSS_PROFILE_BLOCKED_TITLE; -import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_NO_PERSONAL_APPS; -import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_NO_WORK_APPS; -import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_WORK_PAUSED_TITLE; - import android.annotation.Nullable; -import android.app.admin.DevicePolicyManager; import android.content.Context; import android.os.UserHandle; import android.view.LayoutInflater; @@ -43,34 +35,33 @@ import com.android.internal.annotations.VisibleForTesting; public class ResolverMultiProfilePagerAdapter extends AbstractMultiProfilePagerAdapter { private final ResolverProfileDescriptor[] mItems; - private final boolean mShouldShowNoCrossProfileIntentsEmptyState; private boolean mUseLayoutWithDefault; ResolverMultiProfilePagerAdapter(Context context, ResolverListAdapter adapter, - UserHandle personalProfileUserHandle, + EmptyStateProvider emptyStateProvider, + QuietModeManager quietModeManager, UserHandle workProfileUserHandle) { - super(context, /* currentPage */ 0, personalProfileUserHandle, workProfileUserHandle); + super(context, /* currentPage */ 0, emptyStateProvider, quietModeManager, + workProfileUserHandle); mItems = new ResolverProfileDescriptor[] { createProfileDescriptor(adapter) }; - mShouldShowNoCrossProfileIntentsEmptyState = true; } ResolverMultiProfilePagerAdapter(Context context, ResolverListAdapter personalAdapter, ResolverListAdapter workAdapter, + EmptyStateProvider emptyStateProvider, + QuietModeManager quietModeManager, @Profile int defaultProfile, - UserHandle personalProfileUserHandle, - UserHandle workProfileUserHandle, - boolean shouldShowNoCrossProfileIntentsEmptyState) { - super(context, /* currentPage */ defaultProfile, personalProfileUserHandle, + UserHandle workProfileUserHandle) { + super(context, /* currentPage */ defaultProfile, emptyStateProvider, quietModeManager, workProfileUserHandle); mItems = new ResolverProfileDescriptor[] { createProfileDescriptor(personalAdapter), createProfileDescriptor(workAdapter) }; - mShouldShowNoCrossProfileIntentsEmptyState = shouldShowNoCrossProfileIntentsEmptyState; } private ResolverProfileDescriptor createProfileDescriptor( @@ -170,93 +161,6 @@ public class ResolverMultiProfilePagerAdapter extends AbstractMultiProfilePagerA return getListViewForIndex(1 - getCurrentPage()); } - @Override - String getMetricsCategory() { - return ResolverActivity.METRICS_CATEGORY_RESOLVER; - } - - @Override - boolean allowShowNoCrossProfileIntentsEmptyState() { - return mShouldShowNoCrossProfileIntentsEmptyState; - } - - @Override - protected void showWorkProfileOffEmptyState(ResolverListAdapter activeListAdapter, - View.OnClickListener listener) { - showEmptyState(activeListAdapter, - getWorkAppPausedTitle(), - /* subtitle = */ null, - listener); - } - - @Override - protected void showNoPersonalToWorkIntentsEmptyState(ResolverListAdapter activeListAdapter) { - showEmptyState(activeListAdapter, - getCrossProfileBlockedTitle(), - getCantAccessWorkMessage()); - } - - @Override - protected void showNoWorkToPersonalIntentsEmptyState(ResolverListAdapter activeListAdapter) { - showEmptyState(activeListAdapter, - getCrossProfileBlockedTitle(), - getCantAccessPersonalMessage()); - } - - @Override - protected void showNoPersonalAppsAvailableEmptyState(ResolverListAdapter listAdapter) { - showEmptyState(listAdapter, - getNoPersonalAppsAvailableMessage(), - /* subtitle = */ null); - } - - @Override - protected void showNoWorkAppsAvailableEmptyState(ResolverListAdapter listAdapter) { - showEmptyState(listAdapter, - getNoWorkAppsAvailableMessage(), - /* subtitle= */ null); - } - - private String getWorkAppPausedTitle() { - return getContext().getSystemService(DevicePolicyManager.class).getResources().getString( - RESOLVER_WORK_PAUSED_TITLE, - () -> getContext().getString(R.string.resolver_turn_on_work_apps)); - } - - private String getCrossProfileBlockedTitle() { - return getContext().getSystemService(DevicePolicyManager.class).getResources().getString( - RESOLVER_CROSS_PROFILE_BLOCKED_TITLE, - () -> getContext().getString(R.string.resolver_cross_profile_blocked)); - } - - private String getCantAccessWorkMessage() { - return getContext().getSystemService(DevicePolicyManager.class).getResources().getString( - RESOLVER_CANT_ACCESS_WORK, - () -> getContext().getString( - R.string.resolver_cant_access_work_apps_explanation)); - } - - private String getCantAccessPersonalMessage() { - return getContext().getSystemService(DevicePolicyManager.class).getResources().getString( - RESOLVER_CANT_ACCESS_PERSONAL, - () -> getContext().getString( - R.string.resolver_cant_access_personal_apps_explanation)); - } - - private String getNoWorkAppsAvailableMessage() { - return getContext().getSystemService(DevicePolicyManager.class).getResources().getString( - RESOLVER_NO_WORK_APPS, - () -> getContext().getString( - R.string.resolver_no_work_apps_available)); - } - - private String getNoPersonalAppsAvailableMessage() { - return getContext().getSystemService(DevicePolicyManager.class).getResources().getString( - RESOLVER_NO_PERSONAL_APPS, - () -> getContext().getString( - R.string.resolver_no_personal_apps_available)); - } - void setUseLayoutWithDefault(boolean useLayoutWithDefault) { mUseLayoutWithDefault = useLayoutWithDefault; } diff --git a/java/src/com/android/intentresolver/WorkProfilePausedEmptyStateProvider.java b/java/src/com/android/intentresolver/WorkProfilePausedEmptyStateProvider.java new file mode 100644 index 00000000..b7c89907 --- /dev/null +++ b/java/src/com/android/intentresolver/WorkProfilePausedEmptyStateProvider.java @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2022 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; + +import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_WORK_PAUSED_TITLE; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.admin.DevicePolicyEventLogger; +import android.app.admin.DevicePolicyManager; +import android.content.Context; +import android.os.UserHandle; +import android.stats.devicepolicy.nano.DevicePolicyEnums; + +import com.android.internal.R; +import com.android.intentresolver.AbstractMultiProfilePagerAdapter.EmptyState; +import com.android.intentresolver.AbstractMultiProfilePagerAdapter.EmptyStateProvider; +import com.android.intentresolver.AbstractMultiProfilePagerAdapter.OnSwitchOnWorkSelectedListener; +import com.android.intentresolver.AbstractMultiProfilePagerAdapter.QuietModeManager; + +/** + * Chooser/ResolverActivity empty state provider that returns empty state which is shown when + * work profile is paused and we need to show a button to enable it. + */ +public class WorkProfilePausedEmptyStateProvider implements EmptyStateProvider { + + private final UserHandle mWorkProfileUserHandle; + private final QuietModeManager mQuietModeManager; + private final String mMetricsCategory; + private final OnSwitchOnWorkSelectedListener mOnSwitchOnWorkSelectedListener; + private final Context mContext; + + public WorkProfilePausedEmptyStateProvider(@NonNull Context context, + @Nullable UserHandle workProfileUserHandle, + @NonNull QuietModeManager quietModeManager, + @Nullable OnSwitchOnWorkSelectedListener onSwitchOnWorkSelectedListener, + @NonNull String metricsCategory) { + mContext = context; + mWorkProfileUserHandle = workProfileUserHandle; + mQuietModeManager = quietModeManager; + mMetricsCategory = metricsCategory; + mOnSwitchOnWorkSelectedListener = onSwitchOnWorkSelectedListener; + } + + @Nullable + @Override + public EmptyState getEmptyState(ResolverListAdapter resolverListAdapter) { + if (!resolverListAdapter.getUserHandle().equals(mWorkProfileUserHandle) + || !mQuietModeManager.isQuietModeEnabled(mWorkProfileUserHandle) + || resolverListAdapter.getCount() == 0) { + return null; + } + + final String title = mContext.getSystemService(DevicePolicyManager.class) + .getResources().getString(RESOLVER_WORK_PAUSED_TITLE, + () -> mContext.getString(R.string.resolver_turn_on_work_apps)); + + return new WorkProfileOffEmptyState(title, (tab) -> { + tab.showSpinner(); + if (mOnSwitchOnWorkSelectedListener != null) { + mOnSwitchOnWorkSelectedListener.onSwitchOnWorkSelected(); + } + mQuietModeManager.requestQuietModeEnabled(false, mWorkProfileUserHandle); + }, mMetricsCategory); + } + + public static class WorkProfileOffEmptyState implements EmptyState { + + private final String mTitle; + private final ClickListener mOnClick; + private final String mMetricsCategory; + + public WorkProfileOffEmptyState(String title, @NonNull ClickListener onClick, + @NonNull String metricsCategory) { + mTitle = title; + mOnClick = onClick; + mMetricsCategory = metricsCategory; + } + + @Nullable + @Override + public String getTitle() { + return mTitle; + } + + @Nullable + @Override + public ClickListener getButtonClickListener() { + return mOnClick; + } + + @Override + public void onEmptyStateShown() { + DevicePolicyEventLogger + .createEvent(DevicePolicyEnums.RESOLVER_EMPTY_STATE_WORK_APPS_DISABLED) + .setStrings(mMetricsCategory) + .write(); + } + } +} diff --git a/java/tests/src/com/android/intentresolver/ChooserActivityOverrideData.java b/java/tests/src/com/android/intentresolver/ChooserActivityOverrideData.java index dd78b69e..5acdb42c 100644 --- a/java/tests/src/com/android/intentresolver/ChooserActivityOverrideData.java +++ b/java/tests/src/com/android/intentresolver/ChooserActivityOverrideData.java @@ -16,20 +16,24 @@ package com.android.intentresolver; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; -import android.content.Intent; import android.content.pm.PackageManager; import android.content.res.Resources; import android.database.Cursor; import android.graphics.Bitmap; import android.os.UserHandle; +import com.android.intentresolver.AbstractMultiProfilePagerAdapter.CrossProfileIntentsChecker; +import com.android.intentresolver.AbstractMultiProfilePagerAdapter.MyUserIdProvider; +import com.android.intentresolver.AbstractMultiProfilePagerAdapter.QuietModeManager; import com.android.intentresolver.chooser.TargetInfo; import com.android.intentresolver.shortcuts.ShortcutLoader; import com.android.internal.logging.MetricsLogger; -import java.util.List; import java.util.function.Consumer; import java.util.function.Function; @@ -69,7 +73,10 @@ public class ChooserActivityOverrideData { public UserHandle workProfileUserHandle; public boolean hasCrossProfileIntents; public boolean isQuietModeEnabled; - public AbstractMultiProfilePagerAdapter.Injector multiPagerAdapterInjector; + public Integer myUserId; + public QuietModeManager mQuietModeManager; + public MyUserIdProvider mMyUserIdProvider; + public CrossProfileIntentsChecker mCrossProfileIntentsChecker; public PackageManager packageManager; public void reset() { @@ -89,14 +96,9 @@ public class ChooserActivityOverrideData { workProfileUserHandle = null; hasCrossProfileIntents = true; isQuietModeEnabled = false; + myUserId = null; packageManager = null; - multiPagerAdapterInjector = new AbstractMultiProfilePagerAdapter.Injector() { - @Override - public boolean hasCrossProfileIntents(List<Intent> intents, int sourceUserId, - int targetUserId) { - return hasCrossProfileIntents; - } - + mQuietModeManager = new QuietModeManager() { @Override public boolean isQuietModeEnabled(UserHandle workProfileUserHandle) { return isQuietModeEnabled; @@ -107,8 +109,28 @@ public class ChooserActivityOverrideData { UserHandle workProfileUserHandle) { isQuietModeEnabled = enabled; } + + @Override + public void markWorkProfileEnabledBroadcastReceived() { + } + + @Override + public boolean isWaitingToEnableWorkProfile() { + return false; + } }; shortcutLoaderFactory = ((userHandle, resultConsumer) -> null); + + mMyUserIdProvider = new MyUserIdProvider() { + @Override + public int getMyUserId() { + return myUserId != null ? myUserId : UserHandle.myUserId(); + } + }; + + mCrossProfileIntentsChecker = mock(CrossProfileIntentsChecker.class); + when(mCrossProfileIntentsChecker.hasCrossProfileIntents(any(), anyInt(), anyInt())) + .thenAnswer(invocation -> hasCrossProfileIntents); } private ChooserActivityOverrideData() {} diff --git a/java/tests/src/com/android/intentresolver/ChooserWrapperActivity.java b/java/tests/src/com/android/intentresolver/ChooserWrapperActivity.java index 6b74fcd4..56e583bb 100644 --- a/java/tests/src/com/android/intentresolver/ChooserWrapperActivity.java +++ b/java/tests/src/com/android/intentresolver/ChooserWrapperActivity.java @@ -35,13 +35,10 @@ import android.net.Uri; import android.os.UserHandle; import android.util.Size; -import com.android.intentresolver.AbstractMultiProfilePagerAdapter; -import com.android.intentresolver.ChooserActivityLogger; -import com.android.intentresolver.ChooserActivityOverrideData; -import com.android.intentresolver.ChooserListAdapter; -import com.android.intentresolver.IChooserWrapper; +import com.android.intentresolver.AbstractMultiProfilePagerAdapter.CrossProfileIntentsChecker; +import com.android.intentresolver.AbstractMultiProfilePagerAdapter.MyUserIdProvider; +import com.android.intentresolver.AbstractMultiProfilePagerAdapter.QuietModeManager; import com.android.intentresolver.ResolverListAdapter.ResolveInfoPresentationGetter; -import com.android.intentresolver.ResolverListController; import com.android.intentresolver.chooser.DisplayResolveInfo; import com.android.intentresolver.chooser.NotSelectableTargetInfo; import com.android.intentresolver.chooser.TargetInfo; @@ -69,15 +66,6 @@ public class ChooserWrapperActivity } @Override - protected AbstractMultiProfilePagerAdapter createMultiProfilePagerAdapter( - Intent[] initialIntents, List<ResolveInfo> rList, boolean filterLastUsed) { - AbstractMultiProfilePagerAdapter multiProfilePagerAdapter = - super.createMultiProfilePagerAdapter(initialIntents, rList, filterLastUsed); - multiProfilePagerAdapter.setInjector(sOverrides.multiPagerAdapterInjector); - return multiProfilePagerAdapter; - } - - @Override public ChooserListAdapter createChooserListAdapter(Context context, List<Intent> payloadIntents, Intent[] initialIntents, List<ResolveInfo> rList, boolean filterLastUsed, ResolverListController resolverListController) { @@ -150,6 +138,30 @@ public class ChooserWrapperActivity } @Override + protected MyUserIdProvider createMyUserIdProvider() { + if (sOverrides.mMyUserIdProvider != null) { + return sOverrides.mMyUserIdProvider; + } + return super.createMyUserIdProvider(); + } + + @Override + protected CrossProfileIntentsChecker createCrossProfileIntentsChecker() { + if (sOverrides.mCrossProfileIntentsChecker != null) { + return sOverrides.mCrossProfileIntentsChecker; + } + return super.createCrossProfileIntentsChecker(); + } + + @Override + protected QuietModeManager createQuietModeManager() { + if (sOverrides.mQuietModeManager != null) { + return sOverrides.mQuietModeManager; + } + return super.createQuietModeManager(); + } + + @Override public void safelyStartActivity(com.android.intentresolver.chooser.TargetInfo cti) { if (sOverrides.onSafelyStartCallback != null && sOverrides.onSafelyStartCallback.apply(cti)) { diff --git a/java/tests/src/com/android/intentresolver/ResolverWrapperActivity.java b/java/tests/src/com/android/intentresolver/ResolverWrapperActivity.java index b6d63265..7d4b07d8 100644 --- a/java/tests/src/com/android/intentresolver/ResolverWrapperActivity.java +++ b/java/tests/src/com/android/intentresolver/ResolverWrapperActivity.java @@ -16,6 +16,8 @@ package com.android.intentresolver; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -27,6 +29,9 @@ import android.content.pm.ResolveInfo; import android.os.Bundle; import android.os.UserHandle; +import com.android.intentresolver.AbstractMultiProfilePagerAdapter.CrossProfileIntentsChecker; +import com.android.intentresolver.AbstractMultiProfilePagerAdapter.MyUserIdProvider; +import com.android.intentresolver.AbstractMultiProfilePagerAdapter.QuietModeManager; import com.android.intentresolver.chooser.TargetInfo; import java.util.List; @@ -59,12 +64,27 @@ public class ResolverWrapperActivity extends ResolverActivity { } @Override - protected AbstractMultiProfilePagerAdapter createMultiProfilePagerAdapter( - Intent[] initialIntents, List<ResolveInfo> rList, boolean filterLastUsed) { - AbstractMultiProfilePagerAdapter multiProfilePagerAdapter = - super.createMultiProfilePagerAdapter(initialIntents, rList, filterLastUsed); - multiProfilePagerAdapter.setInjector(sOverrides.multiPagerAdapterInjector); - return multiProfilePagerAdapter; + protected MyUserIdProvider createMyUserIdProvider() { + if (sOverrides.mMyUserIdProvider != null) { + return sOverrides.mMyUserIdProvider; + } + return super.createMyUserIdProvider(); + } + + @Override + protected CrossProfileIntentsChecker createCrossProfileIntentsChecker() { + if (sOverrides.mCrossProfileIntentsChecker != null) { + return sOverrides.mCrossProfileIntentsChecker; + } + return super.createCrossProfileIntentsChecker(); + } + + @Override + protected QuietModeManager createQuietModeManager() { + if (sOverrides.mQuietModeManager != null) { + return sOverrides.mQuietModeManager; + } + return super.createQuietModeManager(); } ResolverWrapperAdapter getAdapter() { @@ -144,9 +164,12 @@ public class ResolverWrapperActivity extends ResolverActivity { public ResolverListController workResolverListController; public Boolean isVoiceInteraction; public UserHandle workProfileUserHandle; + public Integer myUserId; public boolean hasCrossProfileIntents; public boolean isQuietModeEnabled; - public AbstractMultiProfilePagerAdapter.Injector multiPagerAdapterInjector; + public QuietModeManager mQuietModeManager; + public MyUserIdProvider mMyUserIdProvider; + public CrossProfileIntentsChecker mCrossProfileIntentsChecker; public void reset() { onSafelyStartCallback = null; @@ -155,15 +178,11 @@ public class ResolverWrapperActivity extends ResolverActivity { resolverListController = mock(ResolverListController.class); workResolverListController = mock(ResolverListController.class); workProfileUserHandle = null; + myUserId = null; hasCrossProfileIntents = true; isQuietModeEnabled = false; - multiPagerAdapterInjector = new AbstractMultiProfilePagerAdapter.Injector() { - @Override - public boolean hasCrossProfileIntents(List<Intent> intents, int sourceUserId, - int targetUserId) { - return hasCrossProfileIntents; - } + mQuietModeManager = new QuietModeManager() { @Override public boolean isQuietModeEnabled(UserHandle workProfileUserHandle) { return isQuietModeEnabled; @@ -174,7 +193,27 @@ public class ResolverWrapperActivity extends ResolverActivity { UserHandle workProfileUserHandle) { isQuietModeEnabled = enabled; } + + @Override + public void markWorkProfileEnabledBroadcastReceived() { + } + + @Override + public boolean isWaitingToEnableWorkProfile() { + return false; + } }; + + mMyUserIdProvider = new MyUserIdProvider() { + @Override + public int getMyUserId() { + return myUserId != null ? myUserId : UserHandle.myUserId(); + } + }; + + mCrossProfileIntentsChecker = mock(CrossProfileIntentsChecker.class); + when(mCrossProfileIntentsChecker.hasCrossProfileIntents(any(), anyInt(), anyInt())) + .thenAnswer(invocation -> hasCrossProfileIntents); } } } diff --git a/java/tests/src/com/android/intentresolver/UnbundledChooserActivityWorkProfileTest.java b/java/tests/src/com/android/intentresolver/UnbundledChooserActivityWorkProfileTest.java new file mode 100644 index 00000000..b7eecb3f --- /dev/null +++ b/java/tests/src/com/android/intentresolver/UnbundledChooserActivityWorkProfileTest.java @@ -0,0 +1,455 @@ +/* + * Copyright (C) 2022 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; + +import static androidx.test.espresso.Espresso.onView; +import static androidx.test.espresso.action.ViewActions.click; +import static androidx.test.espresso.action.ViewActions.swipeUp; +import static androidx.test.espresso.assertion.ViewAssertions.matches; +import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed; +import static androidx.test.espresso.matcher.ViewMatchers.withId; +import static androidx.test.espresso.matcher.ViewMatchers.withText; + +import static com.android.intentresolver.UnbundledChooserActivityWorkProfileTest.TestCase.ExpectedBlocker.NO_BLOCKER; +import static com.android.intentresolver.UnbundledChooserActivityWorkProfileTest.TestCase.ExpectedBlocker.PERSONAL_PROFILE_ACCESS_BLOCKER; +import static com.android.intentresolver.UnbundledChooserActivityWorkProfileTest.TestCase.ExpectedBlocker.PERSONAL_PROFILE_SHARE_BLOCKER; +import static com.android.intentresolver.UnbundledChooserActivityWorkProfileTest.TestCase.ExpectedBlocker.WORK_PROFILE_ACCESS_BLOCKER; +import static com.android.intentresolver.UnbundledChooserActivityWorkProfileTest.TestCase.ExpectedBlocker.WORK_PROFILE_SHARE_BLOCKER; +import static com.android.intentresolver.UnbundledChooserActivityWorkProfileTest.TestCase.Tab.PERSONAL; +import static com.android.intentresolver.UnbundledChooserActivityWorkProfileTest.TestCase.Tab.WORK; +import static com.android.intentresolver.ChooserWrapperActivity.sOverrides; + +import static org.hamcrest.CoreMatchers.not; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +import android.companion.DeviceFilter; +import android.content.Intent; +import android.os.UserHandle; + +import androidx.test.InstrumentationRegistry; +import androidx.test.espresso.NoMatchingViewException; +import androidx.test.rule.ActivityTestRule; + +import com.android.internal.R; +import com.android.intentresolver.ResolverActivity.ResolvedComponentInfo; +import com.android.intentresolver.UnbundledChooserActivityWorkProfileTest.TestCase.Tab; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.mockito.Mockito; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +@DeviceFilter.MediumType +@RunWith(Parameterized.class) +public class UnbundledChooserActivityWorkProfileTest { + + private static final UserHandle PERSONAL_USER_HANDLE = InstrumentationRegistry + .getInstrumentation().getTargetContext().getUser(); + private static final UserHandle WORK_USER_HANDLE = UserHandle.of(10); + + @Rule + public ActivityTestRule<ChooserWrapperActivity> mActivityRule = + new ActivityTestRule<>(ChooserWrapperActivity.class, false, + false); + private final TestCase mTestCase; + + public UnbundledChooserActivityWorkProfileTest(TestCase testCase) { + mTestCase = testCase; + } + + @Before + public void cleanOverrideData() { + // TODO: use the other form of `adoptShellPermissionIdentity()` where we explicitly list the + // permissions we require (which we'll read from the manifest at runtime). + InstrumentationRegistry + .getInstrumentation() + .getUiAutomation() + .adoptShellPermissionIdentity(); + + sOverrides.reset(); + } + + @Test + public void testBlocker() { + setUpPersonalAndWorkComponentInfos(); + sOverrides.hasCrossProfileIntents = mTestCase.hasCrossProfileIntents(); + sOverrides.myUserId = mTestCase.getMyUserHandle().getIdentifier(); + + launchActivity(mTestCase.getIsSendAction()); + switchToTab(mTestCase.getTab()); + + switch (mTestCase.getExpectedBlocker()) { + case NO_BLOCKER: + assertNoBlockerDisplayed(); + break; + case PERSONAL_PROFILE_SHARE_BLOCKER: + assertCantSharePersonalAppsBlockerDisplayed(); + break; + case WORK_PROFILE_SHARE_BLOCKER: + assertCantShareWorkAppsBlockerDisplayed(); + break; + case PERSONAL_PROFILE_ACCESS_BLOCKER: + assertCantAccessPersonalAppsBlockerDisplayed(); + break; + case WORK_PROFILE_ACCESS_BLOCKER: + assertCantAccessWorkAppsBlockerDisplayed(); + break; + } + } + + @Parameterized.Parameters(name = "{0}") + public static Collection tests() { + return Arrays.asList( + new TestCase( + /* isSendAction= */ true, + /* hasCrossProfileIntents= */ true, + /* myUserHandle= */ WORK_USER_HANDLE, + /* tab= */ WORK, + /* expectedBlocker= */ NO_BLOCKER + ), +// TODO(b/256869196) ChooserActivity goes into requestLayout loop +// new TestCase( +// /* isSendAction= */ true, +// /* hasCrossProfileIntents= */ false, +// /* myUserHandle= */ WORK_USER_HANDLE, +// /* tab= */ WORK, +// /* expectedBlocker= */ NO_BLOCKER +// ), + new TestCase( + /* isSendAction= */ true, + /* hasCrossProfileIntents= */ true, + /* myUserHandle= */ PERSONAL_USER_HANDLE, + /* tab= */ WORK, + /* expectedBlocker= */ NO_BLOCKER + ), + new TestCase( + /* isSendAction= */ true, + /* hasCrossProfileIntents= */ false, + /* myUserHandle= */ PERSONAL_USER_HANDLE, + /* tab= */ WORK, + /* expectedBlocker= */ WORK_PROFILE_SHARE_BLOCKER + ), + new TestCase( + /* isSendAction= */ true, + /* hasCrossProfileIntents= */ true, + /* myUserHandle= */ WORK_USER_HANDLE, + /* tab= */ PERSONAL, + /* expectedBlocker= */ NO_BLOCKER + ), +// TODO(b/256869196) ChooserActivity goes into requestLayout loop +// new TestCase( +// /* isSendAction= */ true, +// /* hasCrossProfileIntents= */ false, +// /* myUserHandle= */ WORK_USER_HANDLE, +// /* tab= */ PERSONAL, +// /* expectedBlocker= */ PERSONAL_PROFILE_SHARE_BLOCKER +// ), + new TestCase( + /* isSendAction= */ true, + /* hasCrossProfileIntents= */ true, + /* myUserHandle= */ PERSONAL_USER_HANDLE, + /* tab= */ PERSONAL, + /* expectedBlocker= */ NO_BLOCKER + ), + new TestCase( + /* isSendAction= */ true, + /* hasCrossProfileIntents= */ false, + /* myUserHandle= */ PERSONAL_USER_HANDLE, + /* tab= */ PERSONAL, + /* expectedBlocker= */ NO_BLOCKER + ), + new TestCase( + /* isSendAction= */ false, + /* hasCrossProfileIntents= */ true, + /* myUserHandle= */ WORK_USER_HANDLE, + /* tab= */ WORK, + /* expectedBlocker= */ NO_BLOCKER + ), + new TestCase( + /* isSendAction= */ false, + /* hasCrossProfileIntents= */ false, + /* myUserHandle= */ WORK_USER_HANDLE, + /* tab= */ WORK, + /* expectedBlocker= */ NO_BLOCKER + ), + new TestCase( + /* isSendAction= */ false, + /* hasCrossProfileIntents= */ true, + /* myUserHandle= */ PERSONAL_USER_HANDLE, + /* tab= */ WORK, + /* expectedBlocker= */ NO_BLOCKER + ), + new TestCase( + /* isSendAction= */ false, + /* hasCrossProfileIntents= */ false, + /* myUserHandle= */ PERSONAL_USER_HANDLE, + /* tab= */ WORK, + /* expectedBlocker= */ WORK_PROFILE_ACCESS_BLOCKER + ), + new TestCase( + /* isSendAction= */ false, + /* hasCrossProfileIntents= */ true, + /* myUserHandle= */ WORK_USER_HANDLE, + /* tab= */ PERSONAL, + /* expectedBlocker= */ NO_BLOCKER + ), + new TestCase( + /* isSendAction= */ false, + /* hasCrossProfileIntents= */ false, + /* myUserHandle= */ WORK_USER_HANDLE, + /* tab= */ PERSONAL, + /* expectedBlocker= */ PERSONAL_PROFILE_ACCESS_BLOCKER + ), + new TestCase( + /* isSendAction= */ false, + /* hasCrossProfileIntents= */ true, + /* myUserHandle= */ PERSONAL_USER_HANDLE, + /* tab= */ PERSONAL, + /* expectedBlocker= */ NO_BLOCKER + ), + new TestCase( + /* isSendAction= */ false, + /* hasCrossProfileIntents= */ false, + /* myUserHandle= */ PERSONAL_USER_HANDLE, + /* tab= */ PERSONAL, + /* expectedBlocker= */ NO_BLOCKER + ) + ); + } + + private List<ResolvedComponentInfo> createResolvedComponentsForTestWithOtherProfile( + int numberOfResults, int userId) { + List<ResolvedComponentInfo> infoList = new ArrayList<>(numberOfResults); + for (int i = 0; i < numberOfResults; i++) { + infoList.add( + ResolverDataProvider.createResolvedComponentInfoWithOtherId(i, userId)); + } + return infoList; + } + + private List<ResolvedComponentInfo> createResolvedComponentsForTest(int numberOfResults) { + List<ResolvedComponentInfo> infoList = new ArrayList<>(numberOfResults); + for (int i = 0; i < numberOfResults; i++) { + infoList.add(ResolverDataProvider.createResolvedComponentInfo(i)); + } + return infoList; + } + + private void setUpPersonalAndWorkComponentInfos() { + markWorkProfileUserAvailable(); + int workProfileTargets = 4; + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsForTestWithOtherProfile(3, + /* userId */ WORK_USER_HANDLE.getIdentifier()); + List<ResolvedComponentInfo> workResolvedComponentInfos = + createResolvedComponentsForTest(workProfileTargets); + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + } + + private void setupResolverControllers( + List<ResolvedComponentInfo> personalResolvedComponentInfos, + List<ResolvedComponentInfo> workResolvedComponentInfos) { + when(sOverrides.resolverListController.getResolversForIntent(Mockito.anyBoolean(), + Mockito.anyBoolean(), + Mockito.anyBoolean(), + Mockito.isA(List.class))) + .thenReturn(new ArrayList<>(personalResolvedComponentInfos)); + when(sOverrides.workResolverListController.getResolversForIntent(Mockito.anyBoolean(), + Mockito.anyBoolean(), + Mockito.anyBoolean(), + Mockito.isA(List.class))).thenReturn(workResolvedComponentInfos); + when(sOverrides.workResolverListController.getResolversForIntentAsUser(Mockito.anyBoolean(), + Mockito.anyBoolean(), + Mockito.anyBoolean(), + Mockito.isA(List.class), + eq(UserHandle.SYSTEM))) + .thenReturn(new ArrayList<>(personalResolvedComponentInfos)); + } + + private void waitForIdle() { + InstrumentationRegistry.getInstrumentation().waitForIdleSync(); + } + + private void markWorkProfileUserAvailable() { + ChooserWrapperActivity.sOverrides.workProfileUserHandle = WORK_USER_HANDLE; + } + + private void assertCantAccessWorkAppsBlockerDisplayed() { + onView(withText(R.string.resolver_cross_profile_blocked)) + .check(matches(isDisplayed())); + onView(withText(R.string.resolver_cant_access_work_apps_explanation)) + .check(matches(isDisplayed())); + } + + private void assertCantAccessPersonalAppsBlockerDisplayed() { + onView(withText(R.string.resolver_cross_profile_blocked)) + .check(matches(isDisplayed())); + onView(withText(R.string.resolver_cant_access_personal_apps_explanation)) + .check(matches(isDisplayed())); + } + + private void assertCantShareWorkAppsBlockerDisplayed() { + onView(withText(R.string.resolver_cross_profile_blocked)) + .check(matches(isDisplayed())); + onView(withText(R.string.resolver_cant_share_with_work_apps_explanation)) + .check(matches(isDisplayed())); + } + + private void assertCantSharePersonalAppsBlockerDisplayed() { + onView(withText(R.string.resolver_cross_profile_blocked)) + .check(matches(isDisplayed())); + onView(withText(R.string.resolver_cant_share_with_personal_apps_explanation)) + .check(matches(isDisplayed())); + } + + private void assertNoBlockerDisplayed() { + try { + onView(withText(R.string.resolver_cross_profile_blocked)) + .check(matches(not(isDisplayed()))); + } catch (NoMatchingViewException ignored) { + } + } + + private void switchToTab(Tab tab) { + final int stringId = tab == Tab.WORK ? R.string.resolver_work_tab + : R.string.resolver_personal_tab; + + onView(withText(stringId)).perform(click()); + waitForIdle(); + + onView(withId(R.id.contentPanel)) + .perform(swipeUp()); + waitForIdle(); + } + + private Intent createTextIntent(boolean isSendAction) { + Intent sendIntent = new Intent(); + if (isSendAction) { + sendIntent.setAction(Intent.ACTION_SEND); + } + sendIntent.putExtra(Intent.EXTRA_TEXT, "testing intent sending"); + sendIntent.setType("text/plain"); + return sendIntent; + } + + private void launchActivity(boolean isSendAction) { + Intent sendIntent = createTextIntent(isSendAction); + mActivityRule.launchActivity(Intent.createChooser(sendIntent, "Test")); + waitForIdle(); + } + + public static class TestCase { + private final boolean mIsSendAction; + private final boolean mHasCrossProfileIntents; + private final UserHandle mMyUserHandle; + private final Tab mTab; + private final ExpectedBlocker mExpectedBlocker; + + public enum ExpectedBlocker { + NO_BLOCKER, + PERSONAL_PROFILE_SHARE_BLOCKER, + WORK_PROFILE_SHARE_BLOCKER, + PERSONAL_PROFILE_ACCESS_BLOCKER, + WORK_PROFILE_ACCESS_BLOCKER + } + + public enum Tab { + WORK, + PERSONAL + } + + public TestCase(boolean isSendAction, boolean hasCrossProfileIntents, + UserHandle myUserHandle, Tab tab, ExpectedBlocker expectedBlocker) { + mIsSendAction = isSendAction; + mHasCrossProfileIntents = hasCrossProfileIntents; + mMyUserHandle = myUserHandle; + mTab = tab; + mExpectedBlocker = expectedBlocker; + } + + public boolean getIsSendAction() { + return mIsSendAction; + } + + public boolean hasCrossProfileIntents() { + return mHasCrossProfileIntents; + } + + public UserHandle getMyUserHandle() { + return mMyUserHandle; + } + + public Tab getTab() { + return mTab; + } + + public ExpectedBlocker getExpectedBlocker() { + return mExpectedBlocker; + } + + @Override + public String toString() { + StringBuilder result = new StringBuilder("test"); + + if (mTab == WORK) { + result.append("WorkTab_"); + } else { + result.append("PersonalTab_"); + } + + if (mIsSendAction) { + result.append("sendAction_"); + } else { + result.append("notSendAction_"); + } + + if (mHasCrossProfileIntents) { + result.append("hasCrossProfileIntents_"); + } else { + result.append("doesNotHaveCrossProfileIntents_"); + } + + if (mMyUserHandle.equals(PERSONAL_USER_HANDLE)) { + result.append("myUserIsPersonal_"); + } else { + result.append("myUserIsWork_"); + } + + if (mExpectedBlocker == ExpectedBlocker.NO_BLOCKER) { + result.append("thenNoBlocker"); + } else if (mExpectedBlocker == PERSONAL_PROFILE_ACCESS_BLOCKER) { + result.append("thenAccessBlockerOnPersonalProfile"); + } else if (mExpectedBlocker == PERSONAL_PROFILE_SHARE_BLOCKER) { + result.append("thenShareBlockerOnPersonalProfile"); + } else if (mExpectedBlocker == WORK_PROFILE_ACCESS_BLOCKER) { + result.append("thenAccessBlockerOnWorkProfile"); + } else if (mExpectedBlocker == WORK_PROFILE_SHARE_BLOCKER) { + result.append("thenShareBlockerOnWorkProfile"); + } + + return result.toString(); + } + } +} |