Remote authenticator enrollment enrolling layout.

This flow will be included in Device Unlock settings with the
Fingerprint and Face Unlock.

Bug: b/293908453
Test: atest RemoteAuthEnrollEnrollingTest
Change-Id: I56c7fb2481ace359813c27e7538bec0baceffced
This commit is contained in:
Justin McClain
2023-08-10 14:50:26 +00:00
parent 3b56d1fca5
commit 5c739de558
11 changed files with 542 additions and 1 deletions

View File

@@ -0,0 +1,24 @@
/*
* 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.enrolling
/** UI state of a single discovered authenticator. */
data class DiscoveredAuthenticatorUiState(
val name: String,
val isSelected: Boolean,
val onSelect: () -> Unit,
)

View File

@@ -0,0 +1,34 @@
/*
* 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.enrolling
/** The different states of the enrolling flow. */
enum class EnrollmentUiState {
/** No enrollment is happening. */
NONE,
/** Searching for potential authenticators. */
FINDING_DEVICES,
/**
* An enrollment is in progress.
*/
ENROLLING,
/** An enrollment has succeeded. */
SUCCESS,
}

View File

@@ -0,0 +1,127 @@
/*
* 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.enrolling
import android.os.Bundle
import android.view.View
import android.widget.ProgressBar
import android.widget.TextView
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 com.android.settings.remoteauth.RemoteAuthEnrollBase
import com.google.android.setupcompat.template.FooterButton
import kotlinx.coroutines.launch
class RemoteAuthEnrollEnrolling :
RemoteAuthEnrollBase(
layoutResId = R.layout.remote_auth_enroll_enrolling,
glifLayoutId = R.id.setup_wizard_layout,
) {
// TODO(b/293906345): Scope viewModel to navigation graph when implementing navigation.
private val viewModel = RemoteAuthEnrollEnrollingViewModel()
private val adapter = RemoteAuthEnrollEnrollingRecyclerViewAdapter()
private val progressBar by lazy {
view!!.findViewById<ProgressBar>(R.id.enrolling_list_progress_bar)
}
private val errorText by lazy { view!!.findViewById<TextView>(R.id.error_text) }
private val recyclerView by lazy {
view!!.findViewById<RecyclerView>(R.id.discovered_authenticator_list)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// Set up adapter
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)
}
}
}
}
override fun onStart() {
super.onStart()
// Get list of discovered devices from viewModel.
viewModel.discoverDevices()
}
override fun initializePrimaryFooterButton(): FooterButton {
return FooterButton.Builder(requireContext())
.setText(R.string.security_settings_remoteauth_enroll_enrolling_agree)
.setListener(this::onPrimaryFooterButtonClick)
.setButtonType(FooterButton.ButtonType.NEXT)
.setTheme(com.google.android.setupdesign.R.style.SudGlifButton_Primary)
.build()
}
override fun initializeSecondaryFooterButton(): FooterButton? {
return FooterButton.Builder(requireContext())
.setText(R.string.security_settings_remoteauth_enroll_enrolling_disagree)
.setListener(this::onSecondaryFooterButtonClick)
.setButtonType(FooterButton.ButtonType.SKIP)
.setTheme(com.google.android.setupdesign.R.style.SudGlifButton_Secondary)
.build()
}
private fun onPrimaryFooterButtonClick(view: View) {
viewModel.registerAuthenticator()
}
private fun onSecondaryFooterButtonClick(view: View) {
// TODO(b/293906345): Wire up navigation
}
private fun updateUi(uiState: RemoteAuthEnrollEnrollingUiState) {
progressBar.visibility = View.INVISIBLE
primaryFooterButton.isEnabled = false
// TODO(b/290769765): Add unit tests for all this states.
when (uiState.enrollmentUiState) {
EnrollmentUiState.NONE -> {
adapter.uiStates = uiState.discoveredDeviceUiStates
primaryFooterButton.isEnabled = viewModel.isDeviceSelected()
}
EnrollmentUiState.FINDING_DEVICES -> {
progressBar.visibility = View.VISIBLE
}
EnrollmentUiState.ENROLLING -> {}
EnrollmentUiState.SUCCESS -> {
// TODO(b/293906345): Wire up navigation
}
}
if (uiState.errorMsg != null) {
errorText.visibility = View.VISIBLE
errorText.text = uiState.errorMsg
} else {
errorText.visibility = View.INVISIBLE
errorText.text = ""
}
}
}

View File

@@ -0,0 +1,66 @@
/*
* 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.enrolling
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 RemoteAuthEnrollEnrollingRecyclerViewAdapter :
RecyclerView.Adapter<RemoteAuthEnrollEnrollingRecyclerViewAdapter.ViewHolder>() {
var uiStates = listOf<DiscoveredAuthenticatorUiState>()
set(value) {
field = value
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.remote_auth_enrolling_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.discovered_authenticator_name)
private val selectButton: ImageView = view.findViewById(R.id.authenticator_radio_button)
private val checkedDrawable =
view.context.getDrawable(R.drawable.ic_radio_button_checked_black_24dp)
private val uncheckedDrawable =
view.context.getDrawable(R.drawable.ic_radio_button_unchecked_black_24dp)
fun bind(discoveredAuthenticatorUiState: DiscoveredAuthenticatorUiState) {
titleTextView.text = discoveredAuthenticatorUiState.name
selectButton.background = if (discoveredAuthenticatorUiState.isSelected) {
checkedDrawable
} else {
uncheckedDrawable
}
selectButton.setOnClickListener { discoveredAuthenticatorUiState.onSelect() }
}
}
}

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.
*/
package com.android.settings.remoteauth.enrolling
/** UiState for full enrolling view. */
data class RemoteAuthEnrollEnrollingUiState(
val discoveredDeviceUiStates: List<DiscoveredAuthenticatorUiState> = listOf(),
val enrollmentUiState: EnrollmentUiState = EnrollmentUiState.NONE,
// TODO(b/293906744): Change to error code in teamfood and add errors to strings.xml
val errorMsg: String? = null,
)

View File

@@ -0,0 +1,73 @@
/*
* 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.enrolling
import androidx.lifecycle.ViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlin.properties.Delegates
class RemoteAuthEnrollEnrollingViewModel : ViewModel() {
private val _uiState = MutableStateFlow(RemoteAuthEnrollEnrollingUiState())
val uiState: StateFlow<RemoteAuthEnrollEnrollingUiState> = _uiState.asStateFlow()
private var errorMessage: String? = null
set(value) {
field = value
_uiState.update { currentState ->
currentState.copy(
errorMsg = value,
)
}
}
// TODO(b/293906744): Change to RemoteAuthManager.DiscoveredDevice.
private var selectedDevice: Any? by Delegates.observable(null) { _, _, _ -> discoverDevices() }
/** Returns if a device has been selected */
fun isDeviceSelected() = selectedDevice != null
/**
* Starts searching for nearby authenticators that are currently not enrolled. The devices
* and the state of the searching are both returned in uiState.
*/
fun discoverDevices() {
_uiState.update { currentState ->
currentState.copy(enrollmentUiState = EnrollmentUiState.FINDING_DEVICES)
}
// TODO(b/293906744): Map RemoteAuthManager discovered devices to
// DiscoveredAuthenticatorUiState in viewModelScope.
val discoveredDeviceUiStates = listOf<DiscoveredAuthenticatorUiState>()
_uiState.update { currentState ->
currentState.copy(
discoveredDeviceUiStates = discoveredDeviceUiStates,
enrollmentUiState = EnrollmentUiState.NONE
)
}
}
/** Registers the selected discovered device, if one is selected. */
fun registerAuthenticator() {
// TODO(b/293906744): Call RemoteAuthManager.register with selected device and update
// _uiState.
}
}