1 /*
2  * Copyright (C) 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.settings.remoteauth.introduction
18 
19 import android.content.Context
20 import android.util.AttributeSet
21 import android.util.Log
22 import android.view.LayoutInflater
23 import android.view.View
24 import android.view.ViewGroup
25 import android.widget.ImageView
26 import android.widget.TextView
27 
28 import androidx.annotation.VisibleForTesting
29 import androidx.constraintlayout.widget.ConstraintLayout
30 import androidx.recyclerview.widget.LinearLayoutManager
31 import androidx.recyclerview.widget.RecyclerView
32 import androidx.viewpager2.widget.MarginPageTransformer
33 import androidx.viewpager2.widget.ViewPager2
34 
35 import com.airbnb.lottie.LottieAnimationView
36 
37 import com.android.settings.R
38 import com.android.settingslib.widget.LottieColorUtils
39 
40 class IntroductionImageCarousel : ConstraintLayout {
<lambda>null41     private val carousel: ViewPager2 by lazy { requireViewById<ViewPager2>(R.id.image_carousel) }
<lambda>null42     private val progressIndicator: RecyclerView by lazy {
43         requireViewById<RecyclerView>(R.id.carousel_progress_indicator)
44     }
<lambda>null45     private val backArrow: ImageView by lazy {
46         requireViewById<ImageView>(R.id.carousel_back_arrow)
47     }
<lambda>null48     private val forwardArrow: ImageView by lazy {
49         requireViewById<ImageView>(R.id.carousel_forward_arrow)
50     }
51     private val progressIndicatorAdapter = ProgressIndicatorAdapter()
52     // The index of the current animation we are on
53     private var currentPage = 0
54         set(value) {
55             val pageRange = 0..(ANIMATION_LIST.size - 1)
56             field = value.coerceIn(pageRange)
57             backArrow.isEnabled = field > pageRange.start
58             forwardArrow.isEnabled = field < pageRange.endInclusive
59             carousel.setCurrentItem(field)
60             progressIndicatorAdapter.currentIndex = field
61         }
62 
63     private val onPageChangeCallback =
64         object : ViewPager2.OnPageChangeCallback() {
onPageSelectednull65             override fun onPageSelected(position: Int) {
66                 currentPage = position
67             }
68         }
69     constructor(context: Context) : super(context)
70     constructor(context: Context, attrSet: AttributeSet?) : super(context, attrSet)
71 
72     init {
73         LayoutInflater.from(context).inflate(R.layout.remote_auth_introduction_image_carousel, this)
74 
<lambda>null75         with(carousel) {
76             setPageTransformer(
77                 MarginPageTransformer(
78                     context.resources.getDimension(R.dimen.remoteauth_introduction_fragment_padding_horizontal).toInt()
79                 )
80             )
81             adapter = ImageCarouselAdapter()
82             registerOnPageChangeCallback(onPageChangeCallback)
83         }
84 
<lambda>null85         with(progressIndicator) {
86             layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
87             adapter = progressIndicatorAdapter
88         }
89 
<lambda>null90         backArrow.setOnClickListener { currentPage-- }
<lambda>null91         forwardArrow.setOnClickListener { currentPage++ }
92     }
93 
unregisternull94     fun unregister() {
95         carousel.unregisterOnPageChangeCallback(onPageChangeCallback)
96     }
97 
98     private class AnimationViewHolder(val context: Context, itemView: View) : RecyclerView.ViewHolder(itemView) {
99         val animationView = itemView.requireViewById<LottieAnimationView>(R.id.explanation_animation)
100         val descriptionText = itemView.requireViewById<TextView>(R.id.carousel_text)
101     }
102 
103     /** Adapter for the onboarding animations. */
104     private class ImageCarouselAdapter : RecyclerView.Adapter<AnimationViewHolder>() {
105 
getItemCountnull106         override fun getItemCount() = ANIMATION_LIST.size
107 
108         override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
109             AnimationViewHolder(parent.context, LayoutInflater.from(parent.context).inflate(R.layout.remote_auth_introduction_image_carousel_item, parent, false))
110 
111         override fun onBindViewHolder(holder: AnimationViewHolder, position: Int) {
112             with(holder.animationView) {
113                 setAnimation(ANIMATION_LIST[position].first)
114                 LottieColorUtils.applyDynamicColors(holder.context, this)
115             }
116             holder.descriptionText.setText(ANIMATION_LIST[position].second)
117             with(holder.itemView) {
118                 // This makes sure that the proper description text instead of a generic "Page" label is
119                 // verbalized by Talkback when switching to a new page on the ViewPager2.
120                 contentDescription = context.getString(ANIMATION_LIST[position].second)
121             }
122         }
123     }
124 
125     /** Adapter for icons indicating carousel progress. */
126     private class ProgressIndicatorAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
127 
128         var currentIndex: Int = 0
129             set(value) {
130                 val previousIndex = field
131                 field = value.coerceIn(0, getItemCount() - 1)
132                 notifyItemChanged(previousIndex)
133                 notifyItemChanged(field)
134             }
135 
getItemCountnull136         override fun getItemCount() = ANIMATION_LIST.size
137 
138         override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
139             object :
140                 RecyclerView.ViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.remote_auth_introduction_image_carousel_progress_icon, parent, false)) {}
141 
onBindViewHoldernull142         override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
143             holder.itemView.isSelected = position == currentIndex
144         }
145     }
146     companion object {
147         @VisibleForTesting
148         val ANIMATION_LIST =
149             listOf(
150                 Pair(
151                     R.raw.remoteauth_explanation_swipe_animation,
152                     R.string.security_settings_remoteauth_enroll_introduction_animation_swipe_up
153                 ),
154                 Pair(
155                     R.raw.remoteauth_explanation_notification_animation,
156                     R.string.security_settings_remoteauth_enroll_introduction_animation_tap_notification
157                 ),
158             )
159         const val TAG = "RemoteAuthCarousel"
160     }
161 }
162