diff --git a/res/values/strings.xml b/res/values/strings.xml index a4f709d2bc6..e9a547ce934 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -1175,13 +1175,38 @@ More security & privacy - + Security Privacy Work profile + + Private Space + + Hide apps in a private folder + + Unlock using screen lock + + Show Private Space + + System + + Create Private Space + + Delete Private Space + + Private Space successfully created + + Private Space already exists + + Private Space could not be created + + Private Space successfully deleted + + Private Space could not be deleted + You can add up to %d fingerprints diff --git a/res/xml/private_space_settings.xml b/res/xml/private_space_settings.xml new file mode 100644 index 00000000000..08053e0f5b6 --- /dev/null +++ b/res/xml/private_space_settings.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/com/android/settings/privatespace/CreatePrivateSpaceController.java b/src/com/android/settings/privatespace/CreatePrivateSpaceController.java new file mode 100644 index 00000000000..32149885745 --- /dev/null +++ b/src/com/android/settings/privatespace/CreatePrivateSpaceController.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2023 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.privatespace; + +import android.content.Context; +import android.text.TextUtils; +import android.widget.Toast; + +import androidx.preference.Preference; + +import com.android.settings.R; +import com.android.settings.core.BasePreferenceController; + +// TODO(b/293569406): Remove this when we have the setup flow in place to create PS +/** + * Temp Controller to create the private space from the PS Settings page. This is to allow PM, UX, + * and other folks to play around with PS before the PS setup flow is ready. + */ +public final class CreatePrivateSpaceController extends BasePreferenceController { + + public CreatePrivateSpaceController(Context context, String preferenceKey) { + super(context, preferenceKey); + } + + @Override + public int getAvailabilityStatus() { + return AVAILABLE; + } + + @Override + public boolean handlePreferenceTreeClick(Preference preference) { + if (!TextUtils.equals(preference.getKey(), getPreferenceKey())) { + return false; + } + + if (PrivateSpaceMaintainer.getInstance(mContext).doesPrivateSpaceExist()) { + showPrivateSpaceAlreadyExistsToast(); + return super.handlePreferenceTreeClick(preference); + } + + if (PrivateSpaceMaintainer.getInstance(mContext).createPrivateSpace()) { + showPrivateSpaceCreatedToast(); + } else { + showPrivateSpaceCreationFailedToast(); + } + return super.handlePreferenceTreeClick(preference); + } + + private void showPrivateSpaceCreatedToast() { + Toast.makeText(mContext, R.string.private_space_created, Toast.LENGTH_SHORT).show(); + } + + private void showPrivateSpaceCreationFailedToast() { + Toast.makeText(mContext, R.string.private_space_create_failed, Toast.LENGTH_SHORT).show(); + } + + private void showPrivateSpaceAlreadyExistsToast() { + Toast.makeText(mContext, R.string.private_space_already_exists, Toast.LENGTH_SHORT).show(); + } +} diff --git a/src/com/android/settings/privatespace/DeletePrivateSpaceController.java b/src/com/android/settings/privatespace/DeletePrivateSpaceController.java new file mode 100644 index 00000000000..c94f63a2629 --- /dev/null +++ b/src/com/android/settings/privatespace/DeletePrivateSpaceController.java @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2023 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.privatespace; + +import static com.android.settings.privatespace.PrivateSpaceMaintainer.ErrorDeletingPrivateSpace.DELETE_PS_ERROR_INTERNAL; +import static com.android.settings.privatespace.PrivateSpaceMaintainer.ErrorDeletingPrivateSpace.DELETE_PS_ERROR_NONE; +import static com.android.settings.privatespace.PrivateSpaceMaintainer.ErrorDeletingPrivateSpace.DELETE_PS_ERROR_NO_PRIVATE_SPACE; + +import android.content.Context; +import android.text.TextUtils; +import android.util.Log; +import android.widget.Toast; + +import androidx.preference.Preference; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.settings.R; +import com.android.settings.core.BasePreferenceController; + +/** Controller to delete the private space from the PS Settings page */ +public class DeletePrivateSpaceController extends BasePreferenceController { + private static final String TAG = "DeletePrivateSpaceController"; + private final PrivateSpaceMaintainer mPrivateSpaceMaintainer; + + static class Injector { + PrivateSpaceMaintainer injectPrivateSpaceMaintainer(Context context) { + return PrivateSpaceMaintainer.getInstance(context); + } + } + + public DeletePrivateSpaceController(Context context, String preferenceKey) { + this(context, preferenceKey, new Injector()); + } + + DeletePrivateSpaceController(Context context, String preferenceKey, Injector injector) { + super(context, preferenceKey); + mPrivateSpaceMaintainer = injector.injectPrivateSpaceMaintainer(context); + } + + @Override + public int getAvailabilityStatus() { + return AVAILABLE; + } + + @Override + public boolean handlePreferenceTreeClick(Preference preference) { + if (!TextUtils.equals(preference.getKey(), getPreferenceKey())) { + return false; + } + + PrivateSpaceMaintainer.ErrorDeletingPrivateSpace error = + mPrivateSpaceMaintainer.deletePrivateSpace(); + if (error == DELETE_PS_ERROR_NONE) { + showSuccessfulDeletionToast(); + } else if (error == DELETE_PS_ERROR_INTERNAL) { + showDeletionInternalErrorToast(); + } else if (error == DELETE_PS_ERROR_NO_PRIVATE_SPACE) { + // Ideally this should never happen as PS Settings is not available when there's no + // Private Profile. + Log.e(TAG, "Unexpected attempt to delete non-existent PS"); + } + return super.handlePreferenceTreeClick(preference); + } + + /** Shows a toast saying that the private space was deleted */ + @VisibleForTesting + public void showSuccessfulDeletionToast() { + Toast.makeText(mContext, R.string.private_space_deleted, Toast.LENGTH_SHORT).show(); + } + + /** Shows a toast saying that the private space could not be deleted */ + @VisibleForTesting + public void showDeletionInternalErrorToast() { + Toast.makeText(mContext, R.string.private_space_delete_failed, Toast.LENGTH_SHORT).show(); + } +} diff --git a/src/com/android/settings/privatespace/HidePrivateSpaceController.java b/src/com/android/settings/privatespace/HidePrivateSpaceController.java new file mode 100644 index 00000000000..f27acbd62b9 --- /dev/null +++ b/src/com/android/settings/privatespace/HidePrivateSpaceController.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2023 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.privatespace; + +import android.content.Context; + +import com.android.settings.core.TogglePreferenceController; + +/** Represents the preference controller for (un)hiding the Private Space */ +public final class HidePrivateSpaceController extends TogglePreferenceController { + public HidePrivateSpaceController(Context context, String preferenceKey) { + super(context, preferenceKey); + } + + @Override + public int getAvailabilityStatus() { + return AVAILABLE; + } + + @Override + public boolean isChecked() { + // TODO(b/293569406) Need to check this from a persistent store, maybe like SettingsProvider + return false; + } + + @Override + public boolean setChecked(boolean isChecked) { + // TODO(b/293569406) Need to save this to a persistent store, maybe like SettingsProvider + return true; + } + + @Override + public int getSliceHighlightMenuRes() { + return 0; + } +} diff --git a/src/com/android/settings/privatespace/PrivateSpaceDashboardFragment.java b/src/com/android/settings/privatespace/PrivateSpaceDashboardFragment.java new file mode 100644 index 00000000000..9e1d0d51bd6 --- /dev/null +++ b/src/com/android/settings/privatespace/PrivateSpaceDashboardFragment.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2023 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.privatespace; + +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.util.FeatureFlagUtils; + +import com.android.settings.R; +import com.android.settings.dashboard.DashboardFragment; +import com.android.settings.safetycenter.SafetyCenterManagerWrapper; +import com.android.settings.search.BaseSearchIndexProvider; +import com.android.settingslib.search.SearchIndexable; + +import java.util.List; + +/** Fragment representing the Private Space dashboard in Settings. */ +@SearchIndexable +public class PrivateSpaceDashboardFragment extends DashboardFragment { + private static final String TAG = "PrivateSpaceDashboardFragment"; + private static final String KEY_CREATE_PROFILE_PREFERENCE = "private_space_create"; + private static final String KEY_DELETE_PROFILE_PREFERENCE = "private_space_delete"; + private static final String KEY_ONE_LOCK_PREFERENCE = "private_space_use_one_lock"; + private static final String KEY_PS_HIDDEN_PREFERENCE = "private_space_hidden"; + + @Override + protected int getPreferenceScreenResId() { + return R.xml.private_space_settings; + } + + @Override + public int getMetricsCategory() { + return SettingsEnums.PRIVATE_SPACE_SETTINGS; + } + + @Override + protected String getLogTag() { + return TAG; + } + + public static final BaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = + new BaseSearchIndexProvider(R.xml.private_space_settings) { + @Override + protected boolean isPageSearchEnabled(Context context) { + // Temporary workaround for hiding PS Settings until the trunk stable feature + // flag is available. + // TODO(b/295516544): Remove this workaround when trunk stable feature flag is + // available. + return SafetyCenterManagerWrapper.get().isEnabled(context) + && FeatureFlagUtils.isEnabled(context, + FeatureFlagUtils.SETTINGS_PRIVATE_SPACE_SETTINGS); + } + + @Override + public List getNonIndexableKeys(Context context) { + List keys = super.getNonIndexableKeys(context); + keys.add(KEY_CREATE_PROFILE_PREFERENCE); + keys.add(KEY_DELETE_PROFILE_PREFERENCE); + keys.add(KEY_ONE_LOCK_PREFERENCE); + keys.add(KEY_PS_HIDDEN_PREFERENCE); + return keys; + } + }; +} diff --git a/src/com/android/settings/privatespace/PrivateSpaceMaintainer.java b/src/com/android/settings/privatespace/PrivateSpaceMaintainer.java new file mode 100644 index 00000000000..709814d745f --- /dev/null +++ b/src/com/android/settings/privatespace/PrivateSpaceMaintainer.java @@ -0,0 +1,145 @@ +/* + * Copyright (C) 2023 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.privatespace; + +import static android.os.UserManager.USER_TYPE_PROFILE_PRIVATE; + +import android.app.ActivityManager; +import android.app.IActivityManager; +import android.content.Context; +import android.content.pm.UserInfo; +import android.os.RemoteException; +import android.os.UserHandle; +import android.os.UserManager; +import android.util.ArraySet; +import android.util.Log; + +import com.android.internal.annotations.GuardedBy; + +import java.util.List; + +// TODO(b/293569406): Update the javadoc when we have the setup flow in place to create PS +/** A class to help with the creation / deletion of Private Space */ +public class PrivateSpaceMaintainer { + private static final String TAG = "PrivateSpaceMaintainer"; + @GuardedBy("this") + private static PrivateSpaceMaintainer sPrivateSpaceMaintainer; + + private final Context mContext; + private final UserManager mUserManager; + @GuardedBy("this") + private UserHandle mUserHandle; + + public enum ErrorDeletingPrivateSpace { + DELETE_PS_ERROR_NONE, + DELETE_PS_ERROR_NO_PRIVATE_SPACE, + DELETE_PS_ERROR_INTERNAL + } + + /** + * Returns true if the private space was successfully created. + * + *

This method should be used by the Private Space Setup Flow ONLY. + */ + final synchronized boolean createPrivateSpace() { + // Check if Private space already exists + if (doesPrivateSpaceExist()) { + return true; + } + // a name indicating that the profile was created from the PS Settings page + final String userName = "psSettingsUser"; + + if (mUserHandle == null) { + try { + mUserHandle = mUserManager.createProfile( + userName, USER_TYPE_PROFILE_PRIVATE, new ArraySet<>()); + } catch (Exception e) { + Log.e(TAG, "Error creating private space", e); + return false; + } + + if (mUserHandle == null) { + Log.e(TAG, "Failed to create private space"); + return false; + } + + IActivityManager am = ActivityManager.getService(); + try { + am.startProfile(mUserHandle.getIdentifier()); + } catch (RemoteException e) { + Log.e(TAG, "Failed to start private profile"); + return false; + } + + Log.i(TAG, "Private space created with id: " + mUserHandle.getIdentifier()); + } + return true; + } + + /** Returns the {@link ErrorDeletingPrivateSpace} enum representing the result of operation. + * + *

This method should be used ONLY by the delete-PS controller in the PS Settings page. + */ + public synchronized ErrorDeletingPrivateSpace deletePrivateSpace() { + if (!doesPrivateSpaceExist()) { + return ErrorDeletingPrivateSpace.DELETE_PS_ERROR_NO_PRIVATE_SPACE; + } + + try { + Log.i(TAG, "Deleting Private space with id: " + mUserHandle.getIdentifier()); + if (mUserManager.removeUser(mUserHandle)) { + Log.i(TAG, "Private space deleted"); + mUserHandle = null; + + return ErrorDeletingPrivateSpace.DELETE_PS_ERROR_NONE; + } else { + Log.e(TAG, "Failed to delete private space"); + } + } catch (Exception e) { + Log.e(TAG, "Error deleting private space", e); + } + return ErrorDeletingPrivateSpace.DELETE_PS_ERROR_INTERNAL; + } + + /** Returns true if the Private space exists. */ + public synchronized boolean doesPrivateSpaceExist() { + if (mUserHandle != null) { + return true; + } + + List users = mUserManager.getProfiles(0); + for (UserInfo user : users) { + if (user.isPrivateProfile()) { + mUserHandle = user.getUserHandle(); + return true; + } + } + return false; + } + + static synchronized PrivateSpaceMaintainer getInstance(Context context) { + if (sPrivateSpaceMaintainer == null) { + sPrivateSpaceMaintainer = new PrivateSpaceMaintainer(context); + } + return sPrivateSpaceMaintainer; + } + + private PrivateSpaceMaintainer(Context context) { + mContext = context.getApplicationContext(); + mUserManager = mContext.getSystemService(UserManager.class); + } +} diff --git a/src/com/android/settings/privatespace/PrivateSpaceSafetySource.java b/src/com/android/settings/privatespace/PrivateSpaceSafetySource.java new file mode 100644 index 00000000000..b07c6233cfb --- /dev/null +++ b/src/com/android/settings/privatespace/PrivateSpaceSafetySource.java @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2023 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.privatespace; + +import android.app.PendingIntent; +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.content.Intent; +import android.os.UserManager; +import android.safetycenter.SafetyEvent; +import android.safetycenter.SafetySourceData; +import android.safetycenter.SafetySourceStatus; +import android.util.FeatureFlagUtils; +import android.util.Log; + +import com.android.settings.R; +import com.android.settings.core.SubSettingLauncher; +import com.android.settings.safetycenter.SafetyCenterManagerWrapper; +import com.android.settingslib.transition.SettingsTransitionHelper; + +/** Private Space safety source for the Safety Center */ +public final class PrivateSpaceSafetySource { + public static final String SAFETY_SOURCE_ID = "AndroidPrivateSpace"; + private static final String TAG = "PrivateSpaceSafetySource"; + + private PrivateSpaceSafetySource() {} + + /** Sets lock screen safety data for Safety Center. */ + public static void setSafetySourceData(Context context, + SafetyEvent safetyEvent) { + if (!SafetyCenterManagerWrapper.get().isEnabled(context)) { + Log.i(TAG, "Safety Center disabled"); + return; + } + + // Check the profile type - we don't want to show this for anything other than primary user. + UserManager userManager = context.getSystemService(UserManager.class); + if (userManager != null && !userManager.isMainUser()) { + Log.i(TAG, "setSafetySourceData not main user"); + return; + } + + // Temporary workaround to help prevent the PS Settings showing up in droidfood builds. + // TODO(b/295516544): remove this when the trunk stable feature flag for PS is available. + if (!FeatureFlagUtils.isEnabled(context, + FeatureFlagUtils.SETTINGS_PRIVATE_SPACE_SETTINGS)) { + // Setting null safetySourceData so that an old entry gets cleared out and this way + // provide a response since SC always expects one on rescan. + SafetyCenterManagerWrapper.get().setSafetySourceData( + context, + SAFETY_SOURCE_ID, + /* safetySourceData */ null, + safetyEvent + ); + return; + } + + PendingIntent pendingIntent = getPendingIntentForPsDashboard(context); + + SafetySourceStatus status = new SafetySourceStatus.Builder( + context.getString(R.string.private_space_title), + context.getString(R.string.private_space_summary), + SafetySourceData.SEVERITY_LEVEL_UNSPECIFIED) + .setPendingIntent(pendingIntent).build(); + SafetySourceData safetySourceData = + new SafetySourceData.Builder().setStatus(status).build(); + + Log.d(TAG, "Setting safety source data"); + SafetyCenterManagerWrapper.get().setSafetySourceData( + context, + SAFETY_SOURCE_ID, + safetySourceData, + safetyEvent + ); + } + + private static PendingIntent getPendingIntentForPsDashboard(Context context) { + Intent privateSpaceDashboardIntent = new SubSettingLauncher(context) + .setDestination(PrivateSpaceDashboardFragment.class.getName()) + .setTransitionType(SettingsTransitionHelper.TransitionType.TRANSITION_SLIDE) + .setSourceMetricsCategory(SettingsEnums.PRIVATE_SPACE_SETTINGS) + .toIntent() + .setIdentifier(SAFETY_SOURCE_ID); + + return PendingIntent + .getActivity( + context, + /* requestCode */ 0, + privateSpaceDashboardIntent, + PendingIntent.FLAG_IMMUTABLE); + } +} diff --git a/src/com/android/settings/privatespace/UseOneLockController.java b/src/com/android/settings/privatespace/UseOneLockController.java new file mode 100644 index 00000000000..a94db57a814 --- /dev/null +++ b/src/com/android/settings/privatespace/UseOneLockController.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2023 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.privatespace; + +import android.content.Context; + +import com.android.settings.core.TogglePreferenceController; + +/** Represents the preference controller for using the same lock as the screen lock */ +public class UseOneLockController extends TogglePreferenceController { + public UseOneLockController(Context context, String preferenceKey) { + super(context, preferenceKey); + } + + @Override + public int getAvailabilityStatus() { + return AVAILABLE; + } + + @Override + public boolean isChecked() { + // TODO(b/293569406) Need to save this to a persistent store, maybe like SettingsProvider + return false; + } + + @Override + public boolean setChecked(boolean isChecked) { + // TODO(b/293569406) Need to save this to a persistent store, maybe like SettingsProvider + return true; + } + + @Override + public int getSliceHighlightMenuRes() { + return 0; + } +} diff --git a/src/com/android/settings/safetycenter/SafetySourceBroadcastReceiver.java b/src/com/android/settings/safetycenter/SafetySourceBroadcastReceiver.java index 0b556e74a37..cc0f892e873 100644 --- a/src/com/android/settings/safetycenter/SafetySourceBroadcastReceiver.java +++ b/src/com/android/settings/safetycenter/SafetySourceBroadcastReceiver.java @@ -28,6 +28,7 @@ import android.content.Intent; import android.safetycenter.SafetyCenterManager; import android.safetycenter.SafetyEvent; +import com.android.settings.privatespace.PrivateSpaceSafetySource; import com.android.settings.security.ScreenLockPreferenceDetailsUtils; import com.google.common.collect.ImmutableList; @@ -79,11 +80,16 @@ public class SafetySourceBroadcastReceiver extends BroadcastReceiver { if (sourceIds.contains(BiometricsSafetySource.SAFETY_SOURCE_ID)) { BiometricsSafetySource.setSafetySourceData(context, safetyEvent); } + + if (sourceIds.contains(PrivateSpaceSafetySource.SAFETY_SOURCE_ID)) { + PrivateSpaceSafetySource.setSafetySourceData(context, safetyEvent); + } } private static void refreshAllSafetySources(Context context, SafetyEvent safetyEvent) { LockScreenSafetySource.setSafetySourceData(context, new ScreenLockPreferenceDetailsUtils(context), safetyEvent); BiometricsSafetySource.setSafetySourceData(context, safetyEvent); + PrivateSpaceSafetySource.setSafetySourceData(context, safetyEvent); } } diff --git a/tests/unit/src/com/android/settings/privatespace/DeletePrivateSpaceControllerTest.java b/tests/unit/src/com/android/settings/privatespace/DeletePrivateSpaceControllerTest.java new file mode 100644 index 00000000000..8fb3eae9dab --- /dev/null +++ b/tests/unit/src/com/android/settings/privatespace/DeletePrivateSpaceControllerTest.java @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2023 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.privatespace; + +import static com.android.settings.core.BasePreferenceController.AVAILABLE; +import static com.android.settings.privatespace.PrivateSpaceMaintainer.ErrorDeletingPrivateSpace.DELETE_PS_ERROR_INTERNAL; +import static com.android.settings.privatespace.PrivateSpaceMaintainer.ErrorDeletingPrivateSpace.DELETE_PS_ERROR_NONE; +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.verify; + +import android.content.Context; + +import androidx.preference.Preference; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +@RunWith(AndroidJUnit4.class) +public class DeletePrivateSpaceControllerTest { + @Mock private PrivateSpaceMaintainer mPrivateSpaceMaintainer; + @Mock private Context mContext; + + private Preference mPreference; + private DeletePrivateSpaceController mDeletePrivateSpaceController; + + /** Required setup before a test. */ + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mContext = ApplicationProvider.getApplicationContext(); + final String preferenceKey = "private_space_delete"; + + mPreference = new Preference(ApplicationProvider.getApplicationContext()); + mPreference.setKey(preferenceKey); + + mDeletePrivateSpaceController = + new DeletePrivateSpaceController( + mContext, + preferenceKey, + new DeletePrivateSpaceController.Injector() { + @Override + PrivateSpaceMaintainer injectPrivateSpaceMaintainer(Context context) { + return mPrivateSpaceMaintainer; + } + }); + } + + /** Tests that the controller is always available. */ + @Test + public void getAvailabilityStatus_returnsAvailable() { + assertThat(mDeletePrivateSpaceController.getAvailabilityStatus()).isEqualTo(AVAILABLE); + } + + /** Tests that on click it attempts to delete the PS. */ + @Test + public void handlePreferenceTreeClick_attemptsToDeletePrivateSpace() { + doReturn(DELETE_PS_ERROR_NONE).when(mPrivateSpaceMaintainer).deletePrivateSpace(); + DeletePrivateSpaceController spy = Mockito.spy(mDeletePrivateSpaceController); + doNothing().when(spy).showSuccessfulDeletionToast(); + spy.handlePreferenceTreeClick(mPreference); + + verify(mPrivateSpaceMaintainer).deletePrivateSpace(); + } + + /** Tests that on deletion of PS relevant toast is shown. */ + @Test + public void handlePreferenceTreeClick_onDeletion_showsDeletedToast() { + doReturn(DELETE_PS_ERROR_NONE).when(mPrivateSpaceMaintainer).deletePrivateSpace(); + DeletePrivateSpaceController spy = Mockito.spy(mDeletePrivateSpaceController); + doNothing().when(spy).showSuccessfulDeletionToast(); + spy.handlePreferenceTreeClick(mPreference); + + verify(spy).showSuccessfulDeletionToast(); + } + + /** Tests that on failing to delete the PS relevant toast is shown. */ + @Test + public void handlePreferenceTreeClick_onDeletionError_showsDeletionFailedToast() { + doReturn(DELETE_PS_ERROR_INTERNAL).when(mPrivateSpaceMaintainer).deletePrivateSpace(); + DeletePrivateSpaceController spy = Mockito.spy(mDeletePrivateSpaceController); + doNothing().when(spy).showDeletionInternalErrorToast(); + spy.handlePreferenceTreeClick(mPreference); + + verify(spy).showDeletionInternalErrorToast(); + } +} diff --git a/tests/unit/src/com/android/settings/privatespace/HidePrivateSpaceControllerTest.java b/tests/unit/src/com/android/settings/privatespace/HidePrivateSpaceControllerTest.java new file mode 100644 index 00000000000..1a1769edf57 --- /dev/null +++ b/tests/unit/src/com/android/settings/privatespace/HidePrivateSpaceControllerTest.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2023 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.privatespace; + +import static com.android.settings.core.BasePreferenceController.AVAILABLE; +import static com.google.common.truth.Truth.assertThat; + +import android.content.Context; + +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; + +@RunWith(AndroidJUnit4.class) +public class HidePrivateSpaceControllerTest { + @Mock private Context mContext; + private HidePrivateSpaceController mHidePrivateSpaceController; + + /** Required setup before a test. */ + @Before + public void setUp() { + mContext = ApplicationProvider.getApplicationContext(); + final String preferenceKey = "private_space_hidden"; + + mHidePrivateSpaceController = new HidePrivateSpaceController(mContext, preferenceKey); + } + + /** Tests that the controller is always available. */ + @Test + public void getAvailabilityStatus_returnsAvailable() { + assertThat(mHidePrivateSpaceController.getAvailabilityStatus()).isEqualTo(AVAILABLE); + } +} diff --git a/tests/unit/src/com/android/settings/privatespace/PrivateSpaceSafetySourceTest.java b/tests/unit/src/com/android/settings/privatespace/PrivateSpaceSafetySourceTest.java new file mode 100644 index 00000000000..2dc00e145f1 --- /dev/null +++ b/tests/unit/src/com/android/settings/privatespace/PrivateSpaceSafetySourceTest.java @@ -0,0 +1,143 @@ +/* + * Copyright (C) 2023 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.privatespace; + + +import static android.safetycenter.SafetyEvent.SAFETY_EVENT_TYPE_DEVICE_REBOOTED; +import static com.android.settings.privatespace.PrivateSpaceSafetySource.SAFETY_SOURCE_ID; +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.safetycenter.SafetyEvent; +import android.safetycenter.SafetySourceData; +import android.safetycenter.SafetySourceStatus; +import android.util.FeatureFlagUtils; + +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import com.android.settings.safetycenter.SafetyCenterManagerWrapper; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@RunWith(AndroidJUnit4.class) +public class PrivateSpaceSafetySourceTest { + private static final SafetyEvent EVENT_TYPE_DEVICE_REBOOTED = + new SafetyEvent.Builder(SAFETY_EVENT_TYPE_DEVICE_REBOOTED).build(); + private Context mContext = ApplicationProvider.getApplicationContext(); + @Mock private SafetyCenterManagerWrapper mSafetyCenterManagerWrapper; + + /** Required setup before a test. */ + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + SafetyCenterManagerWrapper.sInstance = mSafetyCenterManagerWrapper; + + FeatureFlagUtils + .setEnabled(mContext, FeatureFlagUtils.SETTINGS_PRIVATE_SPACE_SETTINGS, true); + } + + /** Required setup after a test. */ + @After + public void tearDown() { + SafetyCenterManagerWrapper.sInstance = null; + } + + /** Tests that when SC is disabled we don't set any data. */ + @Test + public void onDeviceRebootedEvent_whenSafetyCenterDisabled_doesNotSetData() { + when(mSafetyCenterManagerWrapper.isEnabled(mContext)).thenReturn(false); + + PrivateSpaceSafetySource.setSafetySourceData(mContext, EVENT_TYPE_DEVICE_REBOOTED); + + verify(mSafetyCenterManagerWrapper, never()).setSafetySourceData( + any(), any(), any(), any()); + } + + /** Tests that when SC is enabled we set data. */ + @Test + public void onDeviceRebootedEvent_whenSafetyCenterEnabled_setsData() { + when(mSafetyCenterManagerWrapper.isEnabled(mContext)).thenReturn(true); + + PrivateSpaceSafetySource.setSafetySourceData(mContext, EVENT_TYPE_DEVICE_REBOOTED); + + verify(mSafetyCenterManagerWrapper).setSafetySourceData( + any(), eq(SAFETY_SOURCE_ID), any(), eq(EVENT_TYPE_DEVICE_REBOOTED)); + } + + // TODO(b/295516544): Modify this test for the new trunk stable flag instead when available. + /** Tests that when the feature is disabled null data is set. */ + @Test + public void setSafetySourceData_whenFeatureDisabled_setsNullData() { + when(mSafetyCenterManagerWrapper.isEnabled(mContext)).thenReturn(true); + FeatureFlagUtils + .setEnabled(mContext, FeatureFlagUtils.SETTINGS_PRIVATE_SPACE_SETTINGS, false); + + PrivateSpaceSafetySource.setSafetySourceData(mContext, EVENT_TYPE_DEVICE_REBOOTED); + + ArgumentCaptor captor = ArgumentCaptor.forClass(SafetySourceData.class); + verify(mSafetyCenterManagerWrapper).setSafetySourceData( + any(), eq(SAFETY_SOURCE_ID), captor.capture(), eq(EVENT_TYPE_DEVICE_REBOOTED)); + SafetySourceData safetySourceData = captor.getValue(); + assertThat(safetySourceData).isNull(); + + FeatureFlagUtils + .setEnabled(mContext, FeatureFlagUtils.SETTINGS_PRIVATE_SPACE_SETTINGS, true); + } + + /** Tests that setSafetySourceData sets the source status enabled. */ + @Test + public void setSafetySourceData_setsEnabled() { + when(mSafetyCenterManagerWrapper.isEnabled(mContext)).thenReturn(true); + + PrivateSpaceSafetySource.setSafetySourceData(mContext, EVENT_TYPE_DEVICE_REBOOTED); + + ArgumentCaptor captor = ArgumentCaptor.forClass(SafetySourceData.class); + verify(mSafetyCenterManagerWrapper).setSafetySourceData( + any(), eq(SAFETY_SOURCE_ID), captor.capture(), eq(EVENT_TYPE_DEVICE_REBOOTED)); + SafetySourceData safetySourceData = captor.getValue(); + SafetySourceStatus safetySourceStatus = safetySourceData.getStatus(); + assertThat(safetySourceStatus.isEnabled()).isTrue(); + } + + /** Tests that setSafetySourceData sets the PS settings page intent. */ + @Test + public void setSafetySourceData_setsPsIntent() { + when(mSafetyCenterManagerWrapper.isEnabled(mContext)).thenReturn(true); + + PrivateSpaceSafetySource.setSafetySourceData(mContext, EVENT_TYPE_DEVICE_REBOOTED); + + ArgumentCaptor captor = ArgumentCaptor.forClass(SafetySourceData.class); + verify(mSafetyCenterManagerWrapper).setSafetySourceData( + any(), eq(SAFETY_SOURCE_ID), captor.capture(), eq(EVENT_TYPE_DEVICE_REBOOTED)); + SafetySourceData safetySourceData = captor.getValue(); + SafetySourceStatus safetySourceStatus = safetySourceData.getStatus(); + assertThat(safetySourceStatus.getPendingIntent().getIntent().getIdentifier()) + .isEqualTo(SAFETY_SOURCE_ID); + } +} diff --git a/tests/unit/src/com/android/settings/privatespace/UseOneLockControllerTest.java b/tests/unit/src/com/android/settings/privatespace/UseOneLockControllerTest.java new file mode 100644 index 00000000000..e7ebb37faf6 --- /dev/null +++ b/tests/unit/src/com/android/settings/privatespace/UseOneLockControllerTest.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2023 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.privatespace; + +import static com.android.settings.core.BasePreferenceController.AVAILABLE; +import static com.google.common.truth.Truth.assertThat; + +import android.content.Context; + +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; + +@RunWith(AndroidJUnit4.class) +public class UseOneLockControllerTest { + @Mock private Context mContext; + private UseOneLockController mUseOneLockController; + + /** Required setup before a test. */ + @Before + public void setUp() { + mContext = ApplicationProvider.getApplicationContext(); + final String preferenceKey = "private_space_use_one_lock"; + + mUseOneLockController = new UseOneLockController(mContext, preferenceKey); + } + + /** Tests that the controller is always available. */ + @Test + public void getAvailabilityStatus_returnsAvailable() { + assertThat(mUseOneLockController.getAvailabilityStatus()).isEqualTo(AVAILABLE); + } +} diff --git a/tests/unit/src/com/android/settings/safetycenter/SafetySourceBroadcastReceiverTest.java b/tests/unit/src/com/android/settings/safetycenter/SafetySourceBroadcastReceiverTest.java index 3ad1874995b..caae44a0546 100644 --- a/tests/unit/src/com/android/settings/safetycenter/SafetySourceBroadcastReceiverTest.java +++ b/tests/unit/src/com/android/settings/safetycenter/SafetySourceBroadcastReceiverTest.java @@ -21,9 +21,7 @@ import static android.safetycenter.SafetyCenterManager.EXTRA_REFRESH_SAFETY_SOUR import static android.safetycenter.SafetyCenterManager.EXTRA_REFRESH_SAFETY_SOURCE_IDS; import static android.safetycenter.SafetyEvent.SAFETY_EVENT_TYPE_DEVICE_REBOOTED; import static android.safetycenter.SafetyEvent.SAFETY_EVENT_TYPE_REFRESH_REQUESTED; - import static com.google.common.truth.Truth.assertThat; - import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; @@ -33,11 +31,14 @@ import static org.mockito.Mockito.when; import android.content.Context; import android.content.Intent; import android.safetycenter.SafetyEvent; +import android.safetycenter.SafetySourceData; +import android.util.FeatureFlagUtils; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.android.internal.widget.LockPatternUtils; +import com.android.settings.privatespace.PrivateSpaceSafetySource; import com.android.settings.testutils.FakeFeatureFactory; import org.junit.After; @@ -216,6 +217,62 @@ public class SafetySourceBroadcastReceiverTest { assertThat(captor.getValue()).isEqualTo(BiometricsSafetySource.SAFETY_SOURCE_ID); } + /** + * Tests that on receiving the refresh broadcast request with the PS source id, the PS data + * is set. + */ + @Test + public void onReceive_onRefresh_withPrivateSpaceSourceId_setsPrivateSpaceData() { + when(mSafetyCenterManagerWrapper.isEnabled(mApplicationContext)).thenReturn(true); + Intent intent = + new Intent() + .setAction(ACTION_REFRESH_SAFETY_SOURCES) + .putExtra( + EXTRA_REFRESH_SAFETY_SOURCE_IDS, + new String[] {PrivateSpaceSafetySource.SAFETY_SOURCE_ID}) + .putExtra(EXTRA_REFRESH_SAFETY_SOURCES_BROADCAST_ID, REFRESH_BROADCAST_ID); + + new SafetySourceBroadcastReceiver().onReceive(mApplicationContext, intent); + ArgumentCaptor captor = ArgumentCaptor.forClass(String.class); + verify(mSafetyCenterManagerWrapper, times(1)) + .setSafetySourceData(any(), captor.capture(), any(), any()); + + assertThat(captor.getValue()).isEqualTo(PrivateSpaceSafetySource.SAFETY_SOURCE_ID); + } + + /** Tests that the PS source sets null data when it's disabled. */ + // TODO(b/295516544): Modify this test for the new trunk stable flag instead when available. + @Test + public void onReceive_onRefresh_withPrivateSpaceFeatureDisabled_setsNullData() { + when(mSafetyCenterManagerWrapper.isEnabled(mApplicationContext)).thenReturn(true); + FeatureFlagUtils + .setEnabled( + mApplicationContext, + FeatureFlagUtils.SETTINGS_PRIVATE_SPACE_SETTINGS, + false); + + Intent intent = + new Intent() + .setAction(ACTION_REFRESH_SAFETY_SOURCES) + .putExtra( + EXTRA_REFRESH_SAFETY_SOURCE_IDS, + new String[] {PrivateSpaceSafetySource.SAFETY_SOURCE_ID}) + .putExtra(EXTRA_REFRESH_SAFETY_SOURCES_BROADCAST_ID, REFRESH_BROADCAST_ID); + + new SafetySourceBroadcastReceiver().onReceive(mApplicationContext, intent); + ArgumentCaptor captor = ArgumentCaptor.forClass(SafetySourceData.class); + verify(mSafetyCenterManagerWrapper, times(1)) + .setSafetySourceData(any(), any(), captor.capture(), any()); + + assertThat(captor.getValue()).isEqualTo(null); + + FeatureFlagUtils + .setEnabled( + mApplicationContext, + FeatureFlagUtils.SETTINGS_PRIVATE_SPACE_SETTINGS, + true); + } + @Test public void onReceive_onBootCompleted_setsBootCompleteEvent() { when(mSafetyCenterManagerWrapper.isEnabled(mApplicationContext)).thenReturn(true); @@ -223,22 +280,22 @@ public class SafetySourceBroadcastReceiverTest { new SafetySourceBroadcastReceiver().onReceive(mApplicationContext, intent); ArgumentCaptor captor = ArgumentCaptor.forClass(SafetyEvent.class); - verify(mSafetyCenterManagerWrapper, times(2)) + verify(mSafetyCenterManagerWrapper, times(3)) .setSafetySourceData(any(), any(), any(), captor.capture()); SafetyEvent bootEvent = new SafetyEvent.Builder(SAFETY_EVENT_TYPE_DEVICE_REBOOTED).build(); assertThat(captor.getAllValues()) - .containsExactlyElementsIn(Arrays.asList(bootEvent, bootEvent)); + .containsExactlyElementsIn(Arrays.asList(bootEvent, bootEvent, bootEvent)); } @Test - public void onReceive_onBootCompleted_sendsBiometricAndLockscreenData() { + public void onReceive_onBootCompleted_sendsAllSafetySourcesData() { when(mSafetyCenterManagerWrapper.isEnabled(mApplicationContext)).thenReturn(true); Intent intent = new Intent().setAction(Intent.ACTION_BOOT_COMPLETED); new SafetySourceBroadcastReceiver().onReceive(mApplicationContext, intent); ArgumentCaptor captor = ArgumentCaptor.forClass(String.class); - verify(mSafetyCenterManagerWrapper, times(2)) + verify(mSafetyCenterManagerWrapper, times(3)) .setSafetySourceData(any(), captor.capture(), any(), any()); List safetySourceIdList = captor.getAllValues(); @@ -246,5 +303,7 @@ public class SafetySourceBroadcastReceiverTest { id -> id.equals(LockScreenSafetySource.SAFETY_SOURCE_ID))).isTrue(); assertThat(safetySourceIdList.stream().anyMatch( id -> id.equals(BiometricsSafetySource.SAFETY_SOURCE_ID))).isTrue(); + assertThat(safetySourceIdList.stream().anyMatch( + id -> id.equals(PrivateSpaceSafetySource.SAFETY_SOURCE_ID))).isTrue(); } }