From a6325eac64fdc9b74a414f7dfec476aedc6510c4 Mon Sep 17 00:00:00 2001 From: Justin McClain Date: Fri, 11 Aug 2023 16:12:21 +0000 Subject: [PATCH] Remote authenticator settings layout. This flow will be included in Device Unlock settings with the Fingerprint and Face Unlock. Settings will allow viewing registered authenticators and unregistering authenticators. Bug: b/293906744 Test: atest RemoteAuthSettingsTest Change-Id: I91307a8449d384ecb8d4908f595a3bf6abaef2b5 --- res/drawable/ic_watch_24dp.xml | 25 +++++ res/layout/remote_auth_settings.xml | 95 +++++++++++++++++++ ...emote_auth_settings_authenticator_item.xml | 68 +++++++++++++ res/values/dimens.xml | 7 ++ res/values/strings.xml | 13 +++ .../RemoteAuthAuthenticatorItemUiState.kt | 23 +++++ .../remoteauth/settings/RemoteAuthSettings.kt | 68 +++++++++++++ .../RemoteAuthSettingsRecyclerViewAdapter.kt | 57 +++++++++++ .../settings/RemoteAuthSettingsUiState.kt | 23 +++++ .../settings/RemoteAuthSettingsViewModel.kt | 54 +++++++++++ .../settings/RemoteAuthSettingsTest.kt | 54 +++++++++++ 11 files changed, 487 insertions(+) create mode 100644 res/drawable/ic_watch_24dp.xml create mode 100644 res/layout/remote_auth_settings.xml create mode 100644 res/layout/remote_auth_settings_authenticator_item.xml create mode 100644 src/com/android/settings/remoteauth/settings/RemoteAuthAuthenticatorItemUiState.kt create mode 100644 src/com/android/settings/remoteauth/settings/RemoteAuthSettings.kt create mode 100644 src/com/android/settings/remoteauth/settings/RemoteAuthSettingsRecyclerViewAdapter.kt create mode 100644 src/com/android/settings/remoteauth/settings/RemoteAuthSettingsUiState.kt create mode 100644 src/com/android/settings/remoteauth/settings/RemoteAuthSettingsViewModel.kt create mode 100644 tests/robotests/src/com/android/settings/remoteauth/settings/RemoteAuthSettingsTest.kt diff --git a/res/drawable/ic_watch_24dp.xml b/res/drawable/ic_watch_24dp.xml new file mode 100644 index 00000000000..c5a391c4b8f --- /dev/null +++ b/res/drawable/ic_watch_24dp.xml @@ -0,0 +1,25 @@ + + + + diff --git a/res/layout/remote_auth_settings.xml b/res/layout/remote_auth_settings.xml new file mode 100644 index 00000000000..089dda45ce3 --- /dev/null +++ b/res/layout/remote_auth_settings.xml @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/layout/remote_auth_settings_authenticator_item.xml b/res/layout/remote_auth_settings_authenticator_item.xml new file mode 100644 index 00000000000..e2ec07cf82a --- /dev/null +++ b/res/layout/remote_auth_settings_authenticator_item.xml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/values/dimens.xml b/res/values/dimens.xml index 30892a3cf75..76f249b1dc1 100755 --- a/res/values/dimens.xml +++ b/res/values/dimens.xml @@ -171,14 +171,21 @@ 40dp + 14sp 24dp 48dp + 24dp + 20sp + 12dp 30dp 8dp 18sp 28dp 8dp 4dp + 22dp + + 16dp diff --git a/res/values/strings.xml b/res/values/strings.xml index 8e4fd655fb9..f116f5be543 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -917,6 +917,19 @@ You can now use your watch to unlock this phone when you swipe up on the lock screen or tap a notification Done + + + Watch Unlock + + You can use your watch to unlock this phone when you swipe up on the lock screen or tap a notification + + To use Watch Unlock, your watch must be unlocked, on your wrist, within reach, and connected to this phone. If the connection is interrupted, you\u2019ll need to unlock the phone before you can use Watch Unlock.\n\nKeep in mind:\nYou can only have one watch set up at a time. To add another watch, first remove the current one. + + Learn more about Watch Unlock + + Add watch + + Remove watch diff --git a/src/com/android/settings/remoteauth/settings/RemoteAuthAuthenticatorItemUiState.kt b/src/com/android/settings/remoteauth/settings/RemoteAuthAuthenticatorItemUiState.kt new file mode 100644 index 00000000000..f1b48f68407 --- /dev/null +++ b/src/com/android/settings/remoteauth/settings/RemoteAuthAuthenticatorItemUiState.kt @@ -0,0 +1,23 @@ +/* + * 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.remoteauth.settings + +data class RemoteAuthAuthenticatorItemUiState( + val name: String, + val isActive: Boolean, + val unregister: () -> Unit, +) \ No newline at end of file diff --git a/src/com/android/settings/remoteauth/settings/RemoteAuthSettings.kt b/src/com/android/settings/remoteauth/settings/RemoteAuthSettings.kt new file mode 100644 index 00000000000..ebf13f8f027 --- /dev/null +++ b/src/com/android/settings/remoteauth/settings/RemoteAuthSettings.kt @@ -0,0 +1,68 @@ +/* + * 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.remoteauth.settings + +import android.os.Bundle +import android.view.View +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.fragment.app.Fragment +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.android.settings.R +import kotlinx.coroutines.launch + +class RemoteAuthSettings : Fragment(R.layout.remote_auth_settings) { + + // TODO(b/293906345): Scope viewModel to navigation graph when implementing navigation. + val viewModel = RemoteAuthSettingsViewModel() + private val adapter = RemoteAuthSettingsRecyclerViewAdapter() + private val recyclerView by lazy { + view!!.findViewById(R.id.registered_authenticator_list) + } + + private val addAuthenticatorLayout by lazy { + view!!.findViewById(R.id.add_authenticator_layout) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + recyclerView.layoutManager = LinearLayoutManager(context) + recyclerView.adapter = adapter + + // Collect UIState and update UI on changes. + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.uiState.collect { + updateUi(it) + } + } + } + // Add new remote authenticator click listener + addAuthenticatorLayout.setOnClickListener { + // TODO(b/293906345): Wire up navigation + } + } + + private fun updateUi(uiState: RemoteAuthSettingsUiState) { + adapter.uiStates = uiState.registeredAuthenticatorUiStates + } + +} \ No newline at end of file diff --git a/src/com/android/settings/remoteauth/settings/RemoteAuthSettingsRecyclerViewAdapter.kt b/src/com/android/settings/remoteauth/settings/RemoteAuthSettingsRecyclerViewAdapter.kt new file mode 100644 index 00000000000..f506a0b6a93 --- /dev/null +++ b/src/com/android/settings/remoteauth/settings/RemoteAuthSettingsRecyclerViewAdapter.kt @@ -0,0 +1,57 @@ +/* + * 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.remoteauth.settings + + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView +import com.android.settings.R + +class RemoteAuthSettingsRecyclerViewAdapter() : + RecyclerView.Adapter() { + var uiStates = listOf() + set(value) { + field = value + notifyDataSetChanged() + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val view = LayoutInflater.from(parent.context) + .inflate(R.layout.remote_auth_settings_authenticator_item, parent, false) + return ViewHolder(view) + } + + override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) { + viewHolder.bind(uiStates[position]) + } + + override fun getItemCount() = uiStates.size + + class ViewHolder(view: View) : RecyclerView.ViewHolder(view) { + private val titleTextView: TextView = view.findViewById(R.id.authenticator_name_text) + private val unregisterButton: ImageView = view.findViewById(R.id.remove_icon) + + fun bind(authenticatorUiState: RemoteAuthAuthenticatorItemUiState) { + titleTextView.text = authenticatorUiState.name + unregisterButton.setOnClickListener { authenticatorUiState.unregister() } + } + } +} \ No newline at end of file diff --git a/src/com/android/settings/remoteauth/settings/RemoteAuthSettingsUiState.kt b/src/com/android/settings/remoteauth/settings/RemoteAuthSettingsUiState.kt new file mode 100644 index 00000000000..e0c0e0df3bb --- /dev/null +++ b/src/com/android/settings/remoteauth/settings/RemoteAuthSettingsUiState.kt @@ -0,0 +1,23 @@ +/* + * 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.remoteauth.settings + +data class RemoteAuthSettingsUiState( + val registeredAuthenticatorUiStates: List = listOf(), + // TODO(b/295524962): Change to error code in teamfood and add errors to strings.xml + val errorMsg: String? = null, +) \ No newline at end of file diff --git a/src/com/android/settings/remoteauth/settings/RemoteAuthSettingsViewModel.kt b/src/com/android/settings/remoteauth/settings/RemoteAuthSettingsViewModel.kt new file mode 100644 index 00000000000..e95dee2368b --- /dev/null +++ b/src/com/android/settings/remoteauth/settings/RemoteAuthSettingsViewModel.kt @@ -0,0 +1,54 @@ +/* + * 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.remoteauth.settings + +import androidx.lifecycle.ViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update + +class RemoteAuthSettingsViewModel : ViewModel() { + private val _uiState = MutableStateFlow(RemoteAuthSettingsUiState()) + val uiState: StateFlow = _uiState.asStateFlow() + + private var errorMessage: String? = null + set(value) { + field = value + _uiState.update { currentState -> + currentState.copy( + errorMsg = value, + ) + } + } + + fun refreshAuthenticatorList() { + // TODO(b/290768873): Pull from RemoteAuthenticationManager and map to UIState + val authenticatorUiStates = listOf() + + _uiState.update { currentState -> + currentState.copy( + registeredAuthenticatorUiStates = authenticatorUiStates, + ) + } + } + + /** Called by UI when user has acknowledged they seen the error dialog, via ok button. */ + fun resetErrorMessage() { + errorMessage = null + } +} \ No newline at end of file diff --git a/tests/robotests/src/com/android/settings/remoteauth/settings/RemoteAuthSettingsTest.kt b/tests/robotests/src/com/android/settings/remoteauth/settings/RemoteAuthSettingsTest.kt new file mode 100644 index 00000000000..81286007b20 --- /dev/null +++ b/tests/robotests/src/com/android/settings/remoteauth/settings/RemoteAuthSettingsTest.kt @@ -0,0 +1,54 @@ +/* + * 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.remoteauth.settings + +import android.os.Bundle +import androidx.fragment.app.testing.launchFragmentInContainer +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.* +import com.android.settings.R +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class RemoteAuthSettingsTest { + @Before + fun setUp() { + launchFragmentInContainer(Bundle(), R.style.SudThemeGlif) + } + + @Test + fun testRemoteAuthenticatorSettings_hasHeader() { + onView(withText(R.string.security_settings_remoteauth_settings_title)).check( + matches( + isDisplayed() + ) + ) + } + + @Test + fun testRemoteAuthenticatorSettings_hasDescription() { + onView(withText(R.string.security_settings_remoteauth_settings_description)).check( + matches( + isDisplayed() + ) + ) + } +} \ No newline at end of file