diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 29d9d39671a..06e0143393e 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -4986,6 +4986,10 @@ + + Private Space successfully deleted Private Space could not be deleted + + Set a screen lock + + To use Private Space, set a screen lock on this device. + + Set screen lock + + Cancel You can add up to %d fingerprints diff --git a/src/com/android/settings/privatespace/PrivateSpaceAuthenticationActivity.java b/src/com/android/settings/privatespace/PrivateSpaceAuthenticationActivity.java new file mode 100644 index 00000000000..50a44e1783d --- /dev/null +++ b/src/com/android/settings/privatespace/PrivateSpaceAuthenticationActivity.java @@ -0,0 +1,152 @@ +/* + * 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.app.admin.DevicePolicyManager.ACTION_SET_NEW_PASSWORD; + +import android.app.AlertDialog; +import android.app.KeyguardManager; +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.os.Bundle; +import android.os.Flags; +import android.util.Log; + +import androidx.activity.result.ActivityResult; +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; +import androidx.annotation.Nullable; +import androidx.fragment.app.FragmentActivity; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.settings.R; +import com.android.settings.core.SubSettingLauncher; +import com.android.settingslib.transition.SettingsTransitionHelper; + +import com.google.android.setupdesign.util.ThemeHelper; + +/** + * Prompts user to set a device lock if not set with an alert dialog. + * If a lock is already set then first authenticates user before displaying private space settings + * page. + */ +public class PrivateSpaceAuthenticationActivity extends FragmentActivity { + private static final String TAG = "PrivateSpaceAuthCheck"; + private PrivateSpaceMaintainer mPrivateSpaceMaintainer; + private KeyguardManager mKeyguardManager; + + private final ActivityResultLauncher mSetDeviceLock = + registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), + this::onSetDeviceLockResult); + private final ActivityResultLauncher mVerifyDeviceLock = + registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), + this::onVerifyDeviceLock); + + static class Injector { + PrivateSpaceMaintainer injectPrivateSpaceMaintainer(Context context) { + return PrivateSpaceMaintainer.getInstance(context); + } + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + if (Flags.allowPrivateProfile()) { + ThemeHelper.trySetDynamicColor(this); + mPrivateSpaceMaintainer = new Injector().injectPrivateSpaceMaintainer( + getApplicationContext()); + if (getKeyguardManager().isDeviceSecure()) { + if (savedInstanceState == null) { + Intent credentialIntent = + mPrivateSpaceMaintainer.getPrivateProfileLockCredentialIntent(); + if (credentialIntent != null) { + mVerifyDeviceLock.launch(credentialIntent); + } else { + Log.e(TAG, "verifyCredentialIntent is null even though device lock is set"); + finish(); + } + } + } else { + promptToSetDeviceLock(); + } + } else { + Log.w(TAG, "allowPrivateProfile flag is Off!"); + finish(); + } + } + + /** Show private space settings page on device lock authentications */ + @VisibleForTesting + public void onLockAuthentication(Context context) { + new SubSettingLauncher(context) + .setDestination(PrivateSpaceDashboardFragment.class.getName()) + .setTransitionType( + SettingsTransitionHelper.TransitionType.TRANSITION_SLIDE) + .setSourceMetricsCategory(SettingsEnums.PRIVATE_SPACE_SETTINGS) + .launch(); + } + + @VisibleForTesting + public void setPrivateSpaceMaintainer(Injector injector) { + mPrivateSpaceMaintainer = injector.injectPrivateSpaceMaintainer(getApplicationContext()); + } + + private void promptToSetDeviceLock() { + new AlertDialog.Builder(this) + .setTitle(R.string.no_device_lock_title) + .setMessage(R.string.no_device_lock_summary) + .setPositiveButton( + R.string.no_device_lock_action_label, + (DialogInterface dialog, int which) -> { + mSetDeviceLock.launch(new Intent(ACTION_SET_NEW_PASSWORD)); + }) + .setNegativeButton( + R.string.no_device_lock_cancel, + (DialogInterface dialog, int which) -> finish()) + .setOnCancelListener( + (DialogInterface dialog) -> { + finish(); + }) + .show(); + } + + private KeyguardManager getKeyguardManager() { + if (mKeyguardManager == null) { + mKeyguardManager = getSystemService(KeyguardManager.class); + } + return mKeyguardManager; + } + + private void onSetDeviceLockResult(@Nullable ActivityResult result) { + if (result != null) { + if (getKeyguardManager().isDeviceSecure()) { + onLockAuthentication(this); + } + finish(); + } + } + + private void onVerifyDeviceLock(@Nullable ActivityResult result) { + if (result != null && result.getResultCode() == RESULT_OK) { + onLockAuthentication(this); + } + finish(); + } +} diff --git a/src/com/android/settings/privatespace/PrivateSpaceMaintainer.java b/src/com/android/settings/privatespace/PrivateSpaceMaintainer.java index 0abf8f7700f..af2da5bbe30 100644 --- a/src/com/android/settings/privatespace/PrivateSpaceMaintainer.java +++ b/src/com/android/settings/privatespace/PrivateSpaceMaintainer.java @@ -20,7 +20,9 @@ import static android.os.UserManager.USER_TYPE_PROFILE_PRIVATE; import android.app.ActivityManager; import android.app.IActivityManager; +import android.app.KeyguardManager; import android.content.Context; +import android.content.Intent; import android.content.pm.UserInfo; import android.os.RemoteException; import android.os.UserHandle; @@ -28,6 +30,8 @@ import android.os.UserManager; import android.util.ArraySet; import android.util.Log; +import androidx.annotation.Nullable; + import com.android.internal.annotations.GuardedBy; import java.util.List; @@ -43,6 +47,7 @@ public class PrivateSpaceMaintainer { private final UserManager mUserManager; @GuardedBy("this") private UserHandle mUserHandle; + private final KeyguardManager mKeyguardManager; public enum ErrorDeletingPrivateSpace { DELETE_PS_ERROR_NONE, @@ -140,6 +145,23 @@ public class PrivateSpaceMaintainer { return mUserManager.isQuietModeEnabled(mUserHandle); } + /** + * Returns an intent to prompt the user to confirm private profile credentials if it is set + * otherwise returns intent to confirm device credentials. + */ + @Nullable + public synchronized Intent getPrivateProfileLockCredentialIntent() { + //TODO(b/307281644): To replace with check for doesPrivateSpaceExist() method once Auth + // changes are merged. + if (isPrivateProfileLockSet()) { + return mKeyguardManager.createConfirmDeviceCredentialIntent( + /* title= */ null, /* description= */null, mUserHandle.getIdentifier()); + } + // TODO(b/304796434) Need to try changing this intent to use BiometricPrompt + return mKeyguardManager.createConfirmDeviceCredentialIntent( + /* title= */ null, /* description= */ null); + } + /** Returns the instance of {@link PrivateSpaceMaintainer} */ public static synchronized PrivateSpaceMaintainer getInstance(Context context) { if (sPrivateSpaceMaintainer == null) { @@ -151,5 +173,19 @@ public class PrivateSpaceMaintainer { private PrivateSpaceMaintainer(Context context) { mContext = context.getApplicationContext(); mUserManager = mContext.getSystemService(UserManager.class); + mKeyguardManager = mContext.getSystemService(KeyguardManager.class); + } + + + // TODO(b/307281644): Remove this method once new auth change is merged + /** + * Returns true if private space exists and a separate private profile lock is set + * otherwise false when the private space does not exit or exists but does not have a + * separate profile lock. + */ + @GuardedBy("this") + private boolean isPrivateProfileLockSet() { + return doesPrivateSpaceExist() + && mKeyguardManager.isDeviceSecure(mUserHandle.getIdentifier()); } } diff --git a/src/com/android/settings/privatespace/PrivateSpaceSafetySource.java b/src/com/android/settings/privatespace/PrivateSpaceSafetySource.java index 4910a7b73b7..6729830ab67 100644 --- a/src/com/android/settings/privatespace/PrivateSpaceSafetySource.java +++ b/src/com/android/settings/privatespace/PrivateSpaceSafetySource.java @@ -17,7 +17,6 @@ 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.Flags; @@ -28,9 +27,7 @@ import android.safetycenter.SafetySourceStatus; 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 { @@ -86,18 +83,15 @@ public final class PrivateSpaceSafetySource { } 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); + Intent privateSpaceAuthenticationIntent = + new Intent(context, PrivateSpaceAuthenticationActivity.class) + .setIdentifier(SAFETY_SOURCE_ID); return PendingIntent .getActivity( context, /* requestCode */ 0, - privateSpaceDashboardIntent, + privateSpaceAuthenticationIntent, PendingIntent.FLAG_IMMUTABLE); } } diff --git a/tests/uitests/Android.bp b/tests/uitests/Android.bp index f149519314a..4a47c906ef4 100644 --- a/tests/uitests/Android.bp +++ b/tests/uitests/Android.bp @@ -47,7 +47,8 @@ android_test { "settings-helper", "sysui-helper", "timeresult-helper-lib", - "truth", + "truth-prebuilt", + "flag-junit", ], //sdk_version: "current", diff --git a/tests/uitests/src/com/android/settings/ui/SecuritySettingsTest.kt b/tests/uitests/src/com/android/settings/ui/SecuritySettingsTest.kt index 5339e95fb4d..b5a4fe989cc 100644 --- a/tests/uitests/src/com/android/settings/ui/SecuritySettingsTest.kt +++ b/tests/uitests/src/com/android/settings/ui/SecuritySettingsTest.kt @@ -16,6 +16,9 @@ package com.android.settings.ui +import android.os.Flags +import android.platform.test.annotations.RequiresFlagsEnabled +import android.platform.test.flag.junit.DeviceFlagsValueProvider import android.provider.Settings import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry @@ -23,13 +26,18 @@ import androidx.test.uiautomator.UiDevice import com.android.settings.ui.testutils.SettingsTestUtils.assertHasTexts import com.android.settings.ui.testutils.SettingsTestUtils.startMainActivityFromHomeScreen import org.junit.Before +import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith + @RunWith(AndroidJUnit4::class) class SecuritySettingsTest { private val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) + @get:Rule + public val mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule() + @Before fun setUp() { device.startMainActivityFromHomeScreen(Settings.ACTION_SECURITY_SETTINGS) @@ -40,6 +48,12 @@ class SecuritySettingsTest { device.assertHasTexts(ON_SCREEN_TEXTS) } + @Test + @RequiresFlagsEnabled(Flags.FLAG_ALLOW_PRIVATE_PROFILE) + fun privateSpace_ifFlagON() { + device.assertHasTexts(listOf("Private Space")) + } + private companion object { // Items we really want to always show val ON_SCREEN_TEXTS = listOf( diff --git a/tests/uitests/src/com/android/settings/ui/privatespace/PrivateSpaceAuthenticationActivityTest.kt b/tests/uitests/src/com/android/settings/ui/privatespace/PrivateSpaceAuthenticationActivityTest.kt new file mode 100644 index 00000000000..87514716082 --- /dev/null +++ b/tests/uitests/src/com/android/settings/ui/privatespace/PrivateSpaceAuthenticationActivityTest.kt @@ -0,0 +1,91 @@ +/* + * 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.ui.privatespace + + +import android.os.Flags +import android.platform.test.annotations.RequiresFlagsEnabled +import android.platform.test.flag.junit.DeviceFlagsValueProvider +import android.provider.Settings +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.uiautomator.By +import androidx.test.uiautomator.UiDevice +import com.android.settings.ui.testutils.SettingsTestUtils.assertHasTexts +import com.android.settings.ui.testutils.SettingsTestUtils.clickObject +import com.android.settings.ui.testutils.SettingsTestUtils.startMainActivityFromHomeScreen +import com.android.settings.ui.testutils.SettingsTestUtils.waitObject +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + + +@RunWith(AndroidJUnit4::class) +@RequiresFlagsEnabled(Flags.FLAG_ALLOW_PRIVATE_PROFILE) +class PrivateSpaceAuthenticationActivityTest { + private val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) + + @get:Rule + public val mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule() + + @Before + fun setUp() { + device.startMainActivityFromHomeScreen(Settings.ACTION_SECURITY_SETTINGS) + device.assertHasTexts(listOf(PRIVATE_SPACE_SETTING)) + } + + @Test + fun showAuthenticationScreen() { + Thread.sleep(1000) + device.clickObject(By.text(PRIVATE_SPACE_SETTING)) + device.waitObject(By.text(DIALOG_TITLE)) + Thread.sleep(1000) + device.assertHasTexts(listOf("Set a screen lock","Cancel")) + } + + @Test + fun onCancelLockExitSetup() { + Thread.sleep(1000) + device.clickObject(By.text(PRIVATE_SPACE_SETTING)) + device.waitObject(By.text(DIALOG_TITLE)) + Thread.sleep(1000) + device.assertHasTexts(listOf(SET_LOCK_BUTTON, CANCEL_TEXT)) + device.clickObject(By.text(CANCEL_TEXT)) + device.assertHasTexts(listOf(PRIVATE_SPACE_SETTING)) + } + + @Test + fun onSetupSetLock() { + Thread.sleep(1000) + device.clickObject(By.text(PRIVATE_SPACE_SETTING)) + device.waitObject(By.text(DIALOG_TITLE)) + Thread.sleep(1000) + device.assertHasTexts(listOf(SET_LOCK_BUTTON,CANCEL_TEXT)) + device.clickObject(By.text(SET_LOCK_BUTTON)) + device.assertHasTexts(listOf(LOCK_SCREEN_TITLE)) + } + + private companion object { + // Items we really want to always show + val PRIVATE_SPACE_SETTING = "Private Space" + const val SET_LOCK_BUTTON = "Set screen lock" + val CANCEL_TEXT = "Cancel" + val DIALOG_TITLE = "Set a screen lock" + val LOCK_SCREEN_TITLE = "Choose screen lock" + } +} diff --git a/tests/unit/src/com/android/settings/privatespace/PrivateSpaceAuthenticationActivityTest.java b/tests/unit/src/com/android/settings/privatespace/PrivateSpaceAuthenticationActivityTest.java new file mode 100644 index 00000000000..d2e12707fb8 --- /dev/null +++ b/tests/unit/src/com/android/settings/privatespace/PrivateSpaceAuthenticationActivityTest.java @@ -0,0 +1,98 @@ +/* + * 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.PrivateSpaceSafetySource.SAFETY_SOURCE_ID; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +import android.content.Context; +import android.content.Intent; +import android.os.Flags; +import android.platform.test.annotations.RequiresFlagsEnabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; + +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.platform.app.InstrumentationRegistry; + +import org.junit.Before; +import org.junit.Rule; +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 PrivateSpaceAuthenticationActivityTest { + @Rule + public final CheckFlagsRule mCheckFlagsRule = + DeviceFlagsValueProvider.createCheckFlagsRule(); + @Mock private PrivateSpaceMaintainer mPrivateSpaceMaintainer; + @Mock private Context mContext; + private PrivateSpaceAuthenticationActivity mPrivateSpaceAuthenticationActivity; + private Intent mDefaultIntent; + + /** Required setup before a test. */ + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + mContext = ApplicationProvider.getApplicationContext(); + mDefaultIntent = new Intent(); + mDefaultIntent.setClass(InstrumentationRegistry.getInstrumentation().getTargetContext(), + PrivateSpaceAuthenticationActivity.class); + InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { + try { + mPrivateSpaceAuthenticationActivity = + spy((PrivateSpaceAuthenticationActivity) InstrumentationRegistry + .getInstrumentation().newActivity( + getClass().getClassLoader(), + PrivateSpaceAuthenticationActivity.class.getName(), + mDefaultIntent)); + } catch (Exception e) { + throw new RuntimeException(e); // nothing to do + } + }); + doNothing().when(mPrivateSpaceAuthenticationActivity).startActivity(any(Intent.class)); + PrivateSpaceAuthenticationActivity.Injector injector = + new PrivateSpaceAuthenticationActivity.Injector() { + @Override + PrivateSpaceMaintainer injectPrivateSpaceMaintainer(Context context) { + return mPrivateSpaceMaintainer; + } + }; + mPrivateSpaceAuthenticationActivity.setPrivateSpaceMaintainer(injector); + } + + /** Tests that on lock authentication Private space settings is launched. */ + @Test + @RequiresFlagsEnabled(Flags.FLAG_ALLOW_PRIVATE_PROFILE) + public void deviceSecurePrivateSpaceExists() { + final ArgumentCaptor intentCaptor = ArgumentCaptor.forClass(Intent.class); + mPrivateSpaceAuthenticationActivity.onLockAuthentication(mContext); + verify(mPrivateSpaceAuthenticationActivity).startActivity(intentCaptor.capture()); + assertThat(intentCaptor.getValue().getIdentifier()).isEqualTo(SAFETY_SOURCE_ID); + } +} diff --git a/tests/unit/src/com/android/settings/privatespace/PrivateSpaceSafetySourceTest.java b/tests/unit/src/com/android/settings/privatespace/PrivateSpaceSafetySourceTest.java index ddf52871a25..bb3f891a55c 100644 --- a/tests/unit/src/com/android/settings/privatespace/PrivateSpaceSafetySourceTest.java +++ b/tests/unit/src/com/android/settings/privatespace/PrivateSpaceSafetySourceTest.java @@ -18,8 +18,11 @@ 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; @@ -122,9 +125,9 @@ public class PrivateSpaceSafetySourceTest { assertThat(safetySourceStatus.isEnabled()).isTrue(); } - /** Tests that setSafetySourceData sets the PS settings page intent. */ + /** Tests that setSafetySourceData sets the PS settings page authenticator intent. */ @Test - public void setSafetySourceData_setsPsIntent() { + public void setSafetySourceData_setsPsAuthenticatorIntent() { when(mSafetyCenterManagerWrapper.isEnabled(mContext)).thenReturn(true); mSetFlagsRule.enableFlags(Flags.FLAG_ALLOW_PRIVATE_PROFILE); @@ -135,7 +138,7 @@ public class PrivateSpaceSafetySourceTest { 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); + assertThat(safetySourceStatus.getPendingIntent().getIntent() + .equals(PrivateSpaceAuthenticationActivity.class)); } }