diff options
-rw-r--r-- | java/res/values/strings.xml | 6 | ||||
-rw-r--r-- | java/src/com/android/intentresolver/ChooserActivity.java | 45 | ||||
-rw-r--r-- | java/src/com/android/intentresolver/ProfileHelper.kt | 8 | ||||
-rw-r--r-- | java/src/com/android/intentresolver/ResolverActivity.java | 41 | ||||
-rw-r--r-- | java/src/com/android/intentresolver/data/repository/DevicePolicyResources.kt | 22 | ||||
-rw-r--r-- | java/src/com/android/intentresolver/emptystate/CompositeEmptyStateProvider.kt (renamed from java/src/com/android/intentresolver/emptystate/CompositeEmptyStateProvider.java) | 28 | ||||
-rw-r--r-- | java/src/com/android/intentresolver/emptystate/DefaultEmptyState.kt | 20 | ||||
-rw-r--r-- | java/src/com/android/intentresolver/emptystate/DevicePolicyBlockerEmptyState.java | 48 | ||||
-rw-r--r-- | java/src/com/android/intentresolver/emptystate/NoAppsAvailableEmptyStateProvider.java | 9 | ||||
-rw-r--r-- | java/src/com/android/intentresolver/emptystate/NoCrossProfileEmptyStateProvider.java | 91 | ||||
-rw-r--r-- | tests/unit/src/com/android/intentresolver/emptystate/NoCrossProfileEmptyStateProviderTest.kt | 218 |
11 files changed, 283 insertions, 253 deletions
diff --git a/java/res/values/strings.xml b/java/res/values/strings.xml index 17a514d7..32c61327 100644 --- a/java/res/values/strings.xml +++ b/java/res/values/strings.xml @@ -284,6 +284,12 @@ <!-- Error message. This message lets the user know that their IT admin doesn't allow them to open this specific content with an app in their personal profile. [CHAR LIMIT=NONE] --> <string name="resolver_cant_access_personal_apps_explanation">This content can\u2019t be opened with personal apps</string> + <!-- Error message. This text is explaining that the user's IT admin doesn't allow this specific content to be shared with apps in the private profile. [CHAR LIMIT=NONE] --> + <string name="resolver_cant_share_with_private_apps_explanation">This content can\u2019t be shared with private apps</string> + + <!-- Error message. This message lets the user know that their IT admin doesn't allow them to open this specific content with an app in their private profile. [CHAR LIMIT=NONE] --> + <string name="resolver_cant_access_private_apps_explanation">This content can\u2019t be opened with private apps</string> + <!-- Error message. This text lets the user know that they need to turn on work apps in order to share or open content. There's also a button a user can tap to turn on the apps. [CHAR LIMIT=NONE] --> <string name="resolver_turn_on_work_apps">Work apps are paused</string> <!-- Button text. This button unpauses a user's work apps and data. [CHAR LIMIT=NONE] --> diff --git a/java/src/com/android/intentresolver/ChooserActivity.java b/java/src/com/android/intentresolver/ChooserActivity.java index 1922c05c..4608f37b 100644 --- a/java/src/com/android/intentresolver/ChooserActivity.java +++ b/java/src/com/android/intentresolver/ChooserActivity.java @@ -17,14 +17,7 @@ package com.android.intentresolver; import static android.app.VoiceInteractor.PickOptionRequest.Option; -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.content.Intent.FLAG_ACTIVITY_NEW_TASK; -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 static androidx.lifecycle.LifecycleKt.getCoroutineScope; @@ -111,8 +104,6 @@ import com.android.intentresolver.data.repository.DevicePolicyResources; import com.android.intentresolver.domain.interactor.UserInteractor; import com.android.intentresolver.emptystate.CompositeEmptyStateProvider; import com.android.intentresolver.emptystate.CrossProfileIntentsChecker; -import com.android.intentresolver.emptystate.DevicePolicyBlockerEmptyState; -import com.android.intentresolver.emptystate.EmptyState; import com.android.intentresolver.emptystate.EmptyStateProvider; import com.android.intentresolver.emptystate.NoAppsAvailableEmptyStateProvider; import com.android.intentresolver.emptystate.NoCrossProfileEmptyStateProvider; @@ -213,7 +204,7 @@ public class ChooserActivity extends Hilt_ChooserActivity implements private static final String TAB_TAG_WORK = "work"; private static final String LAST_SHOWN_TAB_KEY = "last_shown_tab_key"; - protected static final String METRICS_CATEGORY_CHOOSER = "intent_chooser"; + public static final String METRICS_CATEGORY_CHOOSER = "intent_chooser"; private int mLayoutId; private UserHandle mHeaderCreatorUser; @@ -1450,39 +1441,11 @@ public class ChooserActivity extends Hilt_ChooserActivity implements } protected EmptyStateProvider createBlockerEmptyStateProvider() { - final boolean isSendAction = mRequest.isSendActionTarget(); - - 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( mProfiles, - noWorkToPersonalEmptyState, - noPersonalToWorkEmptyState, - createCrossProfileIntentsChecker()); + mDevicePolicyResources, + createCrossProfileIntentsChecker(), + mRequest.isSendActionTarget()); } private int findSelectedProfile() { diff --git a/java/src/com/android/intentresolver/ProfileHelper.kt b/java/src/com/android/intentresolver/ProfileHelper.kt index e1d912c3..53a873a3 100644 --- a/java/src/com/android/intentresolver/ProfileHelper.kt +++ b/java/src/com/android/intentresolver/ProfileHelper.kt @@ -80,12 +80,12 @@ constructor( launchedByUser.handle } - fun findProfileType(handle: UserHandle): Profile.Type? { - val matched = - profiles.firstOrNull { it.primary.handle == handle || it.clone?.handle == handle } - return matched?.type + fun findProfile(handle: UserHandle): Profile? { + return profiles.firstOrNull { it.primary.handle == handle || it.clone?.handle == handle } } + fun findProfileType(handle: UserHandle): Profile.Type? = findProfile(handle)?.type + // Name retained for ease of review, to be renamed later fun getQueryIntentsHandle(handle: UserHandle): UserHandle? { return if (isLaunchedAsCloneProfile && handle == personalHandle) { diff --git a/java/src/com/android/intentresolver/ResolverActivity.java b/java/src/com/android/intentresolver/ResolverActivity.java index 1b08d957..e79cb2d1 100644 --- a/java/src/com/android/intentresolver/ResolverActivity.java +++ b/java/src/com/android/intentresolver/ResolverActivity.java @@ -16,12 +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.content.Intent.FLAG_ACTIVITY_NEW_TASK; -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 static androidx.lifecycle.LifecycleKt.getCoroutineScope; @@ -94,8 +89,6 @@ import com.android.intentresolver.data.repository.DevicePolicyResources; import com.android.intentresolver.domain.interactor.UserInteractor; import com.android.intentresolver.emptystate.CompositeEmptyStateProvider; import com.android.intentresolver.emptystate.CrossProfileIntentsChecker; -import com.android.intentresolver.emptystate.DevicePolicyBlockerEmptyState; -import com.android.intentresolver.emptystate.EmptyState; import com.android.intentresolver.emptystate.EmptyStateProvider; import com.android.intentresolver.emptystate.NoAppsAvailableEmptyStateProvider; import com.android.intentresolver.emptystate.NoCrossProfileEmptyStateProvider; @@ -184,7 +177,6 @@ public class ResolverActivity extends Hilt_ResolverActivity implements private Space mFooterSpacer = null; protected static final String METRICS_CATEGORY_RESOLVER = "intent_resolver"; - protected static final String METRICS_CATEGORY_CHOOSER = "intent_chooser"; /** Tracks if we should ignore future broadcasts telling us the work profile is enabled */ private final boolean mWorkProfileHasBeenEnabled = false; @@ -449,42 +441,17 @@ public class ResolverActivity extends Hilt_ResolverActivity implements } protected EmptyStateProvider createBlockerEmptyStateProvider() { - final boolean shouldShowNoCrossProfileIntentsEmptyState = getUser().equals(getIntentUser()); + boolean shouldShowNoCrossProfileIntentsEmptyState = getUser().equals(getIntentUser()); if (!shouldShowNoCrossProfileIntentsEmptyState) { // Implementation that doesn't show any blockers return new EmptyStateProvider() {}; } - - final 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 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( mProfiles, - noWorkToPersonalEmptyState, - noPersonalToWorkEmptyState, - createCrossProfileIntentsChecker()); + mDevicePolicyResources, + createCrossProfileIntentsChecker(), + /* isShare= */ false); } /** diff --git a/java/src/com/android/intentresolver/data/repository/DevicePolicyResources.kt b/java/src/com/android/intentresolver/data/repository/DevicePolicyResources.kt index 75faa068..eb35a358 100644 --- a/java/src/com/android/intentresolver/data/repository/DevicePolicyResources.kt +++ b/java/src/com/android/intentresolver/data/repository/DevicePolicyResources.kt @@ -27,13 +27,15 @@ import android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_WORK_PROFIL import android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_WORK_TAB import android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_WORK_TAB_ACCESSIBILITY import android.content.res.Resources +import androidx.annotation.OpenForTesting import com.android.intentresolver.R import com.android.intentresolver.inject.ApplicationOwned import javax.inject.Inject import javax.inject.Singleton +@OpenForTesting @Singleton -class DevicePolicyResources +open class DevicePolicyResources @Inject constructor( @ApplicationOwned private val resources: Resources, @@ -102,7 +104,7 @@ constructor( ) } - val crossProfileBlocked by lazy { + open val crossProfileBlocked by lazy { requireNotNull( policyResources.getString(RESOLVER_CROSS_PROFILE_BLOCKED_TITLE) { resources.getString(R.string.resolver_cross_profile_blocked) @@ -110,22 +112,30 @@ constructor( ) } - fun toPersonalBlockedByPolicyMessage(sendAction: Boolean): String { - return if (sendAction) { + open fun toPersonalBlockedByPolicyMessage(share: Boolean): String { + return if (share) { resources.getString(R.string.resolver_cant_share_with_personal_apps_explanation) } else { resources.getString(R.string.resolver_cant_access_personal_apps_explanation) } } - fun toWorkBlockedByPolicyMessage(sendAction: Boolean): String { - return if (sendAction) { + open fun toWorkBlockedByPolicyMessage(share: Boolean): String { + return if (share) { resources.getString(R.string.resolver_cant_share_with_work_apps_explanation) } else { resources.getString(R.string.resolver_cant_access_work_apps_explanation) } } + open fun toPrivateBlockedByPolicyMessage(share: Boolean): String { + return if (share) { + resources.getString(R.string.resolver_cant_share_with_private_apps_explanation) + } else { + resources.getString(R.string.resolver_cant_access_private_apps_explanation) + } + } + fun getWorkProfileNotSupportedMessage(launcherName: String): String { return requireNotNull( policyResources.getString( diff --git a/java/src/com/android/intentresolver/emptystate/CompositeEmptyStateProvider.java b/java/src/com/android/intentresolver/emptystate/CompositeEmptyStateProvider.kt index 41422b66..05062a4b 100644 --- a/java/src/com/android/intentresolver/emptystate/CompositeEmptyStateProvider.java +++ b/java/src/com/android/intentresolver/emptystate/CompositeEmptyStateProvider.kt @@ -13,34 +13,20 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.android.intentresolver.emptystate; +package com.android.intentresolver.emptystate -import android.annotation.Nullable; - -import com.android.intentresolver.ResolverListAdapter; +import com.android.intentresolver.ResolverListAdapter /** * 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 class CompositeEmptyStateProvider implements EmptyStateProvider { - - private final EmptyStateProvider[] mProviders; - - public CompositeEmptyStateProvider(EmptyStateProvider... providers) { - mProviders = providers; - } +class CompositeEmptyStateProvider( + private vararg val providers: EmptyStateProvider, +) : EmptyStateProvider { - @Nullable - @Override - public EmptyState getEmptyState(ResolverListAdapter resolverListAdapter) { - for (EmptyStateProvider provider : mProviders) { - EmptyState emptyState = provider.getEmptyState(resolverListAdapter); - if (emptyState != null) { - return emptyState; - } - } - return null; + override fun getEmptyState(resolverListAdapter: ResolverListAdapter): EmptyState? { + return providers.firstNotNullOfOrNull { it.getEmptyState(resolverListAdapter) } } } diff --git a/java/src/com/android/intentresolver/emptystate/DefaultEmptyState.kt b/java/src/com/android/intentresolver/emptystate/DefaultEmptyState.kt new file mode 100644 index 00000000..ea1a03cc --- /dev/null +++ b/java/src/com/android/intentresolver/emptystate/DefaultEmptyState.kt @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.intentresolver.emptystate + +class DefaultEmptyState : EmptyState { + override fun useDefaultEmptyView() = true +} diff --git a/java/src/com/android/intentresolver/emptystate/DevicePolicyBlockerEmptyState.java b/java/src/com/android/intentresolver/emptystate/DevicePolicyBlockerEmptyState.java index b627636e..1cbc6175 100644 --- a/java/src/com/android/intentresolver/emptystate/DevicePolicyBlockerEmptyState.java +++ b/java/src/com/android/intentresolver/emptystate/DevicePolicyBlockerEmptyState.java @@ -17,40 +17,26 @@ package com.android.intentresolver.emptystate; import android.app.admin.DevicePolicyEventLogger; -import android.app.admin.DevicePolicyManager; -import android.content.Context; -import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.annotation.StringRes; /** * Empty state that gets strings from the device policy manager and tracks events into * event logger of the device policy events. */ public 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 String mTitle; + private final String mSubtitle; private final int mEventId; - @NonNull private final String mEventCategory; - public DevicePolicyBlockerEmptyState(@NonNull Context context, - String devicePolicyStringTitleId, @StringRes int defaultTitleResource, - String devicePolicyStringSubtitleId, @StringRes int defaultSubtitleResource, - int devicePolicyEventId, @NonNull String devicePolicyEventCategory) { - mContext = context; - mDevicePolicyStringTitleId = devicePolicyStringTitleId; - mDefaultTitleResource = defaultTitleResource; - mDevicePolicyStringSubtitleId = devicePolicyStringSubtitleId; - mDefaultSubtitleResource = defaultSubtitleResource; + public DevicePolicyBlockerEmptyState( + String title, + String subtitle, + int devicePolicyEventId, + String devicePolicyEventCategory) { + mTitle = title; + mSubtitle = subtitle; mEventId = devicePolicyEventId; mEventCategory = devicePolicyEventCategory; } @@ -58,24 +44,22 @@ public class DevicePolicyBlockerEmptyState implements EmptyState { @Nullable @Override public String getTitle() { - return mContext.getSystemService(DevicePolicyManager.class).getResources().getString( - mDevicePolicyStringTitleId, - () -> mContext.getString(mDefaultTitleResource)); + return mTitle; } @Nullable @Override public String getSubtitle() { - return mContext.getSystemService(DevicePolicyManager.class).getResources().getString( - mDevicePolicyStringSubtitleId, - () -> mContext.getString(mDefaultSubtitleResource)); + return mSubtitle; } @Override public void onEmptyStateShown() { - DevicePolicyEventLogger.createEvent(mEventId) - .setStrings(mEventCategory) - .write(); + if (mEventId != -1) { + DevicePolicyEventLogger.createEvent(mEventId) + .setStrings(mEventCategory) + .write(); + } } @Override diff --git a/java/src/com/android/intentresolver/emptystate/NoAppsAvailableEmptyStateProvider.java b/java/src/com/android/intentresolver/emptystate/NoAppsAvailableEmptyStateProvider.java index cd1448e4..b3d3e343 100644 --- a/java/src/com/android/intentresolver/emptystate/NoAppsAvailableEmptyStateProvider.java +++ b/java/src/com/android/intentresolver/emptystate/NoAppsAvailableEmptyStateProvider.java @@ -70,13 +70,4 @@ public class NoAppsAvailableEmptyStateProvider implements EmptyStateProvider { ); } } - - - public static class DefaultEmptyState implements EmptyState { - @Override - public boolean useDefaultEmptyView() { - return true; - } - } - } diff --git a/java/src/com/android/intentresolver/emptystate/NoCrossProfileEmptyStateProvider.java b/java/src/com/android/intentresolver/emptystate/NoCrossProfileEmptyStateProvider.java index fa33928b..0cf2ea45 100644 --- a/java/src/com/android/intentresolver/emptystate/NoCrossProfileEmptyStateProvider.java +++ b/java/src/com/android/intentresolver/emptystate/NoCrossProfileEmptyStateProvider.java @@ -16,15 +16,21 @@ package com.android.intentresolver.emptystate; +import static android.stats.devicepolicy.DevicePolicyEnums.RESOLVER_EMPTY_STATE_NO_SHARING_TO_PERSONAL; +import static android.stats.devicepolicy.DevicePolicyEnums.RESOLVER_EMPTY_STATE_NO_SHARING_TO_WORK; + +import static com.android.intentresolver.ChooserActivity.METRICS_CATEGORY_CHOOSER; + +import static java.util.Objects.requireNonNull; + import android.content.Intent; -import android.os.UserHandle; import androidx.annotation.Nullable; import com.android.intentresolver.ProfileHelper; import com.android.intentresolver.ResolverListAdapter; +import com.android.intentresolver.data.repository.DevicePolicyResources; import com.android.intentresolver.shared.model.Profile; -import com.android.intentresolver.shared.model.User; import java.util.List; @@ -35,55 +41,78 @@ import java.util.List; public class NoCrossProfileEmptyStateProvider implements EmptyStateProvider { private final ProfileHelper mProfileHelper; - private final EmptyState mNoWorkToPersonalEmptyState; - private final EmptyState mNoPersonalToWorkEmptyState; + private final DevicePolicyResources mDevicePolicyResources; + private final boolean mIsShare; private final CrossProfileIntentsChecker mCrossProfileIntentsChecker; public NoCrossProfileEmptyStateProvider( ProfileHelper profileHelper, - EmptyState noWorkToPersonalEmptyState, - EmptyState noPersonalToWorkEmptyState, - CrossProfileIntentsChecker crossProfileIntentsChecker) { + DevicePolicyResources devicePolicyResources, + CrossProfileIntentsChecker crossProfileIntentsChecker, + boolean isShare) { mProfileHelper = profileHelper; - mNoWorkToPersonalEmptyState = noWorkToPersonalEmptyState; - mNoPersonalToWorkEmptyState = noPersonalToWorkEmptyState; + mDevicePolicyResources = devicePolicyResources; + mIsShare = isShare; mCrossProfileIntentsChecker = crossProfileIntentsChecker; } - private boolean anyCrossProfileAllowedIntents(ResolverListAdapter selected, UserHandle source) { - List<Intent> intents = selected.getIntents(); - UserHandle target = selected.getUserHandle(); + private boolean hasCrossProfileIntents(List<Intent> intents, Profile source, Profile target) { + if (source.getPrimary().getHandle().equals(target.getPrimary().getHandle())) { + return true; + } + // Note: Use of getPrimary() here also handles delegation of CLONE profile to parent. return mCrossProfileIntentsChecker.hasCrossProfileIntents(intents, - source.getIdentifier(), target.getIdentifier()); + source.getPrimary().getId(), target.getPrimary().getId()); } @Nullable @Override public EmptyState getEmptyState(ResolverListAdapter adapter) { - Profile launchedAsProfile = mProfileHelper.getLaunchedAsProfile(); - User launchedAs = mProfileHelper.getLaunchedAsProfile().getPrimary(); - UserHandle tabOwnerHandle = adapter.getUserHandle(); - boolean launchedAsSameUser = launchedAs.getHandle().equals(tabOwnerHandle); - Profile.Type tabOwnerType = mProfileHelper.findProfileType(tabOwnerHandle); - - // Not applicable for private profile. - if (launchedAsProfile.getType() == Profile.Type.PRIVATE - || tabOwnerType == Profile.Type.PRIVATE) { - return null; + Profile launchedBy = mProfileHelper.getLaunchedAsProfile(); + Profile tabOwner = requireNonNull(mProfileHelper.findProfile(adapter.getUserHandle())); + + // When sharing into or out of Private profile, perform the check using the parent profile + // instead. (Hard-coded application of CROSS_PROFILE_CONTENT_SHARING_DELEGATE_FROM_PARENT) + + Profile effectiveSource = launchedBy; + Profile effectiveTarget = tabOwner; + + // Assumption baked into design: "Personal" profile is the parent of all other profiles. + if (launchedBy.getType() == Profile.Type.PRIVATE) { + effectiveSource = mProfileHelper.getPersonalProfile(); + } + + if (tabOwner.getType() == Profile.Type.PRIVATE) { + effectiveTarget = mProfileHelper.getPersonalProfile(); } - // Allow access to the tab when launched by the same user as the tab owner - // or when there is at least one target which is permitted for cross-profile. - if (launchedAsSameUser || anyCrossProfileAllowedIntents(adapter, - /* source = */ launchedAs.getHandle())) { + // Allow access to the tab when there is at least one target permitted to cross profiles. + if (hasCrossProfileIntents(adapter.getIntents(), effectiveSource, effectiveTarget)) { return null; } - switch (launchedAsProfile.getType()) { - case WORK: return mNoWorkToPersonalEmptyState; - case PERSONAL: return mNoPersonalToWorkEmptyState; + switch (tabOwner.getType()) { + case PERSONAL: + return new DevicePolicyBlockerEmptyState( + mDevicePolicyResources.getCrossProfileBlocked(), + mDevicePolicyResources.toPersonalBlockedByPolicyMessage(mIsShare), + RESOLVER_EMPTY_STATE_NO_SHARING_TO_PERSONAL, + METRICS_CATEGORY_CHOOSER); + + case WORK: + return new DevicePolicyBlockerEmptyState( + mDevicePolicyResources.getCrossProfileBlocked(), + mDevicePolicyResources.toWorkBlockedByPolicyMessage(mIsShare), + RESOLVER_EMPTY_STATE_NO_SHARING_TO_WORK, + METRICS_CATEGORY_CHOOSER); + + case PRIVATE: + return new DevicePolicyBlockerEmptyState( + mDevicePolicyResources.getCrossProfileBlocked(), + mDevicePolicyResources.toPrivateBlockedByPolicyMessage(mIsShare), + /* Suppress log event. TODO: Define a new metrics event for this? */ -1, + METRICS_CATEGORY_CHOOSER); } return null; } - } diff --git a/tests/unit/src/com/android/intentresolver/emptystate/NoCrossProfileEmptyStateProviderTest.kt b/tests/unit/src/com/android/intentresolver/emptystate/NoCrossProfileEmptyStateProviderTest.kt index fe3e844b..135ac064 100644 --- a/tests/unit/src/com/android/intentresolver/emptystate/NoCrossProfileEmptyStateProviderTest.kt +++ b/tests/unit/src/com/android/intentresolver/emptystate/NoCrossProfileEmptyStateProviderTest.kt @@ -20,34 +20,35 @@ import android.content.Intent import com.android.intentresolver.ProfileHelper import com.android.intentresolver.ResolverListAdapter import com.android.intentresolver.annotation.JavaInterop +import com.android.intentresolver.data.repository.DevicePolicyResources import com.android.intentresolver.data.repository.FakeUserRepository import com.android.intentresolver.domain.interactor.UserInteractor import com.android.intentresolver.inject.FakeIntentResolverFlags import com.android.intentresolver.shared.model.User import com.google.common.truth.Truth.assertThat +import com.google.common.truth.Truth.assertWithMessage import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import org.junit.Test -import org.mockito.ArgumentMatchers.anyInt -import org.mockito.ArgumentMatchers.anyList +import org.mockito.Mockito.never +import org.mockito.kotlin.any import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.doReturn import org.mockito.kotlin.mock -import org.mockito.kotlin.never import org.mockito.kotlin.same import org.mockito.kotlin.times import org.mockito.kotlin.verify +import org.mockito.verification.VerificationMode @OptIn(JavaInterop::class) class NoCrossProfileEmptyStateProviderTest { private val personalUser = User(0, User.Role.PERSONAL) private val workUser = User(10, User.Role.WORK) + private val privateUser = User(11, User.Role.PRIVATE) private val flags = FakeIntentResolverFlags() - private val personalBlocker = mock<EmptyState>() - private val workBlocker = mock<EmptyState>() - private val userRepository = FakeUserRepository(listOf(personalUser, workUser)) + private val userRepository = FakeUserRepository(listOf(personalUser, workUser, privateUser)) private val personalIntents = listOf(Intent("PERSONAL")) private val personalListAdapter = @@ -61,96 +62,169 @@ class NoCrossProfileEmptyStateProviderTest { on { userHandle } doReturn workUser.handle on { intents } doReturn workIntents } + private val privateIntents = listOf(Intent("PRIVATE")) + private val privateListAdapter = + mock<ResolverListAdapter> { + on { userHandle } doReturn privateUser.handle + on { intents } doReturn privateIntents + } + + private val devicePolicyResources = + mock<DevicePolicyResources> { + on { crossProfileBlocked } doReturn "Cross profile blocked" + on { toPersonalBlockedByPolicyMessage(any()) } doReturn "Blocked to Personal" + on { toWorkBlockedByPolicyMessage(any()) } doReturn "Blocked to Work" + on { toPrivateBlockedByPolicyMessage(any()) } doReturn "Blocked to Private" + } - // Pretend that no intent can ever be forwarded - val crossProfileIntentsChecker = + // If asked, no intent can ever be forwarded between any pair of users. + private val crossProfileIntentsChecker = mock<CrossProfileIntentsChecker> { on { hasCrossProfileIntents( - /* intents = */ anyList(), - /* source = */ anyInt(), - /* target = */ anyInt() + /* intents = */ any(), + /* source = */ any(), + /* target = */ any() ) - } doReturn false + } doReturn false /* Never allow */ } - private val sourceUserId = argumentCaptor<Int>() - private val targetUserId = argumentCaptor<Int>() @Test - fun testPersonalToWork() { - val userInteractor = UserInteractor(userRepository, launchedAs = personalUser.handle) - - val profileHelper = - ProfileHelper( - userInteractor, - CoroutineScope(Dispatchers.Unconfined), - Dispatchers.Unconfined, - flags - ) + fun verifyTestSetup() { + assertThat(workListAdapter.userHandle).isEqualTo(workUser.handle) + assertThat(personalListAdapter.userHandle).isEqualTo(personalUser.handle) + assertThat(privateListAdapter.userHandle).isEqualTo(privateUser.handle) + } + + @Test + fun sameProfilePermitted() { + val profileHelper = createProfileHelper(launchedAs = workUser) val provider = NoCrossProfileEmptyStateProvider( - /* profileHelper = */ profileHelper, - /* noWorkToPersonalEmptyState = */ personalBlocker, - /* noPersonalToWorkEmptyState = */ workBlocker, - /* crossProfileIntentsChecker = */ crossProfileIntentsChecker + profileHelper, + devicePolicyResources, + crossProfileIntentsChecker, + /* isShare = */ true ) - // Personal to personal, not blocked - assertThat(provider.getEmptyState(personalListAdapter)).isNull() - // Not called because sourceUser == targetUser - verify(crossProfileIntentsChecker, never()) - .hasCrossProfileIntents(anyList(), anyInt(), anyInt()) - - // Personal to work, blocked - assertThat(provider.getEmptyState(workListAdapter)).isSameInstanceAs(workBlocker) - - verify(crossProfileIntentsChecker, times(1)) - .hasCrossProfileIntents( - same(workIntents), - sourceUserId.capture(), - targetUserId.capture() + // Work to work, not blocked + assertThat(provider.getEmptyState(workListAdapter)).isNull() + + crossProfileIntentsChecker.verifyCalled(never()) + } + + @Test + fun testPersonalToWork() { + val profileHelper = createProfileHelper(launchedAs = personalUser) + + val provider = + NoCrossProfileEmptyStateProvider( + profileHelper, + devicePolicyResources, + crossProfileIntentsChecker, + /* isShare = */ true ) - assertThat(sourceUserId.firstValue).isEqualTo(personalUser.id) - assertThat(targetUserId.firstValue).isEqualTo(workUser.id) + + val result = provider.getEmptyState(workListAdapter) + assertThat(result).isNotNull() + assertThat(result?.title).isEqualTo("Cross profile blocked") + assertThat(result?.subtitle).isEqualTo("Blocked to Work") + + crossProfileIntentsChecker.verifyCalled(times(1), workIntents, personalUser, workUser) } @Test fun testWorkToPersonal() { - val userInteractor = UserInteractor(userRepository, launchedAs = workUser.handle) - - val profileHelper = - ProfileHelper( - userInteractor, - CoroutineScope(Dispatchers.Unconfined), - Dispatchers.Unconfined, - flags + val profileHelper = createProfileHelper(launchedAs = workUser) + + val provider = + NoCrossProfileEmptyStateProvider( + profileHelper, + devicePolicyResources, + crossProfileIntentsChecker, + /* isShare = */ true ) + val result = provider.getEmptyState(personalListAdapter) + assertThat(result).isNotNull() + assertThat(result?.title).isEqualTo("Cross profile blocked") + assertThat(result?.subtitle).isEqualTo("Blocked to Personal") + + crossProfileIntentsChecker.verifyCalled(times(1), personalIntents, workUser, personalUser) + } + + @Test + fun testWorkToPrivate() { + val profileHelper = createProfileHelper(launchedAs = workUser) + val provider = NoCrossProfileEmptyStateProvider( - /* profileHelper = */ profileHelper, - /* noWorkToPersonalEmptyState = */ personalBlocker, - /* noPersonalToWorkEmptyState = */ workBlocker, - /* crossProfileIntentsChecker = */ crossProfileIntentsChecker + profileHelper, + devicePolicyResources, + crossProfileIntentsChecker, + /* isShare = */ true ) - // Work to work, not blocked - assertThat(provider.getEmptyState(workListAdapter)).isNull() - // Not called because sourceUser == targetUser - verify(crossProfileIntentsChecker, never()) - .hasCrossProfileIntents(anyList(), anyInt(), anyInt()) - - // Work to personal, blocked - assertThat(provider.getEmptyState(personalListAdapter)).isSameInstanceAs(personalBlocker) - - verify(crossProfileIntentsChecker, times(1)) - .hasCrossProfileIntents( - same(personalIntents), - sourceUserId.capture(), - targetUserId.capture() + val result = provider.getEmptyState(privateListAdapter) + assertThat(result).isNotNull() + assertThat(result?.title).isEqualTo("Cross profile blocked") + assertThat(result?.subtitle).isEqualTo("Blocked to Private") + + // effective target user is personalUser due to "delegate from parent" + crossProfileIntentsChecker.verifyCalled(times(1), privateIntents, workUser, personalUser) + } + + @Test + fun testPrivateToPersonal() { + val profileHelper = createProfileHelper(launchedAs = privateUser) + + val provider = + NoCrossProfileEmptyStateProvider( + profileHelper, + devicePolicyResources, + crossProfileIntentsChecker, + /* isShare = */ true ) - assertThat(sourceUserId.firstValue).isEqualTo(workUser.id) - assertThat(targetUserId.firstValue).isEqualTo(personalUser.id) + + // Private -> Personal is always allowed: + // Private delegates to the parent profile for policy; so personal->personal is allowed. + assertThat(provider.getEmptyState(personalListAdapter)).isNull() + + crossProfileIntentsChecker.verifyCalled(never()) + } + + private fun createProfileHelper(launchedAs: User): ProfileHelper { + val userInteractor = UserInteractor(userRepository, launchedAs = launchedAs.handle) + + return ProfileHelper( + userInteractor, + CoroutineScope(Dispatchers.Unconfined), + Dispatchers.Unconfined, + flags + ) + } + + private fun CrossProfileIntentsChecker.verifyCalled( + mode: VerificationMode, + list: List<Intent>? = null, + sourceUser: User? = null, + targetUser: User? = null, + ) { + val sourceUserId = argumentCaptor<Int>() + val targetUserId = argumentCaptor<Int>() + + verify(this, mode) + .hasCrossProfileIntents(same(list), sourceUserId.capture(), targetUserId.capture()) + sourceUser?.apply { + assertWithMessage("hasCrossProfileIntents: source") + .that(sourceUserId.firstValue) + .isEqualTo(id) + } + targetUser?.apply { + assertWithMessage("hasCrossProfileIntents: target") + .that(targetUserId.firstValue) + .isEqualTo(id) + } } } |