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