Remote authenticator enrollment introduction layout.
This flow will be included in Device Unlock settings with the Fingerprint and Face Unlock. Bug: b/293908278 Test: atest RemoteAuthEnrollIntroductionTest Change-Id: Ia20662e897f925d82547550e25b79a2e61d2dc34
This commit is contained in:
@@ -0,0 +1,159 @@
|
||||
/*
|
||||
* 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.introduction
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.viewpager2.widget.MarginPageTransformer
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
|
||||
import com.airbnb.lottie.LottieAnimationView
|
||||
|
||||
import com.android.settings.R
|
||||
import com.android.settingslib.widget.LottieColorUtils
|
||||
|
||||
class IntroductionImageCarousel : ConstraintLayout {
|
||||
private val carousel: ViewPager2 by lazy { findViewById<ViewPager2>(R.id.image_carousel) }
|
||||
private val progressIndicator: RecyclerView by lazy {
|
||||
findViewById<RecyclerView>(R.id.carousel_progress_indicator)
|
||||
}
|
||||
private val backArrow: ImageView by lazy { findViewById<ImageView>(R.id.carousel_back_arrow) }
|
||||
private val forwardArrow: ImageView by lazy {
|
||||
findViewById<ImageView>(R.id.carousel_forward_arrow)
|
||||
}
|
||||
private val progressIndicatorAdapter = ProgressIndicatorAdapter()
|
||||
// The index of the current animation we are on
|
||||
private var currentPage = 0
|
||||
set(value) {
|
||||
val pageRange = 0..(ANIMATION_LIST.size - 1)
|
||||
field = value.coerceIn(pageRange)
|
||||
backArrow.isEnabled = field > pageRange.start
|
||||
forwardArrow.isEnabled = field < pageRange.endInclusive
|
||||
carousel.setCurrentItem(field)
|
||||
progressIndicatorAdapter.currentIndex = field
|
||||
}
|
||||
|
||||
private val onPageChangeCallback =
|
||||
object : ViewPager2.OnPageChangeCallback() {
|
||||
override fun onPageSelected(position: Int) {
|
||||
currentPage = position
|
||||
}
|
||||
}
|
||||
constructor(context: Context) : super(context)
|
||||
constructor(context: Context, attrSet: AttributeSet?) : super(context, attrSet)
|
||||
|
||||
init {
|
||||
LayoutInflater.from(context).inflate(R.layout.remote_auth_introduction_image_carousel, this)
|
||||
|
||||
with(carousel) {
|
||||
setPageTransformer(
|
||||
MarginPageTransformer(
|
||||
context.resources.getDimension(R.dimen.remoteauth_introduction_fragment_padding_horizontal).toInt()
|
||||
)
|
||||
)
|
||||
adapter = ImageCarouselAdapter()
|
||||
registerOnPageChangeCallback(onPageChangeCallback)
|
||||
}
|
||||
|
||||
with(progressIndicator) {
|
||||
layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
|
||||
adapter = progressIndicatorAdapter
|
||||
}
|
||||
|
||||
backArrow.setOnClickListener { currentPage-- }
|
||||
forwardArrow.setOnClickListener { currentPage++ }
|
||||
}
|
||||
|
||||
fun unregister() {
|
||||
carousel.unregisterOnPageChangeCallback(onPageChangeCallback)
|
||||
}
|
||||
|
||||
private class AnimationViewHolder(val context: Context, itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
val animationView = itemView.requireViewById<LottieAnimationView>(R.id.explanation_animation)
|
||||
val descriptionText = itemView.requireViewById<TextView>(R.id.carousel_text)
|
||||
}
|
||||
|
||||
/** Adapter for the onboarding animations. */
|
||||
private class ImageCarouselAdapter : RecyclerView.Adapter<AnimationViewHolder>() {
|
||||
|
||||
override fun getItemCount() = ANIMATION_LIST.size
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
|
||||
AnimationViewHolder(parent.context, LayoutInflater.from(parent.context).inflate(R.layout.remote_auth_introduction_image_carousel_item, parent, false))
|
||||
|
||||
override fun onBindViewHolder(holder: AnimationViewHolder, position: Int) {
|
||||
with(holder.animationView) {
|
||||
setAnimation(ANIMATION_LIST[position].first)
|
||||
LottieColorUtils.applyDynamicColors(holder.context, this)
|
||||
}
|
||||
holder.descriptionText.setText(ANIMATION_LIST[position].second)
|
||||
with(holder.itemView) {
|
||||
// This makes sure that the proper description text instead of a generic "Page" label is
|
||||
// verbalized by Talkback when switching to a new page on the ViewPager2.
|
||||
contentDescription = context.getString(ANIMATION_LIST[position].second)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Adapter for icons indicating carousel progress. */
|
||||
private class ProgressIndicatorAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||
|
||||
var currentIndex: Int = 0
|
||||
set(value) {
|
||||
val previousIndex = field
|
||||
field = value.coerceIn(0, getItemCount() - 1)
|
||||
notifyItemChanged(previousIndex)
|
||||
notifyItemChanged(field)
|
||||
}
|
||||
|
||||
override fun getItemCount() = ANIMATION_LIST.size
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
|
||||
object :
|
||||
RecyclerView.ViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.remote_auth_introduction_image_carousel_progress_icon, parent, false)) {}
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
holder.itemView.isSelected = position == currentIndex
|
||||
}
|
||||
}
|
||||
companion object {
|
||||
@VisibleForTesting
|
||||
val ANIMATION_LIST =
|
||||
listOf(
|
||||
Pair(
|
||||
R.raw.remoteauth_explanation_swipe_animation,
|
||||
R.string.security_settings_remoteauth_enroll_introduction_animation_swipe_up
|
||||
),
|
||||
Pair(
|
||||
R.raw.remoteauth_explanation_notification_animation,
|
||||
R.string.security_settings_remoteauth_enroll_introduction_animation_tap_notification
|
||||
),
|
||||
)
|
||||
const val TAG = "RemoteAuthCarousel"
|
||||
}
|
||||
}
|
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* 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.introduction
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import com.android.settings.R
|
||||
import com.android.settings.remoteauth.RemoteAuthEnrollBase
|
||||
import com.google.android.setupcompat.template.FooterButton
|
||||
import com.google.android.setupdesign.template.RequireScrollMixin
|
||||
|
||||
/**
|
||||
* Provides introductory info about remote authenticator unlock.
|
||||
*/
|
||||
class RemoteAuthEnrollIntroduction :
|
||||
RemoteAuthEnrollBase(
|
||||
layoutResId = R.layout.remote_auth_enroll_introduction,
|
||||
glifLayoutId = R.id.setup_wizard_layout,
|
||||
) {
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
viewGroup: ViewGroup?,
|
||||
savedInstanceArgs: Bundle?
|
||||
) =
|
||||
super.onCreateView(inflater, viewGroup, savedInstanceArgs).also {
|
||||
initializeRequireScrollMixin(it)
|
||||
}
|
||||
|
||||
|
||||
override fun initializePrimaryFooterButton() : FooterButton {
|
||||
return FooterButton.Builder(context!!)
|
||||
.setText(R.string.security_settings_remoteauth_enroll_introduction_agree)
|
||||
.setListener(::onPrimaryFooterButtonClick)
|
||||
.setButtonType(FooterButton.ButtonType.OPT_IN)
|
||||
.setTheme(R.style.SudGlifButton_Primary)
|
||||
.build()
|
||||
}
|
||||
|
||||
override fun initializeSecondaryFooterButton() : FooterButton {
|
||||
return FooterButton.Builder(context!!)
|
||||
.setText(R.string.security_settings_remoteauth_enroll_introduction_disagree)
|
||||
.setListener(::onSecondaryFooterButtonClick)
|
||||
.setButtonType(FooterButton.ButtonType.NEXT)
|
||||
.setTheme(R.style.SudGlifButton_Primary)
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun onPrimaryFooterButtonClick(view: View) {
|
||||
// TODO(b/293906345): Wire up navigation
|
||||
}
|
||||
|
||||
private fun onSecondaryFooterButtonClick(view: View) {
|
||||
// TODO(b/293906345): Wire up navigation
|
||||
}
|
||||
|
||||
private fun initializeRequireScrollMixin(view: View) {
|
||||
val layout = 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.setOnRequireScrollStateChangedListener { scrollNeeded ->
|
||||
if (scrollNeeded) {
|
||||
primaryFooterButton.setText(requireContext(), R.string.security_settings_remoteauth_enroll_introduction_more)
|
||||
} else {
|
||||
primaryFooterButton.setText(requireContext(), R.string.security_settings_remoteauth_enroll_introduction_agree)
|
||||
secondaryFooterButton?.visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val TAG = "RemoteAuthEnrollIntro"
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user