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 package com.android.launcher3.taskbar
17 
18 import android.os.Bundle
19 import android.view.View
20 import android.view.View.GONE
21 import android.view.View.VISIBLE
22 import android.view.ViewGroup.LayoutParams.MATCH_PARENT
23 import android.view.ViewGroup.MarginLayoutParams
24 import android.view.accessibility.AccessibilityEvent
25 import android.view.accessibility.AccessibilityNodeInfo
26 import androidx.annotation.IntDef
27 import androidx.annotation.LayoutRes
28 import androidx.core.view.updateLayoutParams
29 import com.airbnb.lottie.LottieAnimationView
30 import com.android.launcher3.R
31 import com.android.launcher3.Utilities
32 import com.android.launcher3.taskbar.TaskbarAutohideSuspendController.FLAG_AUTOHIDE_SUSPEND_EDU_OPEN
33 import com.android.launcher3.taskbar.TaskbarControllers.LoggableTaskbarController
34 import com.android.launcher3.util.DisplayController
35 import com.android.launcher3.util.OnboardingPrefs.TASKBAR_EDU_TOOLTIP_STEP
36 import com.android.quickstep.util.LottieAnimationColorUtils
37 import java.io.PrintWriter
38 
39 /** First EDU step for swiping up to show transient Taskbar. */
40 const val TOOLTIP_STEP_SWIPE = 0
41 /** Second EDU step for explaining Taskbar functionality when unstashed. */
42 const val TOOLTIP_STEP_FEATURES = 1
43 /**
44  * EDU is completed.
45  *
46  * This value should match the maximum count for [TASKBAR_EDU_TOOLTIP_STEP].
47  */
48 const val TOOLTIP_STEP_NONE = 2
49 
50 /** Current step in the tooltip EDU flow. */
51 @Retention(AnnotationRetention.SOURCE)
52 @IntDef(TOOLTIP_STEP_SWIPE, TOOLTIP_STEP_FEATURES, TOOLTIP_STEP_NONE)
53 annotation class TaskbarEduTooltipStep
54 
55 /** Controls stepping through the Taskbar tooltip EDU. */
56 class TaskbarEduTooltipController(val activityContext: TaskbarActivityContext) :
57     LoggableTaskbarController {
58 
59     private val isTooltipEnabled: Boolean
60         get() = !Utilities.isRunningInTestHarness()
61     private val isOpen: Boolean
62         get() = tooltip?.isOpen ?: false
63     val isBeforeTooltipFeaturesStep: Boolean
64         get() = isTooltipEnabled && tooltipStep <= TOOLTIP_STEP_FEATURES
65     private lateinit var controllers: TaskbarControllers
66 
67     @TaskbarEduTooltipStep
68     var tooltipStep: Int
69         get() {
70             return activityContext.onboardingPrefs?.getCount(TASKBAR_EDU_TOOLTIP_STEP)
71                 ?: TOOLTIP_STEP_NONE
72         }
73         private set(step) {
74             activityContext.onboardingPrefs?.setEventCount(step, TASKBAR_EDU_TOOLTIP_STEP)
75         }
76 
77     private var tooltip: TaskbarEduTooltip? = null
78 
initnull79     fun init(controllers: TaskbarControllers) {
80         this.controllers = controllers
81     }
82 
83     /** Shows swipe EDU tooltip if it is the current [tooltipStep]. */
maybeShowSwipeEdunull84     fun maybeShowSwipeEdu() {
85         if (
86             !isTooltipEnabled ||
87                 !DisplayController.isTransientTaskbar(activityContext) ||
88                 tooltipStep > TOOLTIP_STEP_SWIPE
89         ) {
90             return
91         }
92 
93         tooltipStep = TOOLTIP_STEP_FEATURES
94         inflateTooltip(R.layout.taskbar_edu_swipe)
95         tooltip?.run {
96             findViewById<LottieAnimationView>(R.id.swipe_animation).supportLightTheme()
97             show()
98         }
99     }
100 
101     /**
102      * Shows feature EDU tooltip if this step has not been seen.
103      *
104      * If [TOOLTIP_STEP_SWIPE] has not been seen at this point, the first step is skipped because a
105      * swipe up is necessary to show this step.
106      */
maybeShowFeaturesEdunull107     fun maybeShowFeaturesEdu() {
108         if (!isTooltipEnabled || tooltipStep > TOOLTIP_STEP_FEATURES) {
109             return
110         }
111 
112         tooltipStep = TOOLTIP_STEP_NONE
113         inflateTooltip(R.layout.taskbar_edu_features)
114         tooltip?.run {
115             val splitscreenAnim = findViewById<LottieAnimationView>(R.id.splitscreen_animation)
116             val suggestionsAnim = findViewById<LottieAnimationView>(R.id.suggestions_animation)
117             val settingsAnim = findViewById<LottieAnimationView>(R.id.settings_animation)
118             val settingsEdu = findViewById<View>(R.id.settings_edu)
119             splitscreenAnim.supportLightTheme()
120             suggestionsAnim.supportLightTheme()
121             settingsAnim.supportLightTheme()
122             if (DisplayController.isTransientTaskbar(activityContext)) {
123                 splitscreenAnim.setAnimation(R.raw.taskbar_edu_splitscreen_transient)
124                 suggestionsAnim.setAnimation(R.raw.taskbar_edu_suggestions_transient)
125                 settingsEdu.visibility = GONE
126             } else {
127                 splitscreenAnim.setAnimation(R.raw.taskbar_edu_splitscreen_persistent)
128                 suggestionsAnim.setAnimation(R.raw.taskbar_edu_suggestions_persistent)
129                 settingsEdu.visibility = VISIBLE
130             }
131 
132             // Set up layout parameters.
133             content.updateLayoutParams { width = MATCH_PARENT }
134             updateLayoutParams<MarginLayoutParams> {
135                 if (DisplayController.isTransientTaskbar(activityContext)) {
136                     width =
137                         resources.getDimensionPixelSize(
138                             R.dimen.taskbar_edu_features_tooltip_width_transient
139                         )
140                     bottomMargin += activityContext.deviceProfile.taskbarHeight
141                 } else {
142                     width =
143                         resources.getDimensionPixelSize(
144                             R.dimen.taskbar_edu_features_tooltip_width_persistent
145                         )
146                 }
147             }
148 
149             findViewById<View>(R.id.done_button)?.setOnClickListener { hide() }
150             show()
151         }
152     }
153 
154     /** Closes the current [tooltip]. */
hidenull155     fun hide() = tooltip?.close(true)
156 
157     /** Initializes [tooltip] with content from [contentResId]. */
158     private fun inflateTooltip(@LayoutRes contentResId: Int) {
159         val overlayContext = controllers.taskbarOverlayController.requestWindow()
160         val tooltip =
161             overlayContext.layoutInflater.inflate(
162                 R.layout.taskbar_edu_tooltip,
163                 overlayContext.dragLayer,
164                 false
165             ) as TaskbarEduTooltip
166 
167         controllers.taskbarAutohideSuspendController.updateFlag(
168             FLAG_AUTOHIDE_SUSPEND_EDU_OPEN,
169             true
170         )
171 
172         tooltip.onCloseCallback = {
173             this.tooltip = null
174             controllers.taskbarAutohideSuspendController.updateFlag(
175                 FLAG_AUTOHIDE_SUSPEND_EDU_OPEN,
176                 false
177             )
178             controllers.taskbarStashController.updateAndAnimateTransientTaskbar(true)
179         }
180         tooltip.accessibilityDelegate = createAccessibilityDelegate()
181 
182         overlayContext.layoutInflater.inflate(contentResId, tooltip.content, true)
183         this.tooltip = tooltip
184     }
185 
createAccessibilityDelegatenull186     private fun createAccessibilityDelegate() =
187         object : View.AccessibilityDelegate() {
188             override fun performAccessibilityAction(
189                 host: View?,
190                 action: Int,
191                 args: Bundle?
192             ): Boolean {
193                 if (action == R.id.close) {
194                     hide()
195                     return true
196                 }
197                 return super.performAccessibilityAction(host, action, args)
198             }
199 
200             override fun onPopulateAccessibilityEvent(host: View?, event: AccessibilityEvent?) {
201                 super.onPopulateAccessibilityEvent(host, event)
202                 if (event?.eventType == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {
203                     event.text?.add(host?.context?.getText(R.string.taskbar_edu_a11y_title))
204                 }
205             }
206 
207             override fun onInitializeAccessibilityNodeInfo(
208                 host: View?,
209                 info: AccessibilityNodeInfo?
210             ) {
211                 super.onInitializeAccessibilityNodeInfo(host, info)
212                 info?.addAction(
213                     AccessibilityNodeInfo.AccessibilityAction(
214                         R.id.close,
215                         host?.context?.getText(R.string.taskbar_edu_close)
216                     )
217                 )
218             }
219         }
220 
dumpLogsnull221     override fun dumpLogs(prefix: String?, pw: PrintWriter?) {
222         pw?.println(prefix + "TaskbarEduTooltipController:")
223         pw?.println("$prefix\tisTooltipEnabled=$isTooltipEnabled")
224         pw?.println("$prefix\tisOpen=$isOpen")
225         pw?.println("$prefix\ttooltipStep=$tooltipStep")
226     }
227 }
228 
229 /**
230  * Maps colors in the dark-themed Lottie assets to their light-themed equivalents.
231  *
232  * For instance, `".blue100" to R.color.lottie_blue400` means objects that are material blue100 in
233  * dark theme should be changed to material blue400 in light theme.
234  */
235 private val DARK_TO_LIGHT_COLORS =
236     mapOf(
237         ".blue100" to R.color.lottie_blue400,
238         ".blue400" to R.color.lottie_blue600,
239         ".green100" to R.color.lottie_green400,
240         ".green400" to R.color.lottie_green600,
241         ".grey300" to R.color.lottie_grey600,
242         ".grey400" to R.color.lottie_grey700,
243         ".grey800" to R.color.lottie_grey200,
244         ".red400" to R.color.lottie_red600,
245         ".yellow100" to R.color.lottie_yellow400,
246         ".yellow400" to R.color.lottie_yellow600,
247     )
248 
LottieAnimationViewnull249 private fun LottieAnimationView.supportLightTheme() {
250     if (Utilities.isDarkTheme(context)) {
251         return
252     }
253 
254     LottieAnimationColorUtils.updateColors(this, DARK_TO_LIGHT_COLORS, context.theme)
255 }
256