diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 6d35545bdb5..fcd6cc07273 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -4341,6 +4341,13 @@ android:value="true" /> + + + + + + diff --git a/src/com/android/settings/safetycenter/BiometricsSafetySource.java b/src/com/android/settings/safetycenter/BiometricsSafetySource.java new file mode 100644 index 00000000000..f37ea0361ae --- /dev/null +++ b/src/com/android/settings/safetycenter/BiometricsSafetySource.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2022 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.safetycenter; + +import android.content.Context; + +/** Combined Biometrics Safety Source for Safety Center. */ +public final class BiometricsSafetySource { + + public static final String SAFETY_SOURCE_ID = "BiometricsSafetySource"; + + private BiometricsSafetySource() {} + + /** Sends biometric safety data to Safety Center. */ + public static void sendSafetyData(Context context) { + if (!SafetyCenterStatusHolder.get().isEnabled(context)) { + return; + } + + // TODO(b/215517420): Send biometric data to Safety Center if there are biometrics available + // on this device. + } +} diff --git a/src/com/android/settings/safetycenter/LockScreenSafetySource.java b/src/com/android/settings/safetycenter/LockScreenSafetySource.java new file mode 100644 index 00000000000..66001f2106b --- /dev/null +++ b/src/com/android/settings/safetycenter/LockScreenSafetySource.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2022 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.safetycenter; + +import android.app.PendingIntent; +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.content.Intent; +import android.safetycenter.SafetySourceData; +import android.safetycenter.SafetySourceStatus; + +import com.android.settings.core.SubSettingLauncher; +import com.android.settings.password.ChooseLockGeneric; +import com.android.settingslib.transition.SettingsTransitionHelper; + +/** Lock Screen Safety Source for Safety Center. */ +public final class LockScreenSafetySource { + + public static final String SAFETY_SOURCE_ID = "LockScreenSafetySource"; + + private LockScreenSafetySource() {} + + /** Sends lock screen safety data to Safety Center. */ + public static void sendSafetyData(Context context) { + if (!SafetyCenterStatusHolder.get().isEnabled(context)) { + return; + } + + // TODO(b/215515298): Replace placeholder SafetySourceData with real data. + // TODO(b/217409995): Replace SECURITY_ALTERNATIVE with Safety Center metrics category. + Intent clickIntent = new SubSettingLauncher(context) + .setDestination(ChooseLockGeneric.ChooseLockGenericFragment.class.getName()) + .setSourceMetricsCategory(SettingsEnums.SECURITY_ALTERNATIVE) + .setTransitionType(SettingsTransitionHelper.TransitionType.TRANSITION_SLIDE) + .toIntent(); + PendingIntent pendingIntent = PendingIntent + .getActivity( + context, + 0 /* requestCode */, + clickIntent, + PendingIntent.FLAG_IMMUTABLE); + SafetySourceData safetySourceData = + new SafetySourceData.Builder(SAFETY_SOURCE_ID).setStatus( + new SafetySourceStatus.Builder( + "Lock Screen", + "Lock screen settings", + SafetySourceStatus.STATUS_LEVEL_OK, + pendingIntent).build() + ).build(); + + SafetyCenterManagerWrapper.get().sendSafetyCenterUpdate(context, safetySourceData); + } +} diff --git a/src/com/android/settings/safetycenter/SafetyCenterManagerWrapper.java b/src/com/android/settings/safetycenter/SafetyCenterManagerWrapper.java new file mode 100644 index 00000000000..7e47f239f93 --- /dev/null +++ b/src/com/android/settings/safetycenter/SafetyCenterManagerWrapper.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2022 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.safetycenter; + +import android.content.Context; +import android.safetycenter.SafetyCenterManager; +import android.safetycenter.SafetySourceData; +import android.util.Log; + +import com.android.internal.annotations.VisibleForTesting; + +/** A wrapper for the SafetyCenterManager system service. */ +public class SafetyCenterManagerWrapper { + + private static final String TAG = "SafetyCenterManagerWrapper"; + + @VisibleForTesting + public static SafetyCenterManagerWrapper sInstance; + + private SafetyCenterManagerWrapper() {} + + /** Returns an instance of {@link SafetyCenterManagerWrapper}. */ + public static SafetyCenterManagerWrapper get() { + if (sInstance == null) { + sInstance = new SafetyCenterManagerWrapper(); + } + return sInstance; + } + + /** Sends updated safety source data to Safety Center. */ + public void sendSafetyCenterUpdate(Context context, SafetySourceData safetySourceData) { + SafetyCenterManager safetyCenterManager = + context.getSystemService(SafetyCenterManager.class); + + if (safetyCenterManager == null) { + Log.e(TAG, "System service SAFETY_CENTER_SERVICE (SafetyCenterManager) is null"); + return; + } + + try { + safetyCenterManager.sendSafetyCenterUpdate(safetySourceData); + } catch (Exception e) { + Log.e(TAG, "Failed to send SafetySourceData", e); + return; + } + } +} diff --git a/src/com/android/settings/safetycenter/SafetySourceBroadcastReceiver.java b/src/com/android/settings/safetycenter/SafetySourceBroadcastReceiver.java new file mode 100644 index 00000000000..55e8bba0b29 --- /dev/null +++ b/src/com/android/settings/safetycenter/SafetySourceBroadcastReceiver.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2022 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.safetycenter; + +import static android.safetycenter.SafetyCenterManager.EXTRA_REFRESH_SAFETY_SOURCE_IDS; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; + +import com.google.common.collect.ImmutableList; + +/** Broadcast receiver for handling requests from Safety Center for fresh data. */ +public class SafetySourceBroadcastReceiver extends BroadcastReceiver { + + @Override + public void onReceive(Context context, Intent intent) { + if (!SafetyCenterStatusHolder.get().isEnabled(context)) { + return; + } + + ImmutableList sourceIds = + ImmutableList.copyOf(intent.getStringArrayExtra(EXTRA_REFRESH_SAFETY_SOURCE_IDS)); + + if (sourceIds.contains(LockScreenSafetySource.SAFETY_SOURCE_ID)) { + LockScreenSafetySource.sendSafetyData(context); + } + + if (sourceIds.contains(BiometricsSafetySource.SAFETY_SOURCE_ID)) { + BiometricsSafetySource.sendSafetyData(context); + } + } +} diff --git a/tests/unit/src/com/android/settings/safetycenter/BiometricsSafetySourceTest.java b/tests/unit/src/com/android/settings/safetycenter/BiometricsSafetySourceTest.java new file mode 100644 index 00000000000..f53d3366474 --- /dev/null +++ b/tests/unit/src/com/android/settings/safetycenter/BiometricsSafetySourceTest.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2022 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.safetycenter; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.Context; + +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@RunWith(AndroidJUnit4.class) +public class BiometricsSafetySourceTest { + + private Context mApplicationContext; + + @Mock + private SafetyCenterManagerWrapper mSafetyCenterManagerWrapper; + + @Mock + private SafetyCenterStatusHolder mSafetyCenterStatusHolder; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mApplicationContext = ApplicationProvider.getApplicationContext(); + SafetyCenterManagerWrapper.sInstance = mSafetyCenterManagerWrapper; + SafetyCenterStatusHolder.sInstance = mSafetyCenterStatusHolder; + } + + @After + public void tearDown() { + SafetyCenterManagerWrapper.sInstance = null; + SafetyCenterStatusHolder.sInstance = null; + } + + @Test + public void sendSafetyData_whenSafetyCenterIsDisabled_sendsNoData() { + when(mSafetyCenterStatusHolder.isEnabled(mApplicationContext)).thenReturn(false); + + BiometricsSafetySource.sendSafetyData(mApplicationContext); + + verify(mSafetyCenterManagerWrapper, never()).sendSafetyCenterUpdate(any(), any()); + } + + @Test + // TODO(b/215517420): Adapt this test when method is implemented. + public void sendSafetyData_whenSafetyCenterIsEnabled_sendsNoData() { + when(mSafetyCenterStatusHolder.isEnabled(mApplicationContext)).thenReturn(true); + + BiometricsSafetySource.sendSafetyData(mApplicationContext); + + verify(mSafetyCenterManagerWrapper, never()).sendSafetyCenterUpdate(any(), any()); + } +} diff --git a/tests/unit/src/com/android/settings/safetycenter/LockScreenSafetySourceTest.java b/tests/unit/src/com/android/settings/safetycenter/LockScreenSafetySourceTest.java new file mode 100644 index 00000000000..052f981ca2a --- /dev/null +++ b/tests/unit/src/com/android/settings/safetycenter/LockScreenSafetySourceTest.java @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2022 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.safetycenter; + +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.verify; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.safetycenter.SafetySourceData; +import android.safetycenter.SafetySourceStatus; + +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import com.android.settings.SettingsActivity; +import com.android.settings.password.ChooseLockGeneric; + +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 LockScreenSafetySourceTest { + + private Context mApplicationContext; + + @Mock + private SafetyCenterManagerWrapper mSafetyCenterManagerWrapper; + + @Mock + private SafetyCenterStatusHolder mSafetyCenterStatusHolder; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mApplicationContext = ApplicationProvider.getApplicationContext(); + SafetyCenterManagerWrapper.sInstance = mSafetyCenterManagerWrapper; + SafetyCenterStatusHolder.sInstance = mSafetyCenterStatusHolder; + } + + @After + public void tearDown() { + SafetyCenterManagerWrapper.sInstance = null; + SafetyCenterStatusHolder.sInstance = null; + } + + @Test + public void sendSafetyData_whenSafetyCenterIsDisabled_sendsNoData() { + when(mSafetyCenterStatusHolder.isEnabled(mApplicationContext)).thenReturn(false); + + LockScreenSafetySource.sendSafetyData(mApplicationContext); + + verify(mSafetyCenterManagerWrapper, never()).sendSafetyCenterUpdate(any(), any()); + } + + @Test + public void sendSafetyData_whenSafetyCenterIsEnabled_sendsPlaceholderData() { + when(mSafetyCenterStatusHolder.isEnabled(mApplicationContext)).thenReturn(true); + + LockScreenSafetySource.sendSafetyData(mApplicationContext); + ArgumentCaptor captor = ArgumentCaptor.forClass(SafetySourceData.class); + verify(mSafetyCenterManagerWrapper).sendSafetyCenterUpdate(any(), captor.capture()); + SafetySourceData safetySourceData = captor.getValue(); + SafetySourceStatus safetySourceStatus = safetySourceData.getStatus(); + + assertThat(safetySourceData.getId()).isEqualTo(LockScreenSafetySource.SAFETY_SOURCE_ID); + assertThat(safetySourceStatus.getTitle().toString()).isEqualTo("Lock Screen"); + assertThat(safetySourceStatus.getSummary().toString()) + .isEqualTo("Lock screen settings"); + assertThat(safetySourceStatus.getStatusLevel()) + .isEqualTo(SafetySourceStatus.STATUS_LEVEL_OK); + assertThat(safetySourceStatus.getPendingIntent()).isNotNull(); + assertThat(safetySourceStatus.getPendingIntent().getIntent().getStringExtra( + SettingsActivity.EXTRA_SHOW_FRAGMENT)) + .isEqualTo(ChooseLockGeneric.ChooseLockGenericFragment.class.getName()); + } +} diff --git a/tests/unit/src/com/android/settings/safetycenter/SafetySourceBroadcastReceiverTest.java b/tests/unit/src/com/android/settings/safetycenter/SafetySourceBroadcastReceiverTest.java new file mode 100644 index 00000000000..581286bad02 --- /dev/null +++ b/tests/unit/src/com/android/settings/safetycenter/SafetySourceBroadcastReceiverTest.java @@ -0,0 +1,122 @@ +/* + * Copyright (C) 2022 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.safetycenter; + +import static android.safetycenter.SafetyCenterManager.EXTRA_REFRESH_SAFETY_SOURCE_IDS; + +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; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.content.Intent; +import android.safetycenter.SafetySourceData; + +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +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 SafetySourceBroadcastReceiverTest { + + private Context mApplicationContext; + + @Mock + private SafetyCenterManagerWrapper mSafetyCenterManagerWrapper; + + @Mock + private SafetyCenterStatusHolder mSafetyCenterStatusHolder; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mApplicationContext = ApplicationProvider.getApplicationContext(); + SafetyCenterManagerWrapper.sInstance = mSafetyCenterManagerWrapper; + SafetyCenterStatusHolder.sInstance = mSafetyCenterStatusHolder; + } + + @After + public void tearDown() { + SafetyCenterManagerWrapper.sInstance = null; + SafetyCenterStatusHolder.sInstance = null; + } + + @Test + public void sendSafetyData_whenSafetyCenterIsDisabled_sendsNoData() { + when(mSafetyCenterStatusHolder.isEnabled(mApplicationContext)).thenReturn(false); + Intent intent = + new Intent().putExtra( + EXTRA_REFRESH_SAFETY_SOURCE_IDS, + new String[]{ LockScreenSafetySource.SAFETY_SOURCE_ID }); + + new SafetySourceBroadcastReceiver().onReceive(mApplicationContext, intent); + + verify(mSafetyCenterManagerWrapper, never()).sendSafetyCenterUpdate(any(), any()); + } + + @Test + public void sendSafetyData_whenSafetyCenterIsEnabled_withNoSourceIds_sendsNoData() { + when(mSafetyCenterStatusHolder.isEnabled(mApplicationContext)).thenReturn(true); + Intent intent = new Intent().putExtra(EXTRA_REFRESH_SAFETY_SOURCE_IDS, new String[]{}); + + new SafetySourceBroadcastReceiver().onReceive(mApplicationContext, intent); + + verify(mSafetyCenterManagerWrapper, never()).sendSafetyCenterUpdate(any(), any()); + } + + @Test + public void sendSafetyData_withLockscreenSourceId_sendsLockscreenData() { + when(mSafetyCenterStatusHolder.isEnabled(mApplicationContext)).thenReturn(true); + Intent intent = + new Intent().putExtra( + EXTRA_REFRESH_SAFETY_SOURCE_IDS, + new String[]{ LockScreenSafetySource.SAFETY_SOURCE_ID }); + + new SafetySourceBroadcastReceiver().onReceive(mApplicationContext, intent); + ArgumentCaptor captor = ArgumentCaptor.forClass(SafetySourceData.class); + verify(mSafetyCenterManagerWrapper, times(1)) + .sendSafetyCenterUpdate(any(), captor.capture()); + SafetySourceData safetySourceData = captor.getValue(); + + assertThat(safetySourceData.getId()).isEqualTo(LockScreenSafetySource.SAFETY_SOURCE_ID); + } + + @Test + public void sendSafetyData_withBiometricsSourceId_sendsBiometricData() { + when(mSafetyCenterStatusHolder.isEnabled(mApplicationContext)).thenReturn(true); + Intent intent = + new Intent().putExtra( + EXTRA_REFRESH_SAFETY_SOURCE_IDS, + new String[]{ BiometricsSafetySource.SAFETY_SOURCE_ID }); + + new SafetySourceBroadcastReceiver().onReceive(mApplicationContext, intent); + + // TODO(b/215517420): Update this test when BiometricSafetySource is implemented. + verify(mSafetyCenterManagerWrapper, never()).sendSafetyCenterUpdate(any(), any()); + } +}