Remote authenticator enrollment navigation.

This change adds an entry point for Remote Auth enrollment and
navigation through the flow.

Bug: b/293906345
Test: make RunSettingsRoboTests -j40
Change-Id: I9bc803e24c4181ed73a0ef90b171897c677e71d1
This commit is contained in:
Justin McClain
2023-08-16 20:53:02 +00:00
parent 0127c488e4
commit 7e837fae82
15 changed files with 329 additions and 21 deletions

View File

@@ -71,6 +71,8 @@ android_library {
"androidx.cardview_cardview",
"androidx.compose.runtime_runtime-livedata",
"androidx.activity_activity-ktx",
"androidx.navigation_navigation-fragment-ktx",
"androidx.navigation_navigation-ui-ktx",
"androidx.preference_preference",
"androidx.recyclerview_recyclerview",
"androidx.window_window",

View File

@@ -2610,6 +2610,18 @@
<activity android:name=".biometrics.activeunlock.ActiveUnlockRequireBiometricSetup" android:exported="false"/>
<activity android:name=".remoteauth.RemoteAuthActivity"
android:exported="true">
<intent-filter>
<action android:name="android.settings.REMOTE_AUTHENTICATOR_ENROLL" />
<category android:name="android.intent.category.BROWSABLE" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<activity android:name=".remoteauth.RemoteAuthActivityInternal"
android:exported="false"
android:taskAffinity="com.android.settings.root" />
<!-- Note this must not be exported since it returns the password in the intent -->
<activity android:name=".password.ConfirmLockPattern$InternalActivity"
android:exported="false"

View File

@@ -0,0 +1,33 @@
<?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.
-->
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:ignore="MergeRootFrame">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true" />
</FrameLayout>

View File

@@ -0,0 +1,53 @@
<?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.
-->
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/remote_auth_navigation">
<fragment android:id="@+id/remote_auth_enroll_introduction_fragment"
android:name="com.android.settings.remoteauth.introduction.RemoteAuthEnrollIntroduction"
android:label="fragment_enroll_introduction">
<action
android:id="@+id/action_introduction_to_enrolling"
app:destination="@id/remote_auth_enroll_enrolling_fragment"/>
</fragment>
<fragment android:id="@+id/remote_auth_enroll_enrolling_fragment"
android:name="com.android.settings.remoteauth.enrolling.RemoteAuthEnrollEnrolling"
android:label="fragment_enroll_enrolling">
<action
android:id="@+id/action_enrolling_to_finish"
app:destination="@id/remote_auth_enroll_finish_fragment"/>
</fragment>
<fragment android:id="@+id/remote_auth_enroll_finish_fragment"
android:name="com.android.settings.remoteauth.finish.RemoteAuthEnrollFinish"
android:label="fragment_enroll_finish">
<action
android:id="@+id/action_finish_to_settings"
app:destination="@id/remote_auth_settings_fragment"/>
</fragment>
<fragment android:id="@+id/remote_auth_settings_fragment"
android:name="com.android.settings.remoteauth.settings.RemoteAuthSettings"
android:label="fragment_settings">
<action
android:id="@+id/action_settings_to_introduction"
app:destination="@id/remote_auth_enroll_introduction_fragment"/>
</fragment>
</navigation>

View File

@@ -887,6 +887,10 @@
<string name="security_settings_fingerprint_multiple_face_watch_preference_summary">Face, fingerprints, and <xliff:g id="watch" example="Dani's Watch">%s</xliff:g> added</string>
<!-- RemoteAuth unlock enrollment and settings --><skip />
<!-- Title shown for menu item that launches watch unlock settings. [CHAR LIMIT=40] -->
<string name ="security_settings_remoteauth_preference_title">Remote Authenticator Unlock</string>
<!-- Message shown in summary field when remote authenticator is set up. [CHAR LIMIT=40] -->
<string name="security_settings_remoteauth_preference_summary">Watch added</string>
<!-- Strings for RemoteAuth enroll introduction page -->
<!-- Introduction title shown in remote enrollment to introduce the remote feature [CHAR LIMIT=29] -->
<string name="security_settings_remoteauth_enroll_introduction_title">Set up your watch</string>

View File

@@ -47,6 +47,13 @@
android:summary="@string/summary_placeholder"
settings:keywords="@string/keywords_active_unlock_settings"
settings:controller="com.android.settings.biometrics.activeunlock.ActiveUnlockStatusPreferenceController" />
<com.android.settingslib.RestrictedPreference
android:key="biometric_remote_authenticator_settings"
android:title="@string/security_settings_remoteauth_preference_title"
android:summary="@string/summary_placeholder"
settings:keywords="@string/keywords_active_unlock_settings"
settings:controller="com.android.settings.remoteauth.RemoteAuthStatusPreferenceController" />
</PreferenceCategory>
<PreferenceCategory

View File

@@ -49,6 +49,8 @@ import com.android.settings.homepage.DeepLinkHomepageActivityInternal;
import com.android.settings.homepage.SettingsHomepageActivity;
import com.android.settings.overlay.FeatureFactory;
import com.android.settings.password.ChooseLockPattern;
import com.android.settings.remoteauth.RemoteAuthActivity;
import com.android.settings.remoteauth.RemoteAuthActivityInternal;
import com.android.settingslib.users.AvatarPickerActivity;
import java.util.Collection;
@@ -257,6 +259,8 @@ public class ActivityEmbeddingRulesController {
addActivityFilter(activityFilters, FingerprintEnrollEnrolling.class);
addActivityFilter(activityFilters, FaceEnrollIntroductionInternal.class);
addActivityFilter(activityFilters, FaceEnrollIntroduction.class);
addActivityFilter(activityFilters, RemoteAuthActivity.class);
addActivityFilter(activityFilters, RemoteAuthActivityInternal.class);
addActivityFilter(activityFilters, AvatarPickerActivity.class);
addActivityFilter(activityFilters, ChooseLockPattern.class);
ActivityRule activityRule = new ActivityRule.Builder(activityFilters).setAlwaysExpand(true)

View File

@@ -0,0 +1,53 @@
/*
* 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
import android.os.Bundle
import androidx.annotation.IdRes
import androidx.navigation.fragment.NavHostFragment
import com.android.settings.R
import com.android.settings.SetupWizardUtils
import com.android.settings.core.InstrumentedActivity
import com.google.android.setupdesign.util.ThemeHelper
open class RemoteAuthActivity : InstrumentedActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setTheme(SetupWizardUtils.getTheme(this, getIntent()))
ThemeHelper.trySetDynamicColor(this)
setContentView(R.layout.remote_auth_root)
// TODO(b/290768873): Change to remote_auth_enroll_introduction_fragment if no device is
// enrolled.
initializeNavigation(R.id.remote_auth_settings_fragment)
}
override fun getMetricsCategory(): Int {
// TODO() Update frameworks/proto_logging/stats/enums/app/settings_enums.proto
return 0
}
private fun initializeNavigation(@IdRes startDestinationId: Int) {
val navHostFragment =
supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
val navController = navHostFragment.navController
val navGraph = navController.navInflater.inflate(R.navigation.remote_auth_navigation)
navGraph.setStartDestination(startDestinationId)
navController.graph = navGraph
}
}

View File

@@ -0,0 +1,22 @@
/*
* 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
/**
* Wrapper of {@link RemoteAuthActivity} to use with a pre-defined task affinity.
*/
class RemoteAuthActivityInternal : RemoteAuthActivity()

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
import android.content.Context
import android.util.FeatureFlagUtils
import com.android.settings.biometrics.BiometricStatusPreferenceController
class RemoteAuthStatusPreferenceController(
private val context: Context,
key: String = KEY_REMOTE_AUTHENTICATOR_SETTINGS
) : BiometricStatusPreferenceController(
context, key
) {
override fun isDeviceSupported(): Boolean {
// TODO(b/290768873): Change based on RemoteAuthManager.
return FeatureFlagUtils.isEnabled(
context,
FeatureFlagUtils.SETTINGS_REMOTEAUTH_ENROLLMENT_SETTINGS
)
}
override fun isHardwareSupported(): Boolean {
// TODO(b/290768873): Change based on RemoteAuthManager.
return FeatureFlagUtils.isEnabled(
context,
FeatureFlagUtils.SETTINGS_REMOTEAUTH_ENROLLMENT_SETTINGS
)
}
override fun getSummaryText() = RemoteAuthStatusUtils.getSummary(context)
override fun getSettingsClassName() = RemoteAuthStatusUtils.getSettingsClassName()
private companion object {
/**
* Preference key.
*
* This must match the key found in security_settings_combined_biometric.xml
**/
const val KEY_REMOTE_AUTHENTICATOR_SETTINGS = "biometric_remote_authenticator_settings"
}
}

View File

@@ -0,0 +1,39 @@
/*
* 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
import android.content.Context
import com.android.settings.R
/**
* Utilities for remoteauth details shared between Security Settings and Safety Center.
*/
object RemoteAuthStatusUtils {
/**
* Returns the summary of remote auth settings entity.
*/
fun getSummary(context: Context): String {
// TODO(b/290768873): Update text based on if authenticator is enrolled.
return context.resources.getString(R.string.security_settings_remoteauth_preference_summary)
}
/**
* Returns the class name of the Settings page corresponding to remote auth settings.
*/
fun getSettingsClassName() = RemoteAuthActivityInternal::class.java.name
}

View File

@@ -20,10 +20,13 @@ import android.os.Bundle
import android.view.View
import android.widget.ProgressBar
import android.widget.TextView
import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.navigation.fragment.NavHostFragment.Companion.findNavController
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
@@ -38,8 +41,8 @@ class RemoteAuthEnrollEnrolling :
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 viewModel: RemoteAuthEnrollEnrollingViewModel by viewModels()
private val navController by lazy { findNavController(this) }
private val adapter = RemoteAuthEnrollEnrollingRecyclerViewAdapter()
private val progressBar by lazy {
view!!.requireViewById<ProgressBar>(R.id.enrolling_list_progress_bar)
@@ -94,7 +97,7 @@ class RemoteAuthEnrollEnrolling :
}
private fun onSecondaryFooterButtonClick(view: View) {
// TODO(b/293906345): Wire up navigation
navController.navigateUp()
}
private fun updateUi(uiState: RemoteAuthEnrollEnrollingUiState) {
@@ -113,7 +116,7 @@ class RemoteAuthEnrollEnrolling :
EnrollmentUiState.ENROLLING -> {}
EnrollmentUiState.SUCCESS -> {
// TODO(b/293906345): Wire up navigation
navController.navigate(R.id.action_enrolling_to_finish)
}
}
if (uiState.errorMsg != null) {

View File

@@ -18,6 +18,7 @@ package com.android.settings.remoteauth.finish
import android.os.Bundle
import android.view.View
import androidx.navigation.fragment.NavHostFragment.Companion.findNavController
import com.airbnb.lottie.LottieAnimationView
import com.android.settings.R
import com.android.settings.remoteauth.RemoteAuthEnrollBase
@@ -35,7 +36,10 @@ class RemoteAuthEnrollFinish :
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
LottieColorUtils.applyDynamicColors(requireContext(), view.findViewById<LottieAnimationView>(R.id.enroll_finish_animation))
LottieColorUtils.applyDynamicColors(
requireContext(),
view.findViewById<LottieAnimationView>(R.id.enroll_finish_animation)
)
}
override fun initializePrimaryFooterButton(): FooterButton {
@@ -50,10 +54,10 @@ class RemoteAuthEnrollFinish :
override fun initializeSecondaryFooterButton(): FooterButton? = null
fun onPrimaryFooterButtonClick(view: View) {
// TODO(b/293906345): Wire up navigation
findNavController(this).navigate(R.id.action_finish_to_settings)
}
private companion object{
private companion object {
const val TAG = "RemoteAuthEnrollFinish"
}
}

View File

@@ -20,6 +20,7 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.navigation.fragment.NavHostFragment.Companion.findNavController
import com.android.settings.R
import com.android.settings.remoteauth.RemoteAuthEnrollBase
import com.google.android.setupcompat.template.FooterButton
@@ -33,6 +34,7 @@ class RemoteAuthEnrollIntroduction :
layoutResId = R.layout.remote_auth_enroll_introduction,
glifLayoutId = R.id.setup_wizard_layout,
) {
private val navController by lazy { findNavController(this) }
override fun onCreateView(
inflater: LayoutInflater,
@@ -44,7 +46,7 @@ class RemoteAuthEnrollIntroduction :
}
override fun initializePrimaryFooterButton() : FooterButton {
override fun initializePrimaryFooterButton(): FooterButton {
return FooterButton.Builder(context!!)
.setText(R.string.security_settings_remoteauth_enroll_introduction_agree)
.setListener(::onPrimaryFooterButtonClick)
@@ -53,7 +55,7 @@ class RemoteAuthEnrollIntroduction :
.build()
}
override fun initializeSecondaryFooterButton() : FooterButton {
override fun initializeSecondaryFooterButton(): FooterButton {
return FooterButton.Builder(context!!)
.setText(R.string.security_settings_remoteauth_enroll_introduction_disagree)
.setListener(::onSecondaryFooterButtonClick)
@@ -63,24 +65,34 @@ class RemoteAuthEnrollIntroduction :
}
private fun onPrimaryFooterButtonClick(view: View) {
// TODO(b/293906345): Wire up navigation
navController.navigate(R.id.action_introduction_to_enrolling)
}
private fun onSecondaryFooterButtonClick(view: View) {
// TODO(b/293906345): Wire up navigation
navController.navigateUp()
}
private fun initializeRequireScrollMixin(view: View) {
val layout = checkNotNull(getGlifLayout(view))
secondaryFooterButton?.visibility = View.INVISIBLE
val requireScrollMixin = layout.getMixin(RequireScrollMixin::class.java)
requireScrollMixin.requireScrollWithButton(requireContext(), primaryFooterButton,
R.string.security_settings_remoteauth_enroll_introduction_more, ::onPrimaryFooterButtonClick)
requireScrollMixin.requireScrollWithButton(
requireContext(),
primaryFooterButton,
R.string.security_settings_remoteauth_enroll_introduction_more,
::onPrimaryFooterButtonClick
)
requireScrollMixin.setOnRequireScrollStateChangedListener { scrollNeeded ->
if (scrollNeeded) {
primaryFooterButton.setText(requireContext(), R.string.security_settings_remoteauth_enroll_introduction_more)
primaryFooterButton.setText(
requireContext(),
R.string.security_settings_remoteauth_enroll_introduction_more
)
} else {
primaryFooterButton.setText(requireContext(), R.string.security_settings_remoteauth_enroll_introduction_agree)
primaryFooterButton.setText(
requireContext(),
R.string.security_settings_remoteauth_enroll_introduction_agree
)
secondaryFooterButton?.visibility = View.VISIBLE
}
}

View File

@@ -20,9 +20,11 @@ import android.os.Bundle
import android.view.View
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.navigation.fragment.NavHostFragment.Companion.findNavController
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.android.settings.R
@@ -30,8 +32,7 @@ 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()
val viewModel: RemoteAuthSettingsViewModel by viewModels()
private val adapter = RemoteAuthSettingsRecyclerViewAdapter()
private val recyclerView by lazy {
view!!.requireViewById<RecyclerView>(R.id.registered_authenticator_list)
@@ -47,6 +48,11 @@ class RemoteAuthSettings : Fragment(R.layout.remote_auth_settings) {
recyclerView.layoutManager = LinearLayoutManager(context)
recyclerView.adapter = adapter
// Add new remote authenticator click listener
addAuthenticatorLayout.setOnClickListener {
findNavController(this).navigate(R.id.action_settings_to_introduction)
}
// Collect UIState and update UI on changes.
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
@@ -55,10 +61,7 @@ class RemoteAuthSettings : Fragment(R.layout.remote_auth_settings) {
}
}
}
// Add new remote authenticator click listener
addAuthenticatorLayout.setOnClickListener {
// TODO(b/293906345): Wire up navigation
}
}
private fun updateUi(uiState: RemoteAuthSettingsUiState) {