summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--java/res/values/strings.xml6
-rw-r--r--java/src/com/android/intentresolver/ChooserActivity.java45
-rw-r--r--java/src/com/android/intentresolver/ProfileHelper.kt8
-rw-r--r--java/src/com/android/intentresolver/ResolverActivity.java41
-rw-r--r--java/src/com/android/intentresolver/data/repository/DevicePolicyResources.kt22
-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.kt20
-rw-r--r--java/src/com/android/intentresolver/emptystate/DevicePolicyBlockerEmptyState.java48
-rw-r--r--java/src/com/android/intentresolver/emptystate/NoAppsAvailableEmptyStateProvider.java9
-rw-r--r--java/src/com/android/intentresolver/emptystate/NoCrossProfileEmptyStateProvider.java91
-rw-r--r--tests/unit/src/com/android/intentresolver/emptystate/NoCrossProfileEmptyStateProviderTest.kt218
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)
+ }
}
}