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.wm.shell.bubbles.bar
17 
18 import android.annotation.LayoutRes
19 import android.content.Context
20 import android.graphics.Point
21 import android.graphics.Rect
22 import android.util.Log
23 import android.view.LayoutInflater
24 import android.view.View
25 import android.view.ViewGroup
26 import android.widget.FrameLayout
27 import androidx.core.view.doOnLayout
28 import androidx.dynamicanimation.animation.DynamicAnimation
29 import androidx.dynamicanimation.animation.SpringForce
30 import com.android.wm.shell.R
31 import com.android.wm.shell.bubbles.BubbleDebugConfig.DEBUG_USER_EDUCATION
32 import com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_BUBBLES
33 import com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME
34 import com.android.wm.shell.bubbles.BubbleEducationController
35 import com.android.wm.shell.bubbles.BubbleViewProvider
36 import com.android.wm.shell.bubbles.setup
37 import com.android.wm.shell.common.bubbles.BubblePopupDrawable
38 import com.android.wm.shell.common.bubbles.BubblePopupView
39 import com.android.wm.shell.shared.animation.PhysicsAnimator
40 import kotlin.math.roundToInt
41 
42 /** Manages bubble education presentation and animation */
43 class BubbleEducationViewController(private val context: Context, private val listener: Listener) {
44     interface Listener {
onEducationVisibilityChangednull45         fun onEducationVisibilityChanged(isVisible: Boolean)
46     }
47 
48     private var rootView: ViewGroup? = null
49     private var educationView: BubblePopupView? = null
50     private var animator: PhysicsAnimator<BubblePopupView>? = null
51 
52     private val springConfig by lazy {
53         PhysicsAnimator.SpringConfig(
54             SpringForce.STIFFNESS_MEDIUM,
55             SpringForce.DAMPING_RATIO_LOW_BOUNCY
56         )
57     }
58 
<lambda>null59     private val scrimView by lazy {
60         View(context).apply {
61             importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
62             setOnClickListener { hideEducation(animated = true) }
63         }
64     }
65 
<lambda>null66     private val controller by lazy { BubbleEducationController(context) }
67 
68     /** Whether the education view is visible or being animated */
69     val isEducationVisible: Boolean
70         get() = educationView != null && rootView != null
71 
72     /**
73      * Hide the current education view if visible
74      *
75      * @param animated whether should hide with animation
76      */
77     @JvmOverloads
<lambda>null78     fun hideEducation(animated: Boolean, endActions: () -> Unit = {}) {
<lambda>null79         log { "hideEducation animated: $animated" }
80 
81         if (animated) {
<lambda>null82             animateTransition(show = false) {
83                 cleanUp()
84                 endActions()
85                 listener.onEducationVisibilityChanged(isVisible = false)
86             }
87         } else {
88             cleanUp()
89             endActions()
90             listener.onEducationVisibilityChanged(isVisible = false)
91         }
92     }
93 
94     /**
95      * Show bubble bar stack user education.
96      *
97      * @param position the reference position for the user education in Screen coordinates.
98      * @param root the view to show user education in.
99      * @param educationClickHandler the on click handler for the user education view
100      */
showStackEducationnull101     fun showStackEducation(position: Point, root: ViewGroup, educationClickHandler: () -> Unit) {
102         hideEducation(animated = false)
103         log { "showStackEducation at: $position" }
104 
105         educationView =
106             createEducationView(R.layout.bubble_bar_stack_education, root).apply {
107                 setArrowDirection(BubblePopupDrawable.ArrowDirection.DOWN)
108                 setArrowPosition(BubblePopupDrawable.ArrowPosition.End)
109                 updateEducationPosition(view = this, position, root)
110                 val arrowToEdgeOffset = popupDrawable?.config?.cornerRadius ?: 0f
111                 doOnLayout {
112                     it.pivotX = it.width - arrowToEdgeOffset
113                     it.pivotY = it.height.toFloat()
114                 }
115                 setOnClickListener { educationClickHandler() }
116             }
117 
118         rootView = root
119         animator = createAnimator()
120 
121         root.addView(scrimView)
122         root.addView(educationView)
123         animateTransition(show = true) {
124             controller.hasSeenStackEducation = true
125             listener.onEducationVisibilityChanged(isVisible = true)
126         }
127     }
128 
129     /**
130      * Show manage bubble education if hasn't been shown before
131      *
132      * @param bubble the bubble used for the manage education check
133      * @param root the view to show manage education in
134      */
maybeShowManageEducationnull135     fun maybeShowManageEducation(bubble: BubbleViewProvider, root: ViewGroup) {
136         log { "maybeShowManageEducation bubble: $bubble" }
137         if (!controller.shouldShowManageEducation(bubble)) return
138         showManageEducation(root)
139     }
140 
141     /**
142      * Show manage education with animation
143      *
144      * @param root the view to show manage education in
145      */
showManageEducationnull146     private fun showManageEducation(root: ViewGroup) {
147         hideEducation(animated = false)
148         log { "showManageEducation" }
149 
150         educationView =
151             createEducationView(R.layout.bubble_bar_manage_education, root).apply {
152                 pivotY = 0f
153                 doOnLayout { it.pivotX = it.width / 2f }
154                 setOnClickListener { hideEducation(animated = true) }
155             }
156 
157         rootView = root
158         animator = createAnimator()
159 
160         root.addView(scrimView)
161         root.addView(educationView)
162         animateTransition(show = true) {
163             controller.hasSeenManageEducation = true
164             listener.onEducationVisibilityChanged(isVisible = true)
165         }
166     }
167 
168     /**
169      * Animate show/hide transition for the education view
170      *
171      * @param show whether to show or hide the view
172      * @param endActions a closure to be called when the animation completes
173      */
animateTransitionnull174     private fun animateTransition(show: Boolean, endActions: () -> Unit) {
175         animator
176             ?.spring(DynamicAnimation.ALPHA, if (show) 1f else 0f)
177             ?.spring(DynamicAnimation.SCALE_X, if (show) 1f else EDU_SCALE_HIDDEN)
178             ?.spring(DynamicAnimation.SCALE_Y, if (show) 1f else EDU_SCALE_HIDDEN)
179             ?.withEndActions(endActions)
180             ?.start()
181             ?: endActions()
182     }
183 
184     /** Remove education view from the root and clean up all relative properties */
cleanUpnull185     private fun cleanUp() {
186         log { "cleanUp" }
187         rootView?.removeView(educationView)
188         rootView?.removeView(scrimView)
189         educationView = null
190         rootView = null
191         animator = null
192     }
193 
194     /**
195      * Create education view by inflating layout provided.
196      *
197      * @param layout layout resource id to inflate. The root view should be [BubblePopupView]
198      * @param root view group to use as root for inflation, is not attached to root
199      */
createEducationViewnull200     private fun createEducationView(@LayoutRes layout: Int, root: ViewGroup): BubblePopupView {
201         val view = LayoutInflater.from(context).inflate(layout, root, false) as BubblePopupView
202         view.setup()
203         view.alpha = 0f
204         view.scaleX = EDU_SCALE_HIDDEN
205         view.scaleY = EDU_SCALE_HIDDEN
206         return view
207     }
208 
209     /** Create animator for the user education transitions */
createAnimatornull210     private fun createAnimator(): PhysicsAnimator<BubblePopupView>? {
211         return educationView?.let {
212             PhysicsAnimator.getInstance(it).apply { setDefaultSpringConfig(springConfig) }
213         }
214     }
215 
216     /**
217      * Update user education view position relative to the reference position
218      *
219      * @param view the user education view to layout
220      * @param position the reference position in Screen coordinates
221      * @param root the root view to use for the layout
222      */
updateEducationPositionnull223     private fun updateEducationPosition(view: BubblePopupView, position: Point, root: ViewGroup) {
224         val rootBounds = Rect()
225         // Get root bounds on screen as position is in screen coordinates
226         root.getBoundsOnScreen(rootBounds)
227         // Get the offset to the arrow from the edge of the education view
228         val arrowToEdgeOffset =
229             view.popupDrawable?.config?.let { it.cornerRadius + it.arrowWidth / 2f }?.roundToInt()
230                 ?: 0
231         // Calculate education view margins
232         val params = view.layoutParams as FrameLayout.LayoutParams
233         params.bottomMargin = rootBounds.bottom - position.y
234         params.rightMargin = rootBounds.right - position.x - arrowToEdgeOffset
235         view.layoutParams = params
236     }
237 
lognull238     private fun log(msg: () -> String) {
239         if (DEBUG_USER_EDUCATION) Log.d(TAG, msg())
240     }
241 
242     companion object {
243         private val TAG = if (TAG_WITH_CLASS_NAME) "BubbleEducationViewController" else TAG_BUBBLES
244         private const val EDU_SCALE_HIDDEN = 0.5f
245     }
246 }
247