diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 3cabb0e428f..67859666a7f 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -2471,6 +2471,28 @@ android:value="com.android.settings.applications.specialaccess.pictureinpicture.PictureInPictureDetails" /> + + + + + + + + + + + + + + + + + App Details > Picture-in-picture > Description. [CHAR LIMIT=NONE] --> Allow this app to create a picture-in-picture window while the app is open or after you leave it (for example, to continue watching a video). This window displays on top of other apps you\'re using. + + Connected work and personal apps + + + No connected apps + + + cross profile connected app apps work and personal + + + Connected work and personal apps + + + Connect these apps + + + Connected apps share permissions and can access each other\u2019s data. + + + Only connect apps that you trust with your personal data.Your data may be exposed to your IT admin. + + + + Trust work Calendar with your personal data? + + + + Calendar may expose your personal data to your IT admin + Do Not Disturb access diff --git a/res/xml/app_info_settings.xml b/res/xml/app_info_settings.xml index 435a7ef6357..a76f0d94206 100644 --- a/res/xml/app_info_settings.xml +++ b/res/xml/app_info_settings.xml @@ -150,6 +150,12 @@ android:summary="@string/summary_placeholder" settings:controller="com.android.settings.applications.appinfo.ExternalSourceDetailPreferenceController" /> + + diff --git a/res/xml/interact_across_profiles.xml b/res/xml/interact_across_profiles.xml new file mode 100644 index 00000000000..6fd18854c39 --- /dev/null +++ b/res/xml/interact_across_profiles.xml @@ -0,0 +1,23 @@ + + + + + diff --git a/res/xml/interact_across_profiles_permissions_details.xml b/res/xml/interact_across_profiles_permissions_details.xml new file mode 100644 index 00000000000..e9a48038a6d --- /dev/null +++ b/res/xml/interact_across_profiles_permissions_details.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + diff --git a/res/xml/special_access.xml b/res/xml/special_access.xml index e511d178682..c033e17fa6d 100644 --- a/res/xml/special_access.xml +++ b/res/xml/special_access.xml @@ -154,6 +154,13 @@ android:value="com.android.settings.Settings$ChangeWifiStateActivity" /> + + profiles = mUserManager.getProfiles(UserHandle.myUserId()); + for (final UserInfo userInfo : profiles) { + if (userInfo.isManagedProfile()) { + return AVAILABLE; + } + } + return DISABLED_FOR_USER; + } +} diff --git a/src/com/android/settings/applications/specialaccess/interactacrossprofiles/InteractAcrossProfilesDetails.java b/src/com/android/settings/applications/specialaccess/interactacrossprofiles/InteractAcrossProfilesDetails.java new file mode 100644 index 00000000000..ad40d7095eb --- /dev/null +++ b/src/com/android/settings/applications/specialaccess/interactacrossprofiles/InteractAcrossProfilesDetails.java @@ -0,0 +1,201 @@ +/* + * Copyright (C) 2020 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.settings.applications.specialaccess.interactacrossprofiles; + +import android.Manifest; +import android.app.AppOpsManager; +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.content.DialogInterface; +import android.content.PermissionChecker; +import android.content.pm.CrossProfileApps; +import android.content.pm.UserInfo; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.os.UserHandle; +import android.os.UserManager; +import android.util.IconDrawableFactory; +import android.widget.ImageView; + +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import androidx.preference.Preference; +import androidx.preference.SwitchPreference; + +import com.android.settings.R; +import com.android.settings.applications.AppInfoBase; +import com.android.settingslib.widget.LayoutPreference; + +public class InteractAcrossProfilesDetails extends AppInfoBase + implements Preference.OnPreferenceClickListener { + + private static final String INTERACT_ACROSS_PROFILES_SETTINGS_SWITCH = + "interact_across_profiles_settings_switch"; + private static final String INTERACT_ACROSS_PROFILES_HEADER = "interact_across_profiles_header"; + + private Context mContext; + private CrossProfileApps mCrossProfileApps; + private UserManager mUserManager; + private SwitchPreference mSwitchPref; + private LayoutPreference mHeader; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + mContext = getContext(); + mCrossProfileApps = mContext.getSystemService(CrossProfileApps.class); + mUserManager = mContext.getSystemService(UserManager.class); + + addPreferencesFromResource(R.xml.interact_across_profiles_permissions_details); + mSwitchPref = findPreference(INTERACT_ACROSS_PROFILES_SETTINGS_SWITCH); + mSwitchPref.setOnPreferenceClickListener(this); + mHeader = findPreference(INTERACT_ACROSS_PROFILES_HEADER); + + // refreshUi checks that the user can still configure the appOp, return to the + // previous page if it can't. + if (!refreshUi()) { + setIntentAndFinish(true/* appChanged */); + } + final UserHandle workProfile = getWorkProfile(); + final UserHandle personalProfile = mUserManager.getProfileParent(workProfile); + addAppIcons(personalProfile, workProfile); + } + + private void addAppIcons(UserHandle personalProfile, UserHandle workProfile) { + final ImageView personalIconView = mHeader.findViewById(R.id.entity_header_icon_personal); + if (personalIconView != null) { + personalIconView.setImageDrawable(IconDrawableFactory.newInstance(mContext) + .getBadgedIcon(mPackageInfo.applicationInfo, personalProfile.getIdentifier())); + } + final ImageView workIconView2 = mHeader.findViewById(R.id.entity_header_icon_work); + if (workIconView2 != null) { + workIconView2.setImageDrawable(IconDrawableFactory.newInstance(mContext) + .getBadgedIcon(mPackageInfo.applicationInfo, workProfile.getIdentifier())); + } + } + + @Nullable + private UserHandle getWorkProfile() { + for (UserInfo user : mUserManager.getProfiles(UserHandle.myUserId())) { + if (mUserManager.isManagedProfile(user.id)) { + return user.getUserHandle(); + } + } + return null; + } + + @Override + public boolean onPreferenceClick(Preference preference) { + if (preference != mSwitchPref) { + return false; + } + // refreshUi checks that the user can still configure the appOp, return to the + // previous page if it can't. + if (!refreshUi()) { + setIntentAndFinish(true/* appChanged */); + } + if (isInteractAcrossProfilesEnabled()) { + enableInteractAcrossProfiles(false); + refreshUi(); + return true; + } + if (!isInteractAcrossProfilesEnabled()) { + // TODO(b/148594054): Create a proper dialogue. + new AlertDialog.Builder(getActivity()) + .setTitle(R.string.interact_across_profiles_consent_dialog_title) + .setMessage(R.string.interact_across_profiles_consent_dialog_summary) + .setPositiveButton(R.string.allow, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + enableInteractAcrossProfiles(true); + refreshUi(); + } + }) + .setNegativeButton(R.string.deny, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + refreshUi(); + } + }) + .create().show(); + } else { + enableInteractAcrossProfiles(false); + refreshUi(); + } + return true; + } + + private boolean isInteractAcrossProfilesEnabled() { + return isInteractAcrossProfilesEnabled( + mContext, mPackageName, mPackageInfo.applicationInfo.uid); + } + + private static boolean isInteractAcrossProfilesEnabled(Context context, String packageName, int uid) { + return PermissionChecker.PERMISSION_GRANTED + == PermissionChecker.checkPermissionForPreflight( + context, + Manifest.permission.INTERACT_ACROSS_PROFILES, + PermissionChecker.PID_UNKNOWN, + uid, + packageName); + } + + private void enableInteractAcrossProfiles(boolean newState) { + mCrossProfileApps.setInteractAcrossProfilesAppOp( + mPackageName, newState ? AppOpsManager.MODE_ALLOWED : AppOpsManager.MODE_IGNORED); + } + + /** + * @return the summary for the current state of whether the app associated with the given + * {@code packageName} is allowed to interact across profiles. + */ + public static CharSequence getPreferenceSummary(Context context, String packageName, int uid) { + return context.getString(isInteractAcrossProfilesEnabled(context, packageName, uid) + ? R.string.app_permission_summary_allowed + : R.string.app_permission_summary_not_allowed); + } + + @Override + protected boolean refreshUi() { + if (mPackageInfo == null || mPackageInfo.applicationInfo == null) { + return false; + } + if (!mCrossProfileApps.canConfigureInteractAcrossProfiles(mPackageName)) { + // Invalid app entry. Should not allow changing permission + mSwitchPref.setEnabled(false); + return false; + } + + mSwitchPref.setChecked(isInteractAcrossProfilesEnabled()); + final ImageView horizontalArrowIcon = mHeader.findViewById(R.id.entity_header_swap_horiz); + if (horizontalArrowIcon != null) { + final Drawable icon = mSwitchPref.isChecked() + ? mContext.getDrawable(R.drawable.ic_swap_horiz_blue) + : mContext.getDrawable(R.drawable.ic_swap_horiz_grey); + horizontalArrowIcon.setImageDrawable(icon); + } + return true; + } + + @Override + protected AlertDialog createDialog(int id, int errorCode) { + return null; + } + + @Override + public int getMetricsCategory() { + return SettingsEnums.INTERACT_ACROSS_PROFILES; + } +} diff --git a/src/com/android/settings/applications/specialaccess/interactacrossprofiles/InteractAcrossProfilesDetailsPreferenceController.java b/src/com/android/settings/applications/specialaccess/interactacrossprofiles/InteractAcrossProfilesDetailsPreferenceController.java new file mode 100644 index 00000000000..41e25a7d1d0 --- /dev/null +++ b/src/com/android/settings/applications/specialaccess/interactacrossprofiles/InteractAcrossProfilesDetailsPreferenceController.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2020 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.settings.applications.specialaccess.interactacrossprofiles; + +import android.content.Context; +import android.content.pm.CrossProfileApps; + +import androidx.preference.Preference; + +import com.android.settings.SettingsPreferenceFragment; +import com.android.settings.applications.appinfo.AppInfoPreferenceControllerBase; + +public class InteractAcrossProfilesDetailsPreferenceController + extends AppInfoPreferenceControllerBase { + + private String mPackageName; + + public InteractAcrossProfilesDetailsPreferenceController(Context context, String key) { + super(context, key); + } + + @Override + public int getAvailabilityStatus() { + return canConfigureInteractAcrossProfiles() ? AVAILABLE : DISABLED_FOR_USER; + } + + @Override + public void updateState(Preference preference) { + preference.setSummary(getPreferenceSummary()); + } + + @Override + protected Class getDetailFragmentClass() { + return InteractAcrossProfilesDetails.class; + } + + private CharSequence getPreferenceSummary() { + return InteractAcrossProfilesDetails.getPreferenceSummary(mContext, mPackageName, + mParent.getPackageInfo().applicationInfo.uid); + } + + private boolean canConfigureInteractAcrossProfiles() { + return mContext.getSystemService(CrossProfileApps.class) + .canConfigureInteractAcrossProfiles(mPackageName); + } + + public void setPackageName(String packageName) { + mPackageName = packageName; + } +} diff --git a/src/com/android/settings/applications/specialaccess/interactacrossprofiles/InteractAcrossProfilesSettings.java b/src/com/android/settings/applications/specialaccess/interactacrossprofiles/InteractAcrossProfilesSettings.java new file mode 100644 index 00000000000..2fd1e9fca5c --- /dev/null +++ b/src/com/android/settings/applications/specialaccess/interactacrossprofiles/InteractAcrossProfilesSettings.java @@ -0,0 +1,161 @@ +/* + * Copyright (C) 2020 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.settings.applications.specialaccess.interactacrossprofiles; + +import static android.content.pm.PackageManager.GET_ACTIVITIES; + +import android.annotation.Nullable; +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.CrossProfileApps; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.UserInfo; +import android.os.Bundle; +import android.os.UserHandle; +import android.os.UserManager; +import android.util.IconDrawableFactory; +import android.util.Pair; +import android.view.View; + +import androidx.preference.Preference; +import androidx.preference.Preference.OnPreferenceClickListener; +import androidx.preference.PreferenceScreen; + +import com.android.settings.R; +import com.android.settings.applications.AppInfoBase; +import com.android.settings.search.BaseSearchIndexProvider; +import com.android.settings.widget.EmptyTextSettings; +import com.android.settingslib.search.SearchIndexable; +import com.android.settingslib.widget.apppreference.AppPreference; + +import java.util.ArrayList; +import java.util.List; + +@SearchIndexable +public class InteractAcrossProfilesSettings extends EmptyTextSettings { + private Context mContext; + private PackageManager mPackageManager; + private UserManager mUserManager; + private CrossProfileApps mCrossProfileApps; + private IconDrawableFactory mIconDrawableFactory; + + @Override + public void onCreate(Bundle icicle) { + super.onCreate(icicle); + + mContext = getContext(); + mPackageManager = mContext.getPackageManager(); + mUserManager = mContext.getSystemService(UserManager.class); + mIconDrawableFactory = IconDrawableFactory.newInstance(mContext); + mCrossProfileApps = mContext.getSystemService(CrossProfileApps.class); + } + + @Override + public void onResume() { + super.onResume(); + + final PreferenceScreen screen = getPreferenceScreen(); + screen.removeAll(); + + final ArrayList> crossProfileApps = + collectConfigurableApps(); + + final Context prefContext = getPrefContext(); + for (final Pair appData : crossProfileApps) { + final ApplicationInfo appInfo = appData.first; + final UserHandle user = appData.second; + final String packageName = appInfo.packageName; + final CharSequence label = appInfo.loadLabel(mPackageManager); + + final Preference pref = new AppPreference(prefContext); + pref.setIcon(mIconDrawableFactory.getBadgedIcon(appInfo, user.getIdentifier())); + pref.setTitle(mPackageManager.getUserBadgedLabel(label, user)); + pref.setSummary(InteractAcrossProfilesDetails.getPreferenceSummary(prefContext, + packageName, appInfo.uid)); + pref.setOnPreferenceClickListener(new OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + AppInfoBase.startAppInfoFragment(InteractAcrossProfilesDetails.class, + R.string.interact_across_profiles_title, + packageName, + appInfo.uid, + InteractAcrossProfilesSettings.this/* source */, + -1/* request */, + getMetricsCategory()); + return true; + } + }); + screen.addPreference(pref); + } + } + + @Override + public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + setEmptyText(R.string.interact_across_profiles_empty_text); + } + + @Override + protected int getPreferenceScreenResId() { + return R.xml.interact_across_profiles; + } + + @Override + public int getMetricsCategory() { + return SettingsEnums.INTERACT_ACROSS_PROFILES; + } + + /** + * @return the list of applications for the personal profile in the calling user's profile group + * that can configure interact across profiles. + */ + ArrayList> collectConfigurableApps() { + final UserHandle personalProfile = getPersonalProfileForCallingUser(); + if (personalProfile == null) { + return new ArrayList<>(); + } + + final ArrayList> crossProfileApps = new ArrayList<>(); + final List installedPackages = mPackageManager.getInstalledPackagesAsUser( + GET_ACTIVITIES, personalProfile.getIdentifier()); + for (PackageInfo packageInfo : installedPackages) { + if (mCrossProfileApps.canConfigureInteractAcrossProfiles(packageInfo.packageName)) { + crossProfileApps.add(new Pair<>(packageInfo.applicationInfo, personalProfile)); + } + } + return crossProfileApps; + } + + /** + * Returns the personal profile in the profile group of the calling user. + * Returns null if user is not in a profile group. + */ + @Nullable + private UserHandle getPersonalProfileForCallingUser() { + final int callingUser = UserHandle.myUserId(); + if (mUserManager.getProfiles(callingUser).isEmpty()) { + return null; + } + final UserInfo parentProfile = mUserManager.getProfileParent(callingUser); + return parentProfile == null + ? UserHandle.of(callingUser) : parentProfile.getUserHandle(); + } + + public static final BaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = + new BaseSearchIndexProvider(R.xml.interact_across_profiles); +} diff --git a/src/com/android/settings/core/gateway/SettingsGateway.java b/src/com/android/settings/core/gateway/SettingsGateway.java index 5ad8bb2d285..ae817e7bca5 100644 --- a/src/com/android/settings/core/gateway/SettingsGateway.java +++ b/src/com/android/settings/core/gateway/SettingsGateway.java @@ -46,6 +46,8 @@ import com.android.settings.applications.assist.ManageAssist; import com.android.settings.applications.manageapplications.ManageApplications; import com.android.settings.applications.managedomainurls.ManageDomainUrls; import com.android.settings.applications.specialaccess.deviceadmin.DeviceAdminSettings; +import com.android.settings.applications.specialaccess.interactacrossprofiles.InteractAcrossProfilesDetails; +import com.android.settings.applications.specialaccess.interactacrossprofiles.InteractAcrossProfilesSettings; import com.android.settings.applications.specialaccess.notificationaccess.NotificationAccessDetails; import com.android.settings.applications.specialaccess.pictureinpicture.PictureInPictureDetails; import com.android.settings.applications.specialaccess.pictureinpicture.PictureInPictureSettings; @@ -293,7 +295,9 @@ public class SettingsGateway { GlobalActionsPanelSettings.class.getName(), DarkModeSettingsFragment.class.getName(), BugReportHandlerPicker.class.getName(), - GestureNavigationSettingsFragment.class.getName() + GestureNavigationSettingsFragment.class.getName(), + InteractAcrossProfilesSettings.class.getName(), + InteractAcrossProfilesDetails.class.getName() }; public static final String[] SETTINGS_FOR_RESTRICTED = { diff --git a/tests/robotests/src/com/android/settings/applications/specialaccess/interactacrossprofiles/InteractAcrossProfilesControllerTest.java b/tests/robotests/src/com/android/settings/applications/specialaccess/interactacrossprofiles/InteractAcrossProfilesControllerTest.java new file mode 100644 index 00000000000..730a3ccae8f --- /dev/null +++ b/tests/robotests/src/com/android/settings/applications/specialaccess/interactacrossprofiles/InteractAcrossProfilesControllerTest.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2020 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.settings.applications.specialaccess.interactacrossprofiles; + +import static com.google.common.truth.Truth.assertThat; + +import static org.robolectric.Shadows.shadowOf; + +import android.content.Context; +import android.content.pm.UserInfo; +import android.os.UserManager; + +import androidx.test.core.app.ApplicationProvider; + +import com.android.settings.core.BasePreferenceController; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +@RunWith(RobolectricTestRunner.class) +public class InteractAcrossProfilesControllerTest { + private static final int PERSONAL_PROFILE_ID = 0; + private static final int WORK_PROFILE_ID = 10; + + private final Context mContext = ApplicationProvider.getApplicationContext(); + private final UserManager mUserManager = mContext.getSystemService(UserManager.class); + private final InteractAcrossProfilesController mController = + new InteractAcrossProfilesController(mContext, "test_key"); + + @Test + public void getAvailabilityStatus_multipleProfiles_returnsAvailable() { + shadowOf(mUserManager).addUser( + PERSONAL_PROFILE_ID, "personal-profile"/* name */, 0/* flags */); + shadowOf(mUserManager).addProfile( + PERSONAL_PROFILE_ID, + WORK_PROFILE_ID, + "work-profile"/* profileName */, + UserInfo.FLAG_MANAGED_PROFILE); + + assertThat(mController.getAvailabilityStatus()) + .isEqualTo(BasePreferenceController.AVAILABLE); + } + + @Test + public void getAvailabilityStatus_oneProfile_returnsDisabled() { + shadowOf(mUserManager).addUser( + PERSONAL_PROFILE_ID, "personal-profile"/* name */, 0/* flags */); + + assertThat(mController.getAvailabilityStatus()) + .isEqualTo(BasePreferenceController.DISABLED_FOR_USER); + } +} diff --git a/tests/robotests/src/com/android/settings/applications/specialaccess/interactacrossprofiles/InteractAcrossProfilesDetailsTest.java b/tests/robotests/src/com/android/settings/applications/specialaccess/interactacrossprofiles/InteractAcrossProfilesDetailsTest.java new file mode 100644 index 00000000000..9e48b9ea3bb --- /dev/null +++ b/tests/robotests/src/com/android/settings/applications/specialaccess/interactacrossprofiles/InteractAcrossProfilesDetailsTest.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2020 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.settings.applications.specialaccess.interactacrossprofiles; + +import static com.google.common.truth.Truth.assertThat; + +import static org.robolectric.Shadows.shadowOf; + +import android.app.AppOpsManager; +import android.content.Context; +import android.content.pm.PackageManager; +import android.content.pm.PermissionInfo; +import android.os.Process; + +import androidx.test.core.app.ApplicationProvider; + +import com.android.settings.R; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +@RunWith(RobolectricTestRunner.class) +public class InteractAcrossProfilesDetailsTest { + + private static final String CROSS_PROFILE_PACKAGE_NAME = "crossProfilePackage"; + public static final String INTERACT_ACROSS_PROFILES_PERMISSION = + "android.permission.INTERACT_ACROSS_PROFILES"; + + private final Context mContext = ApplicationProvider.getApplicationContext(); + private final AppOpsManager mAppOpsManager = mContext.getSystemService(AppOpsManager.class); + private final PackageManager mPackageManager = mContext.getPackageManager(); + private final InteractAcrossProfilesDetails mFragment = new InteractAcrossProfilesDetails(); + + @Test + public void getPreferenceSummary_appOpAllowed_returnsAllowed() { + String appOp = AppOpsManager.permissionToOp(INTERACT_ACROSS_PROFILES_PERMISSION); + shadowOf(mAppOpsManager).setMode( + appOp, Process.myUid(), CROSS_PROFILE_PACKAGE_NAME, AppOpsManager.MODE_ALLOWED); + shadowOf(mPackageManager).addPermissionInfo(createCrossProfilesPermissionInfo()); + + assertThat(mFragment.getPreferenceSummary( + mContext, CROSS_PROFILE_PACKAGE_NAME, Process.myUid())) + .isEqualTo(mContext.getString(R.string.app_permission_summary_allowed)); + } + + @Test + public void getPreferenceSummary_appOpNotAllowed_returnsNotAllowed() { + String appOp = AppOpsManager.permissionToOp(INTERACT_ACROSS_PROFILES_PERMISSION); + shadowOf(mAppOpsManager).setMode( + appOp, Process.myUid(), CROSS_PROFILE_PACKAGE_NAME, AppOpsManager.MODE_IGNORED); + shadowOf(mPackageManager).addPermissionInfo(createCrossProfilesPermissionInfo()); + + assertThat(mFragment.getPreferenceSummary( + mContext, CROSS_PROFILE_PACKAGE_NAME, Process.myUid())) + .isEqualTo(mContext.getString(R.string.app_permission_summary_not_allowed)); + } + + private PermissionInfo createCrossProfilesPermissionInfo() { + PermissionInfo permissionInfo = new PermissionInfo(); + permissionInfo.name = INTERACT_ACROSS_PROFILES_PERMISSION; + permissionInfo.protectionLevel = PermissionInfo.PROTECTION_FLAG_APPOP; + return permissionInfo; + } +} diff --git a/tests/robotests/src/com/android/settings/applications/specialaccess/interactacrossprofiles/InteractAcrossProfilesPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/applications/specialaccess/interactacrossprofiles/InteractAcrossProfilesPreferenceControllerTest.java new file mode 100644 index 00000000000..bac7437d81d --- /dev/null +++ b/tests/robotests/src/com/android/settings/applications/specialaccess/interactacrossprofiles/InteractAcrossProfilesPreferenceControllerTest.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2020 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.settings.applications.specialaccess.interactacrossprofiles; + +import static com.google.common.truth.Truth.assertThat; + +import static org.robolectric.Shadows.shadowOf; + +import android.content.Context; +import android.content.pm.CrossProfileApps; + +import androidx.test.core.app.ApplicationProvider; + +import com.android.settings.core.BasePreferenceController; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +@RunWith(RobolectricTestRunner.class) +public class InteractAcrossProfilesPreferenceControllerTest { + + private static final String CROSS_PROFILE_PACKAGE_NAME = "crossProfilePackage"; + private static final String NOT_CROSS_PROFILE_PACKAGE_NAME = "notCrossProfilePackage"; + + private final Context mContext = ApplicationProvider.getApplicationContext(); + private final CrossProfileApps mCrossProfileApps = + mContext.getSystemService(CrossProfileApps.class); + private final InteractAcrossProfilesDetailsPreferenceController mController = + new InteractAcrossProfilesDetailsPreferenceController(mContext, "test_key"); + + @Test + public void getAvailabilityStatus_crossProfilePackage_returnsAvailable() { + mController.setPackageName(CROSS_PROFILE_PACKAGE_NAME); + shadowOf(mCrossProfileApps).addCrossProfilePackage(CROSS_PROFILE_PACKAGE_NAME); + + assertThat(mController.getAvailabilityStatus()) + .isEqualTo(BasePreferenceController.AVAILABLE); + } + + @Test + public void getAvailabilityStatus_notCrossProfilePackage_returnsDisabled() { + mController.setPackageName(NOT_CROSS_PROFILE_PACKAGE_NAME); + + assertThat(mController.getAvailabilityStatus()) + .isEqualTo(BasePreferenceController.DISABLED_FOR_USER); + } + + @Test + public void getDetailFragmentClass_shouldReturnInteractAcrossProfilesDetails() { + assertThat(mController.getDetailFragmentClass()) + .isEqualTo(InteractAcrossProfilesDetails.class); + } +} diff --git a/tests/robotests/src/com/android/settings/applications/specialaccess/interactacrossprofiles/InteractAcrossProfilesSettingsTest.java b/tests/robotests/src/com/android/settings/applications/specialaccess/interactacrossprofiles/InteractAcrossProfilesSettingsTest.java new file mode 100644 index 00000000000..9a4c56b34ce --- /dev/null +++ b/tests/robotests/src/com/android/settings/applications/specialaccess/interactacrossprofiles/InteractAcrossProfilesSettingsTest.java @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2020 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.settings.applications.specialaccess.interactacrossprofiles; + +import static com.google.common.truth.Truth.assertThat; + +import static org.robolectric.Shadows.shadowOf; + +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.CrossProfileApps; +import android.content.pm.PackageManager; +import android.os.UserHandle; +import android.os.UserManager; +import android.util.Pair; + +import androidx.test.core.app.ApplicationProvider; + +import com.google.common.collect.ImmutableList; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.shadows.ShadowProcess; +import org.robolectric.util.ReflectionHelpers; + +import java.util.List; + +@RunWith(RobolectricTestRunner.class) +public class InteractAcrossProfilesSettingsTest { + + private static final int PERSONAL_PROFILE_ID = 0; + private static final int WORK_PROFILE_ID = 10; + private static final int WORK_UID = UserHandle.PER_USER_RANGE * WORK_PROFILE_ID; + + private static final String PERSONAL_CROSS_PROFILE_PACKAGE = "personalCrossProfilePackage"; + private static final String PERSONAL_NON_CROSS_PROFILE_PACKAGE = + "personalNonCrossProfilePackage"; + private static final String WORK_CROSS_PROFILE_PACKAGE = "workCrossProfilePackage"; + private static final String WORK_NON_CROSS_PROFILE_PACKAGE = + "workNonCrossProfilePackage"; + private static final List PERSONAL_PROFILE_INSTALLED_PACKAGES = + ImmutableList.of(PERSONAL_CROSS_PROFILE_PACKAGE, PERSONAL_NON_CROSS_PROFILE_PACKAGE); + private static final List WORK_PROFILE_INSTALLED_PACKAGES = + ImmutableList.of(WORK_CROSS_PROFILE_PACKAGE, WORK_NON_CROSS_PROFILE_PACKAGE); + + private final Context mContext = ApplicationProvider.getApplicationContext(); + private final PackageManager mPackageManager = mContext.getPackageManager(); + private final UserManager mUserManager = mContext.getSystemService(UserManager.class); + private final CrossProfileApps mCrossProfileApps = + mContext.getSystemService(CrossProfileApps.class); + private final InteractAcrossProfilesSettings mFragment = new InteractAcrossProfilesSettings(); + + @Before + public void setup() { + ReflectionHelpers.setField(mFragment, "mPackageManager", mPackageManager); + ReflectionHelpers.setField(mFragment, "mUserManager", mUserManager); + ReflectionHelpers.setField(mFragment, "mCrossProfileApps", mCrossProfileApps); + } + + @Test + public void collectConfigurableApps_fromPersonal_returnsPersonalPackages() { + shadowOf(mUserManager).addUser( + PERSONAL_PROFILE_ID, "personal-profile"/* name */, 0/* flags */); + shadowOf(mUserManager).addProfile( + PERSONAL_PROFILE_ID, WORK_PROFILE_ID, + "work-profile"/* profileName */, 0/* profileFlags */); + shadowOf(mPackageManager).setInstalledPackagesForUserId( + PERSONAL_PROFILE_ID, PERSONAL_PROFILE_INSTALLED_PACKAGES); + shadowOf(mPackageManager).setInstalledPackagesForUserId( + WORK_PROFILE_ID, WORK_PROFILE_INSTALLED_PACKAGES); + shadowOf(mCrossProfileApps).addCrossProfilePackage(PERSONAL_CROSS_PROFILE_PACKAGE); + shadowOf(mCrossProfileApps).addCrossProfilePackage(WORK_CROSS_PROFILE_PACKAGE); + + List> apps = mFragment.collectConfigurableApps(); + + assertThat(apps.size()).isEqualTo(1); + assertThat(apps.get(0).first.packageName).isEqualTo(PERSONAL_CROSS_PROFILE_PACKAGE); + } + + @Test + public void collectConfigurableApps_fromWork_returnsPersonalPackages() { + shadowOf(mUserManager).addUser( + PERSONAL_PROFILE_ID, "personal-profile"/* name */, 0/* flags */); + shadowOf(mUserManager).addProfile( + PERSONAL_PROFILE_ID, WORK_PROFILE_ID, + "work-profile"/* profileName */, 0/* profileFlags */); + ShadowProcess.setUid(WORK_UID); + shadowOf(mPackageManager).setInstalledPackagesForUserId( + PERSONAL_PROFILE_ID, PERSONAL_PROFILE_INSTALLED_PACKAGES); + shadowOf(mPackageManager).setInstalledPackagesForUserId( + WORK_PROFILE_ID, WORK_PROFILE_INSTALLED_PACKAGES); + shadowOf(mCrossProfileApps).addCrossProfilePackage(PERSONAL_CROSS_PROFILE_PACKAGE); + shadowOf(mCrossProfileApps).addCrossProfilePackage(WORK_CROSS_PROFILE_PACKAGE); + + List> apps = mFragment.collectConfigurableApps(); + + assertThat(apps.size()).isEqualTo(1); + assertThat(apps.get(0).first.packageName).isEqualTo(PERSONAL_CROSS_PROFILE_PACKAGE); + } + + @Test + public void collectConfigurableApps_onlyOneProfile_returnsEmpty() { + shadowOf(mUserManager).addUser( + PERSONAL_PROFILE_ID, "personal-profile"/* name */, 0/* flags */); + shadowOf(mPackageManager).setInstalledPackagesForUserId( + PERSONAL_PROFILE_ID, PERSONAL_PROFILE_INSTALLED_PACKAGES); + shadowOf(mCrossProfileApps).addCrossProfilePackage(PERSONAL_CROSS_PROFILE_PACKAGE); + + List> apps = mFragment.collectConfigurableApps(); + + assertThat(apps).isEmpty(); + } +}