diff --git a/res/layout/remote_auth_enroll_enrolling.xml b/res/layout/remote_auth_enroll_enrolling.xml
new file mode 100644
index 00000000000..45886f736bb
--- /dev/null
+++ b/res/layout/remote_auth_enroll_enrolling.xml
@@ -0,0 +1,64 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/res/layout/remote_auth_enrolling_authenticator_item.xml b/res/layout/remote_auth_enrolling_authenticator_item.xml
new file mode 100644
index 00000000000..c92222baf69
--- /dev/null
+++ b/res/layout/remote_auth_enrolling_authenticator_item.xml
@@ -0,0 +1,52 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 76f249b1dc1..1d79b683a74 100755
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -183,8 +183,10 @@
28dp
8dp
4dp
+ 12dp
+ 16dp
+ 16dp
22dp
-
16dp
diff --git a/res/values/strings.xml b/res/values/strings.xml
index e62d8f4eb2c..da9d71c1924 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -910,6 +910,15 @@
Tap a notification
Swipe up on the lock screen
+
+
+ Choose your watch
+
+ Available watches
+
+ Cancel
+
+ Confirm
You\u2019re all set!
diff --git a/src/com/android/settings/remoteauth/enrolling/DiscoveredAuthenticatorUiState.kt b/src/com/android/settings/remoteauth/enrolling/DiscoveredAuthenticatorUiState.kt
new file mode 100644
index 00000000000..2fcc1d6aaee
--- /dev/null
+++ b/src/com/android/settings/remoteauth/enrolling/DiscoveredAuthenticatorUiState.kt
@@ -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,
+)
\ No newline at end of file
diff --git a/src/com/android/settings/remoteauth/enrolling/EnrollmentUiState.kt b/src/com/android/settings/remoteauth/enrolling/EnrollmentUiState.kt
new file mode 100644
index 00000000000..e8c8a11db2f
--- /dev/null
+++ b/src/com/android/settings/remoteauth/enrolling/EnrollmentUiState.kt
@@ -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,
+}
\ No newline at end of file
diff --git a/src/com/android/settings/remoteauth/enrolling/RemoteAuthEnrollEnrolling.kt b/src/com/android/settings/remoteauth/enrolling/RemoteAuthEnrollEnrolling.kt
new file mode 100644
index 00000000000..4569760e8b9
--- /dev/null
+++ b/src/com/android/settings/remoteauth/enrolling/RemoteAuthEnrollEnrolling.kt
@@ -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(R.id.enrolling_list_progress_bar)
+ }
+ private val errorText by lazy { view!!.findViewById(R.id.error_text) }
+ private val recyclerView by lazy {
+ view!!.findViewById(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 = ""
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/settings/remoteauth/enrolling/RemoteAuthEnrollEnrollingRecyclerViewAdapter.kt b/src/com/android/settings/remoteauth/enrolling/RemoteAuthEnrollEnrollingRecyclerViewAdapter.kt
new file mode 100644
index 00000000000..81b88018d9a
--- /dev/null
+++ b/src/com/android/settings/remoteauth/enrolling/RemoteAuthEnrollEnrollingRecyclerViewAdapter.kt
@@ -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() {
+ 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_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() }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/settings/remoteauth/enrolling/RemoteAuthEnrollEnrollingUiState.kt b/src/com/android/settings/remoteauth/enrolling/RemoteAuthEnrollEnrollingUiState.kt
new file mode 100644
index 00000000000..285edf25ed1
--- /dev/null
+++ b/src/com/android/settings/remoteauth/enrolling/RemoteAuthEnrollEnrollingUiState.kt
@@ -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 = 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,
+)
\ No newline at end of file
diff --git a/src/com/android/settings/remoteauth/enrolling/RemoteAuthEnrollEnrollingViewModel.kt b/src/com/android/settings/remoteauth/enrolling/RemoteAuthEnrollEnrollingViewModel.kt
new file mode 100644
index 00000000000..c06862f6bf3
--- /dev/null
+++ b/src/com/android/settings/remoteauth/enrolling/RemoteAuthEnrollEnrollingViewModel.kt
@@ -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 = _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()
+
+ _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.
+ }
+}
\ No newline at end of file
diff --git a/tests/robotests/src/com/android/settings/remoteauth/enrolling/RemoteAuthEnrollEnrollingTest.kt b/tests/robotests/src/com/android/settings/remoteauth/enrolling/RemoteAuthEnrollEnrollingTest.kt
new file mode 100644
index 00000000000..d10330512da
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/remoteauth/enrolling/RemoteAuthEnrollEnrollingTest.kt
@@ -0,0 +1,65 @@
+/*
+ * 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 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.hamcrest.core.IsNot.not
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+
+@RunWith(RobolectricTestRunner::class)
+class RemoteAuthEnrollEnrollingTest {
+ @Before
+ fun setup() {
+ launchFragmentInContainer(Bundle(), R.style.SudThemeGlif)
+ }
+
+ @Test
+ fun testRemoteAuthenticatorEnrollEnrolling_hasHeader() {
+ onView(withText(R.string.security_settings_remoteauth_enroll_enrolling_title)).check(
+ matches(
+ isDisplayed()
+ )
+ )
+ }
+
+ @Test
+ fun testRemoteAuthenticatorEnrollEnrolling_primaryButtonDisabled() {
+ onView(withText(R.string.security_settings_remoteauth_enroll_enrolling_agree)).check(
+ matches(
+ isNotEnabled()
+ )
+ )
+ }
+
+ @Test
+ fun testRemoteAuthenticatorEnrollEnrolling_progressBarNotDisplayed() {
+ onView(withId(R.id.enrolling_list_progress_bar)).check(matches(not(isDisplayed())))
+ }
+
+ @Test
+ fun testRemoteAuthenticatorEnrollEnrolling_errorTextNotDisplayed() {
+ onView(withId(R.id.error_text)).check(matches(not(isDisplayed())))
+ }
+}
\ No newline at end of file