1 /*
<lambda>null2  * 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.content.Context
19 import android.content.Intent
20 import android.net.Uri
21 import android.os.Bundle
22 import android.text.SpannableString
23 import android.text.method.LinkMovementMethod
24 import android.text.style.URLSpan
25 import android.view.Gravity
26 import android.view.View
27 import android.view.View.GONE
28 import android.view.View.VISIBLE
29 import android.view.ViewGroup.LayoutParams.MATCH_PARENT
30 import android.view.ViewGroup.MarginLayoutParams
31 import android.view.accessibility.AccessibilityEvent
32 import android.view.accessibility.AccessibilityNodeInfo
33 import android.widget.TextView
34 import androidx.annotation.IntDef
35 import androidx.annotation.LayoutRes
36 import androidx.core.text.HtmlCompat
37 import androidx.core.view.updateLayoutParams
38 import com.airbnb.lottie.LottieAnimationView
39 import com.android.launcher3.LauncherPrefs
40 import com.android.launcher3.R
41 import com.android.launcher3.Utilities
42 import com.android.launcher3.config.FeatureFlags.enableTaskbarPinning
43 import com.android.launcher3.taskbar.TaskbarAutohideSuspendController.FLAG_AUTOHIDE_SUSPEND_EDU_OPEN
44 import com.android.launcher3.taskbar.TaskbarControllers.LoggableTaskbarController
45 import com.android.launcher3.util.DisplayController
46 import com.android.launcher3.util.OnboardingPrefs.TASKBAR_EDU_TOOLTIP_STEP
47 import com.android.launcher3.util.OnboardingPrefs.TASKBAR_SEARCH_EDU_SEEN
48 import com.android.launcher3.util.ResourceBasedOverride
49 import com.android.launcher3.views.ActivityContext
50 import com.android.launcher3.views.BaseDragLayer
51 import com.android.quickstep.util.LottieAnimationColorUtils
52 import java.io.PrintWriter
53 
54 /** First EDU step for swiping up to show transient Taskbar. */
55 const val TOOLTIP_STEP_SWIPE = 0
56 /** Second EDU step for explaining Taskbar functionality when unstashed. */
57 const val TOOLTIP_STEP_FEATURES = 1
58 /** Third EDU step for explaining Taskbar pinning. */
59 const val TOOLTIP_STEP_PINNING = 2
60 
61 /**
62  * EDU is completed.
63  *
64  * This value should match the maximum count for [TASKBAR_EDU_TOOLTIP_STEP].
65  */
66 const val TOOLTIP_STEP_NONE = 3
67 /** The base URL for the Privacy Policy that will later be localized. */
68 private const val PRIVACY_POLICY_BASE_URL = "https://policies.google.com/privacy/embedded?hl="
69 /** The base URL for the Terms of Service that will later be localized. */
70 private const val TOS_BASE_URL = "https://policies.google.com/terms?hl="
71 
72 /** Current step in the tooltip EDU flow. */
73 @Retention(AnnotationRetention.SOURCE)
74 @IntDef(TOOLTIP_STEP_SWIPE, TOOLTIP_STEP_FEATURES, TOOLTIP_STEP_PINNING, TOOLTIP_STEP_NONE)
75 annotation class TaskbarEduTooltipStep
76 
77 /** Controls stepping through the Taskbar tooltip EDU. */
78 open class TaskbarEduTooltipController(context: Context) :
79     ResourceBasedOverride, LoggableTaskbarController {
80 
81     protected val activityContext: TaskbarActivityContext = ActivityContext.lookupContext(context)
82     open val shouldShowSearchEdu = false
83     private val isTooltipEnabled: Boolean
84         get() {
85             return !Utilities.isRunningInTestHarness() &&
86                 !activityContext.isPhoneMode &&
87                 !activityContext.isTinyTaskbar
88         }
89 
90     private val isOpen: Boolean
91         get() = tooltip?.isOpen ?: false
92 
93     val isBeforeTooltipFeaturesStep: Boolean
94         get() = isTooltipEnabled && tooltipStep <= TOOLTIP_STEP_FEATURES
95 
96     private lateinit var controllers: TaskbarControllers
97 
98     // Keep track of whether the user has seen the Search Edu
99     private var userHasSeenSearchEdu: Boolean
100         get() {
101             return TASKBAR_SEARCH_EDU_SEEN.get(activityContext)
102         }
103         private set(seen) {
104             LauncherPrefs.get(activityContext).put(TASKBAR_SEARCH_EDU_SEEN, seen)
105         }
106 
107     @TaskbarEduTooltipStep
108     var tooltipStep: Int
109         get() {
110             return TASKBAR_EDU_TOOLTIP_STEP.get(activityContext)
111         }
112         private set(step) {
113             TASKBAR_EDU_TOOLTIP_STEP.set(step, activityContext)
114         }
115 
116     private var tooltip: TaskbarEduTooltip? = null
117 
118     fun init(controllers: TaskbarControllers) {
119         this.controllers = controllers
120         // We want to show the Search Edu right after pinning the taskbar, so we post it here
121         activityContext.dragLayer.post { maybeShowSearchEdu() }
122     }
123 
124     /** Shows swipe EDU tooltip if it is the current [tooltipStep]. */
125     fun maybeShowSwipeEdu() {
126         if (
127             !isTooltipEnabled ||
128                 !DisplayController.isTransientTaskbar(activityContext) ||
129                 tooltipStep > TOOLTIP_STEP_SWIPE
130         ) {
131             return
132         }
133 
134         tooltipStep = TOOLTIP_STEP_FEATURES
135         inflateTooltip(R.layout.taskbar_edu_swipe)
136         tooltip?.run {
137             requireViewById<LottieAnimationView>(R.id.swipe_animation).supportLightTheme()
138             show()
139         }
140     }
141 
142     /**
143      * Shows feature EDU tooltip if this step has not been seen.
144      *
145      * If [TOOLTIP_STEP_SWIPE] has not been seen at this point, the first step is skipped because a
146      * swipe up is necessary to show this step.
147      */
148     fun maybeShowFeaturesEdu() {
149         if (!isTooltipEnabled || tooltipStep > TOOLTIP_STEP_FEATURES) {
150             maybeShowPinningEdu()
151             maybeShowSearchEdu()
152             return
153         }
154 
155         tooltipStep = TOOLTIP_STEP_NONE
156         inflateTooltip(R.layout.taskbar_edu_features)
157         tooltip?.run {
158             allowTouchDismissal = false
159             val splitscreenAnim = requireViewById<LottieAnimationView>(R.id.splitscreen_animation)
160             val suggestionsAnim = requireViewById<LottieAnimationView>(R.id.suggestions_animation)
161             val pinningAnim = requireViewById<LottieAnimationView>(R.id.pinning_animation)
162             val pinningEdu = requireViewById<View>(R.id.pinning_edu)
163             splitscreenAnim.supportLightTheme()
164             suggestionsAnim.supportLightTheme()
165             pinningAnim.supportLightTheme()
166             if (DisplayController.isTransientTaskbar(activityContext)) {
167                 splitscreenAnim.setAnimation(R.raw.taskbar_edu_splitscreen_transient)
168                 suggestionsAnim.setAnimation(R.raw.taskbar_edu_suggestions_transient)
169                 pinningEdu.visibility = if (enableTaskbarPinning()) VISIBLE else GONE
170             } else {
171                 splitscreenAnim.setAnimation(R.raw.taskbar_edu_splitscreen_persistent)
172                 suggestionsAnim.setAnimation(R.raw.taskbar_edu_suggestions_persistent)
173                 pinningEdu.visibility = GONE
174             }
175 
176             // Set up layout parameters.
177             content.updateLayoutParams { width = MATCH_PARENT }
178             updateLayoutParams<MarginLayoutParams> {
179                 if (DisplayController.isTransientTaskbar(activityContext)) {
180                     width =
181                         resources.getDimensionPixelSize(
182                             if (enableTaskbarPinning())
183                                 R.dimen.taskbar_edu_features_tooltip_width_with_three_features
184                             else R.dimen.taskbar_edu_features_tooltip_width_with_two_features
185                         )
186 
187                     bottomMargin += activityContext.deviceProfile.taskbarHeight
188                 } else {
189                     width =
190                         resources.getDimensionPixelSize(
191                             R.dimen.taskbar_edu_features_tooltip_width_with_two_features
192                         )
193                 }
194             }
195 
196             findViewById<View>(R.id.done_button)?.setOnClickListener { hide() }
197             show()
198         }
199     }
200 
201     /**
202      * Shows standalone Pinning EDU tooltip if this EDU has not been seen.
203      *
204      * We show this standalone edu if users have seen the previous version of taskbar education,
205      * which did not include the pinning feature.
206      */
207     private fun maybeShowPinningEdu() {
208         // use old value of tooltipStep that was set to the previous value of TOOLTIP_STEP_NONE (2
209         // for the original 2 edu steps) as a proxy to needing to show the separate pinning edu
210         if (
211             !enableTaskbarPinning() ||
212                 !DisplayController.isTransientTaskbar(activityContext) ||
213                 !isTooltipEnabled ||
214                 tooltipStep > TOOLTIP_STEP_PINNING ||
215                 tooltipStep < TOOLTIP_STEP_FEATURES
216         ) {
217             return
218         }
219         tooltipStep = TOOLTIP_STEP_NONE
220         inflateTooltip(R.layout.taskbar_edu_pinning)
221 
222         tooltip?.run {
223             allowTouchDismissal = true
224             requireViewById<LottieAnimationView>(R.id.standalone_pinning_animation)
225                 .supportLightTheme()
226 
227             updateLayoutParams<BaseDragLayer.LayoutParams> {
228                 if (DisplayController.isTransientTaskbar(activityContext)) {
229                     bottomMargin += activityContext.deviceProfile.taskbarHeight
230                 }
231                 // Unlike other tooltips, we want to align with taskbar divider rather than center.
232                 gravity = Gravity.BOTTOM
233                 marginStart = 0
234                 width =
235                     resources.getDimensionPixelSize(
236                         R.dimen.taskbar_edu_features_tooltip_width_with_one_feature
237                     )
238             }
239 
240             // Calculate the amount the tooltip must be shifted by to align with the taskbar divider
241             val taskbarDividerView = controllers.taskbarViewController.taskbarDividerView
242             val dividerLocation = taskbarDividerView.x + taskbarDividerView.width / 2
243             x = dividerLocation - layoutParams.width / 2
244 
245             show()
246         }
247     }
248 
249     /**
250      * Shows standalone Search EDU tooltip if this EDU has not been seen.
251      *
252      * We show this standalone edu for users to learn to how to trigger Search from the pinned
253      * taskbar
254      */
255     fun maybeShowSearchEdu() {
256         if (
257             !enableTaskbarPinning() ||
258                 !DisplayController.isPinnedTaskbar(activityContext) ||
259                 !isTooltipEnabled ||
260                 !shouldShowSearchEdu ||
261                 userHasSeenSearchEdu
262         ) {
263             return
264         }
265         userHasSeenSearchEdu = true
266         inflateTooltip(R.layout.taskbar_edu_search)
267         tooltip?.run {
268             allowTouchDismissal = true
269             requireViewById<LottieAnimationView>(R.id.search_edu_animation).supportLightTheme()
270             val eduSubtitle: TextView = requireViewById(R.id.search_edu_text)
271             showDisclosureText(eduSubtitle)
272             updateLayoutParams<BaseDragLayer.LayoutParams> {
273                 if (DisplayController.isTransientTaskbar(activityContext)) {
274                     bottomMargin += activityContext.deviceProfile.taskbarHeight
275                 }
276                 // Unlike other tooltips, we want to align with the all apps button rather than
277                 // center.
278                 gravity = Gravity.BOTTOM
279                 marginStart = 0
280                 width =
281                     resources.getDimensionPixelSize(
282                         R.dimen.taskbar_edu_features_tooltip_width_with_one_feature
283                     )
284             }
285 
286             // Calculate the amount the tooltip must be shifted by to align with the action key
287             val allAppsButtonView = controllers.taskbarViewController.allAppsButtonView
288             if (allAppsButtonView != null) {
289                 val allAppsIconLocation = allAppsButtonView.x + allAppsButtonView.width / 2
290                 x = allAppsIconLocation - layoutParams.width / 2
291             }
292 
293             show()
294         }
295     }
296 
297     /**
298      * Set up the provided TextView to display legal disclosures. The method takes locale into
299      * account to show the appropriate links to regional disclosures.
300      */
301     private fun TaskbarEduTooltip.showDisclosureText(
302         textView: TextView,
303         stringId: Int = R.string.taskbar_edu_search_disclosure,
304     ) {
305         val locale = resources.configuration.locales[0]
306         val text =
307             SpannableString(
308                 HtmlCompat.fromHtml(
309                     resources.getString(
310                         stringId,
311                         PRIVACY_POLICY_BASE_URL + locale.language,
312                         TOS_BASE_URL + locale.language,
313                     ),
314                     HtmlCompat.FROM_HTML_MODE_COMPACT,
315                 )
316             )
317         // Directly process URLSpan clicks
318         text.getSpans(0, text.length, URLSpan::class.java).forEach { urlSpan ->
319             val url: URLSpan =
320                 object : URLSpan(urlSpan.url) {
321                     override fun onClick(widget: View) {
322                         val uri = Uri.parse(urlSpan.url)
323                         val context = widget.context
324                         val intent =
325                             Intent(Intent.ACTION_VIEW, uri).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
326                         context.startActivity(intent)
327                     }
328                 }
329 
330             val spanStart = text.getSpanStart(urlSpan)
331             val spanEnd = text.getSpanEnd(urlSpan)
332             val spanFlags = text.getSpanFlags(urlSpan)
333             text.removeSpan(urlSpan)
334             text.setSpan(url, spanStart, spanEnd, spanFlags)
335         }
336         textView.text = text
337         textView.movementMethod = LinkMovementMethod.getInstance()
338     }
339 
340     /** Closes the current [tooltip]. */
341     fun hide() {
342         tooltip?.close(true)
343     }
344 
345     /** Initializes [tooltip] with content from [contentResId]. */
346     private fun inflateTooltip(@LayoutRes contentResId: Int) {
347         val overlayContext = controllers.taskbarOverlayController.requestWindow()
348         val tooltip =
349             overlayContext.layoutInflater.inflate(
350                 R.layout.taskbar_edu_tooltip,
351                 overlayContext.dragLayer,
352                 false
353             ) as TaskbarEduTooltip
354 
355         controllers.taskbarAutohideSuspendController.updateFlag(
356             FLAG_AUTOHIDE_SUSPEND_EDU_OPEN,
357             true
358         )
359 
360         tooltip.onCloseCallback = {
361             this.tooltip = null
362             controllers.taskbarAutohideSuspendController.updateFlag(
363                 FLAG_AUTOHIDE_SUSPEND_EDU_OPEN,
364                 false
365             )
366             controllers.taskbarStashController.updateAndAnimateTransientTaskbar(true)
367         }
368         tooltip.accessibilityDelegate = createAccessibilityDelegate()
369 
370         overlayContext.layoutInflater.inflate(contentResId, tooltip.content, true)
371         this.tooltip = tooltip
372     }
373 
374     private fun createAccessibilityDelegate() =
375         object : View.AccessibilityDelegate() {
376             override fun performAccessibilityAction(
377                 host: View,
378                 action: Int,
379                 args: Bundle?
380             ): Boolean {
381                 if (action == R.id.close) {
382                     hide()
383                     return true
384                 }
385                 return super.performAccessibilityAction(host, action, args)
386             }
387 
388             override fun onPopulateAccessibilityEvent(host: View, event: AccessibilityEvent) {
389                 super.onPopulateAccessibilityEvent(host, event)
390                 if (event.eventType == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {
391                     event.text.add(host.context?.getText(R.string.taskbar_edu_a11y_title))
392                 }
393             }
394 
395             override fun onInitializeAccessibilityNodeInfo(
396                 host: View,
397                 info: AccessibilityNodeInfo
398             ) {
399                 super.onInitializeAccessibilityNodeInfo(host, info)
400                 info.addAction(
401                     AccessibilityNodeInfo.AccessibilityAction(
402                         R.id.close,
403                         host.context?.getText(R.string.taskbar_edu_close)
404                     )
405                 )
406             }
407         }
408 
409     override fun dumpLogs(prefix: String?, pw: PrintWriter?) {
410         pw?.println(prefix + "TaskbarEduTooltipController:")
411         pw?.println("$prefix\tisTooltipEnabled=$isTooltipEnabled")
412         pw?.println("$prefix\tisOpen=$isOpen")
413         pw?.println("$prefix\ttooltipStep=$tooltipStep")
414     }
415 
416     companion object {
417         @JvmStatic
418         fun newInstance(context: Context): TaskbarEduTooltipController {
419             return ResourceBasedOverride.Overrides.getObject(
420                 TaskbarEduTooltipController::class.java,
421                 context,
422                 R.string.taskbar_edu_tooltip_controller_class
423             )
424         }
425     }
426 }
427 
428 /**
429  * Maps colors in the dark-themed Lottie assets to their light-themed equivalents.
430  *
431  * For instance, `".blue100" to R.color.lottie_blue400` means objects that are material blue100 in
432  * dark theme should be changed to material blue400 in light theme.
433  */
434 private val DARK_TO_LIGHT_COLORS =
435     mapOf(
436         ".blue100" to R.color.lottie_blue400,
437         ".blue400" to R.color.lottie_blue600,
438         ".green100" to R.color.lottie_green400,
439         ".green400" to R.color.lottie_green600,
440         ".grey300" to R.color.lottie_grey600,
441         ".grey400" to R.color.lottie_grey700,
442         ".grey800" to R.color.lottie_grey200,
443         ".red400" to R.color.lottie_red600,
444         ".yellow100" to R.color.lottie_yellow400,
445         ".yellow400" to R.color.lottie_yellow600,
446     )
447 
LottieAnimationViewnull448 private fun LottieAnimationView.supportLightTheme() {
449     if (Utilities.isDarkTheme(context)) {
450         return
451     }
452 
453     LottieAnimationColorUtils.updateToColorResources(this, DARK_TO_LIGHT_COLORS, context.theme)
454 }
455