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
This commit is contained in:
Justin McClain
2023-08-11 16:12:21 +00:00
parent f6c3736352
commit a6325eac64
11 changed files with 487 additions and 0 deletions

View File

@@ -0,0 +1,25 @@
<!--
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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="m360,880 l-54,-182q-48,-38 -77,-95t-29,-123q0,-66 29,-123t77,-95l54,-182h240l54,182q48,38 77,95t29,123q0,66 -29,123t-77,95L600,880L360,880ZM480,680q83,0 141.5,-58.5T680,480q0,-83 -58.5,-141.5T480,280q-83,0 -141.5,58.5T280,480q0,83 58.5,141.5T480,680Z"/>
</vector>

View File

@@ -0,0 +1,95 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<com.google.android.setupdesign.GlifLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/setup_wizard_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:sucUsePartnerResource="false"
app:sucHeaderText="@string/security_settings_remoteauth_settings_title"
app:sudDescriptionText="@string/security_settings_remoteauth_settings_description" >
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="start"
android:paddingHorizontal="@dimen/remoteauth_padding_horizontal">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/registered_authenticator_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/remoteauth_settings_top_margin"/>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/add_authenticator_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginVertical="@dimen/remoteauth_device_vertical_margin"
android:minHeight="@dimen/remoteauth_touchable_area_minimum_span"
android:clickable="true">
<ImageView
android:id="@+id/add_icon"
android:layout_width="@dimen/remoteauth_icon_small_size"
android:layout_height="@dimen/remoteauth_icon_small_size"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:background="@drawable/ic_add_24dp"
android:clickable="false" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginHorizontal="@dimen/remoteauth_settings_device_horizontal_margin"
android:textColor="?android:attr/textColorPrimary"
android:textSize="@dimen/remoteauth_device_name_text_size"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@id/add_icon"
app:layout_constraintTop_toTopOf="parent"
android:text="@string/security_settings_remoteauth_settings_register_new_authenticator"
android:clickable="false" />
</androidx.constraintlayout.widget.ConstraintLayout>
<ImageView
android:layout_marginTop="@dimen/remoteauth_settings_top_margin"
android:layout_width="@dimen/remoteauth_icon_small_size"
android:layout_height="@dimen/remoteauth_icon_small_size"
android:background="@drawable/ic_info_outline_24dp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/remoteauth_settings_top_margin"
android:textSize="@dimen/remoteauth_fragment_subtitle_text_size"
android:text="@string/security_settings_remoteauth_settings_info_footer"
android:textColor="?android:attr/textColorPrimary" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/remoteauth_settings_top_margin"
android:textSize="@dimen/remoteauth_fragment_subtitle_text_size"
android:text="@string/security_settings_remoteauth_settings_learn_more"
android:textColor="?android:attr/textColorPrimary" />
</LinearLayout>
</com.google.android.setupdesign.GlifLayout>

View File

@@ -0,0 +1,68 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/authenticator_item_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginVertical="@dimen/remoteauth_device_vertical_margin"
android:minHeight="@dimen/remoteauth_touchable_area_minimum_span">
<ImageView
android:id="@+id/device_icon"
android:layout_width="@dimen/remoteauth_icon_small_size"
android:layout_height="@dimen/remoteauth_icon_small_size"
android:background="@drawable/ic_watch_24dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
<TextView
android:id="@+id/authenticator_name_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginHorizontal="@dimen/remoteauth_settings_device_horizontal_margin"
android:ellipsize="end"
android:maxLines="1"
app:layout_constrainedWidth="true"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/barrier"
app:layout_constraintStart_toEndOf="@id/device_icon"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/remove_icon"
android:layout_width="@dimen/remoteauth_icon_small_size"
android:layout_height="@dimen/remoteauth_icon_small_size"
android:tint="?android:attr/colorPrimary"
android:background="@drawable/ic_delete"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:contentDescription="@string/security_settings_remoteauth_settings_remove_device" />
<androidx.constraintlayout.widget.Barrier
android:id="@+id/barrier"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="start"
app:constraint_referenced_ids="remove_icon" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -171,14 +171,21 @@
<!-- RemoteAuth-->
<dimen name="remoteauth_fragment_padding_horizontal">40dp</dimen>
<dimen name="remoteauth_fragment_subtitle_text_size">14sp</dimen>
<dimen name="remoteauth_icon_small_size">24dp</dimen>
<dimen name="remoteauth_touchable_area_minimum_span">48dp</dimen>
<dimen name="remoteauth_padding_horizontal">24dp</dimen>
<dimen name="remoteauth_device_name_text_size">20sp</dimen>
<dimen name="remoteauth_device_vertical_margin">12dp</dimen>
<dimen name="remoteauth_introduction_fragment_padding_horizontal">30dp</dimen>
<dimen name="remoteauth_introduction_description_start_margin">8dp</dimen>
<dimen name="remoteauth_introduction_subheading_text_size">18sp</dimen>
<dimen name="remoteauth_carousel_progress_margin">28dp</dimen>
<dimen name="remoteauth_carousel_progress_circle_diameter">8dp</dimen>
<dimen name="remoteauth_carousel_progress_circle_margin">4dp</dimen>
<dimen name="remoteauth_settings_top_margin">22dp</dimen>
<dimen name="remoteauth_settings_device_horizontal_margin">16dp</dimen>
<!-- Lock pattern view size, align sysui biometric_auth_pattern_view_size -->

View File

@@ -917,6 +917,19 @@
<string name="security_settings_remoteauth_enroll_finish_description">You can now use your watch to unlock this phone when you swipe up on the lock screen or tap a notification</string>
<!-- Button text to finish enrollment [CHAR LIMIT=30] -->
<string name="security_settings_remoteauth_enroll_finish_btn_next">Done</string>
<!-- Strings for RemoteAuth settings page-->
<!-- Title for remote authenticator settings page [CHAR_LIMIT=NONE]-->
<string name="security_settings_remoteauth_settings_title">Watch Unlock</string>
<!-- Explains when a watch can be used to unlock the phone [CHAR_LIMIT=NONE] -->
<string name="security_settings_remoteauth_settings_description">You can use your watch to unlock this phone when you swipe up on the lock screen or tap a notification</string>
<!-- Explains how to enable the Watch Unlock feature on a watch [CHAR_LIMIT=NONE] -->
<string name="security_settings_remoteauth_settings_info_footer">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.</string>
<!-- Links to the Watch Unlock help center article [CHAR_LIMIT=NONE] -->
<string name="security_settings_remoteauth_settings_learn_more">Learn more about Watch Unlock</string>
<!-- Button text to add new watch [CHAR_LIMIT=NONE] -->
<string name="security_settings_remoteauth_settings_register_new_authenticator">Add watch</string>
<!-- Accessibility label of a button that lets users remove enrolled watches from Watch Unlock settings [CHAR_LIMIT=NONE] -->
<string name="security_settings_remoteauth_settings_remove_device">Remove watch</string>
<!-- Biometric settings --><skip />
<!-- Title shown for menu item that launches biometric settings. [CHAR LIMIT=66] -->

View File

@@ -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,
)

View File

@@ -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<RecyclerView>(R.id.registered_authenticator_list)
}
private val addAuthenticatorLayout by lazy {
view!!.findViewById<ConstraintLayout>(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
}
}

View File

@@ -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<RemoteAuthSettingsRecyclerViewAdapter.ViewHolder>() {
var uiStates = listOf<RemoteAuthAuthenticatorItemUiState>()
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() }
}
}
}

View File

@@ -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<RemoteAuthAuthenticatorItemUiState> = listOf(),
// TODO(b/295524962): Change to error code in teamfood and add errors to strings.xml
val errorMsg: String? = null,
)

View File

@@ -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<RemoteAuthSettingsUiState> = _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<RemoteAuthAuthenticatorItemUiState>()
_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
}
}

View File

@@ -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<RemoteAuthSettings>(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()
)
)
}
}