• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright (C) 2024 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.wm.shell.windowdecor.education
18 
19 import android.annotation.ColorInt
20 import android.annotation.DimenRes
21 import android.annotation.LayoutRes
22 import android.content.Context
23 import android.content.res.Resources
24 import android.graphics.Point
25 import android.util.Size
26 import android.view.LayoutInflater
27 import android.view.MotionEvent
28 import android.view.View
29 import android.view.View.LAYOUT_DIRECTION_RTL
30 import android.view.View.MeasureSpec.UNSPECIFIED
31 import android.view.WindowManager
32 import android.widget.ImageView
33 import android.widget.LinearLayout
34 import android.widget.TextView
35 import android.window.DisplayAreaInfo
36 import android.window.WindowContainerTransaction
37 import androidx.core.graphics.drawable.DrawableCompat
38 import androidx.dynamicanimation.animation.DynamicAnimation
39 import androidx.dynamicanimation.animation.SpringForce
40 import com.android.wm.shell.R
41 import com.android.wm.shell.common.DisplayChangeController.OnDisplayChangingListener
42 import com.android.wm.shell.common.DisplayController
43 import com.android.wm.shell.shared.animation.PhysicsAnimator
44 import com.android.wm.shell.windowdecor.WindowManagerWrapper
45 import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalSystemViewContainer
46 
47 /**
48  * Controls the lifecycle of an education tooltip, including showing and hiding it. Ensures that
49  * only one tooltip is displayed at a time.
50  */
51 class DesktopWindowingEducationTooltipController(
52     private val context: Context,
53     private val additionalSystemViewContainerFactory: AdditionalSystemViewContainer.Factory,
54     private val displayController: DisplayController,
55 ) : OnDisplayChangingListener {
56   private var tooltipView: View? = null
57   private var animator: PhysicsAnimator<View>? = null
58   private val springConfig by lazy {
59     PhysicsAnimator.SpringConfig(SpringForce.STIFFNESS_MEDIUM, SpringForce.DAMPING_RATIO_LOW_BOUNCY)
60   }
61   private var popupWindow: AdditionalSystemViewContainer? = null
62 
63   override fun onDisplayChange(
64       displayId: Int,
65       fromRotation: Int,
66       toRotation: Int,
67       newDisplayAreaInfo: DisplayAreaInfo?,
68       t: WindowContainerTransaction?
69   ) {
70     // Exit if the rotation hasn't changed or is changed by 180 degrees. [fromRotation] and
71     // [toRotation] can be one of the [@Surface.Rotation] values.
72     if ((fromRotation % 2 == toRotation % 2)) return
73     hideEducationTooltip()
74     // TODO: b/370820018 - Update tooltip position on orientation change instead of dismissing
75   }
76 
77   /**
78    * Shows education tooltip.
79    *
80    * @param tooltipViewConfig features of tooltip.
81    * @param taskId is used in the title of popup window created for the tooltip view.
82    */
83   fun showEducationTooltip(tooltipViewConfig: TooltipEducationViewConfig, taskId: Int) {
84     hideEducationTooltip()
85     tooltipView = createEducationTooltipView(tooltipViewConfig, taskId)
86     animator = createAnimator()
87     animateShowTooltipTransition()
88     displayController.addDisplayChangingController(this)
89   }
90 
91   /** Hide the current education view if visible */
92   fun hideEducationTooltip() = animateHideTooltipTransition { cleanUp() }
93 
94   /** Create education view by inflating layout provided. */
95   private fun createEducationTooltipView(
96       tooltipViewConfig: TooltipEducationViewConfig,
97       taskId: Int,
98   ): View {
99     val tooltipView =
100         LayoutInflater.from(context)
101             .inflate(
102                 tooltipViewConfig.tooltipViewLayout, /* root= */ null, /* attachToRoot= */ false)
103             .apply {
104               alpha = 0f
105               scaleX = 0f
106               scaleY = 0f
107 
108               requireViewById<TextView>(R.id.tooltip_text).apply {
109                 text = tooltipViewConfig.tooltipText
110               }
111 
112               setOnTouchListener { _, motionEvent ->
113                 if (motionEvent.action == MotionEvent.ACTION_OUTSIDE) {
114                   hideEducationTooltip()
115                   tooltipViewConfig.onDismissAction()
116                   true
117                 } else {
118                   false
119                 }
120               }
121               setOnClickListener {
122                 hideEducationTooltip()
123                 tooltipViewConfig.onEducationClickAction()
124               }
125               setTooltipColorScheme(tooltipViewConfig.tooltipColorScheme)
126             }
127 
128     val tooltipDimens = tooltipDimens(tooltipView = tooltipView, tooltipViewConfig.arrowDirection)
129     val tooltipViewGlobalCoordinates =
130         tooltipViewGlobalCoordinates(
131             tooltipViewGlobalCoordinates = tooltipViewConfig.tooltipViewGlobalCoordinates,
132             arrowDirection = tooltipViewConfig.arrowDirection,
133             tooltipDimen = tooltipDimens)
134     createTooltipPopupWindow(
135         taskId, tooltipViewGlobalCoordinates, tooltipDimens, tooltipView = tooltipView)
136 
137     return tooltipView
138   }
139 
140   /** Create animator for education transitions */
141   private fun createAnimator(): PhysicsAnimator<View>? =
142       tooltipView?.let {
143         PhysicsAnimator.getInstance(it).apply { setDefaultSpringConfig(springConfig) }
144       }
145 
146   /** Animate show transition for the education view */
147   private fun animateShowTooltipTransition() {
148     animator
149         ?.spring(DynamicAnimation.ALPHA, 1f)
150         ?.spring(DynamicAnimation.SCALE_X, 1f)
151         ?.spring(DynamicAnimation.SCALE_Y, 1f)
152         ?.start()
153   }
154 
155   /** Animate hide transition for the education view */
156   private fun animateHideTooltipTransition(endActions: () -> Unit) {
157     animator
158         ?.spring(DynamicAnimation.ALPHA, 0f)
159         ?.spring(DynamicAnimation.SCALE_X, 0f)
160         ?.spring(DynamicAnimation.SCALE_Y, 0f)
161         ?.start()
162     endActions()
163   }
164 
165   /** Remove education tooltip and clean up all relative properties */
166   private fun cleanUp() {
167     tooltipView = null
168     animator = null
169     popupWindow?.releaseView()
170     popupWindow = null
171     displayController.removeDisplayChangingController(this)
172   }
173 
174   private fun createTooltipPopupWindow(
175       taskId: Int,
176       tooltipViewGlobalCoordinates: Point,
177       tooltipDimen: Size,
178       tooltipView: View,
179   ) {
180     popupWindow =
181         additionalSystemViewContainerFactory.create(
182             windowManagerWrapper =
183                 WindowManagerWrapper(context.getSystemService(WindowManager::class.java)),
184             taskId = taskId,
185             x = tooltipViewGlobalCoordinates.x,
186             y = tooltipViewGlobalCoordinates.y,
187             width = tooltipDimen.width,
188             height = tooltipDimen.height,
189             flags =
190                 WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or
191                     WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH,
192             view = tooltipView)
193   }
194 
195   private fun View.setTooltipColorScheme(tooltipColorScheme: TooltipColorScheme) {
196     requireViewById<LinearLayout>(R.id.tooltip_container).apply {
197       background.setTint(tooltipColorScheme.container)
198     }
199     requireViewById<ImageView>(R.id.arrow_icon).apply {
200         val wrappedDrawable = DrawableCompat.wrap(this.drawable)
201         DrawableCompat.setTint(wrappedDrawable, tooltipColorScheme.container)
202         if (isRtl()) scaleX = -1f
203     }
204     requireViewById<TextView>(R.id.tooltip_text).apply { setTextColor(tooltipColorScheme.text) }
205     requireViewById<ImageView>(R.id.tooltip_icon).apply {
206       val wrappedDrawable = DrawableCompat.wrap(this.drawable)
207       DrawableCompat.setTint(wrappedDrawable, tooltipColorScheme.icon)
208     }
209   }
210 
211   private fun tooltipViewGlobalCoordinates(
212       tooltipViewGlobalCoordinates: Point,
213       arrowDirection: TooltipArrowDirection,
214       tooltipDimen: Size,
215   ): Point {
216     var tooltipX = tooltipViewGlobalCoordinates.x
217     var tooltipY = tooltipViewGlobalCoordinates.y
218 
219     // Current values of [tooltipX]/[tooltipY] are the coordinates of tip of the arrow.
220     // Parameter x and y passed to [AdditionalSystemViewContainer] is the top left position of
221     // the window to be created. Hence we will need to move the coordinates left/up in order
222     // to position the tooltip correctly.
223     if (arrowDirection == TooltipArrowDirection.UP) {
224       // Arrow is placed at horizontal center on top edge of the tooltip. Hence decrement
225       // half of tooltip width from [tooltipX] to horizontally position the tooltip.
226       tooltipX -= tooltipDimen.width / 2
227     } else {
228       // Arrow is placed at vertical center on the left edge of the tooltip. Hence decrement
229       // half of tooltip height from [tooltipY] to vertically position the tooltip.
230       tooltipY -= tooltipDimen.height / 2
231       if (isRtl()) {
232           tooltipX -= tooltipDimen.width
233       }
234     }
235     return Point(tooltipX, tooltipY)
236   }
237 
238   private fun tooltipDimens(tooltipView: View, arrowDirection: TooltipArrowDirection): Size {
239     val tooltipBackground = tooltipView.requireViewById<LinearLayout>(R.id.tooltip_container)
240     val arrowView = tooltipView.requireViewById<ImageView>(R.id.arrow_icon)
241     tooltipBackground.measure(
242         /* widthMeasureSpec= */ UNSPECIFIED, /* heightMeasureSpec= */ UNSPECIFIED)
243     arrowView.measure(/* widthMeasureSpec= */ UNSPECIFIED, /* heightMeasureSpec= */ UNSPECIFIED)
244 
245     var desiredWidth =
246         tooltipBackground.measuredWidth +
247             2 * loadDimensionPixelSize(R.dimen.desktop_windowing_education_tooltip_padding)
248     var desiredHeight =
249         tooltipBackground.measuredHeight +
250             2 * loadDimensionPixelSize(R.dimen.desktop_windowing_education_tooltip_padding)
251     if (arrowDirection == TooltipArrowDirection.UP) {
252       // desiredHeight currently does not account for the height of arrow, hence adding it.
253       desiredHeight += arrowView.height
254     } else {
255       // desiredWidth currently does not account for the width of arrow, hence adding it.
256       desiredWidth += arrowView.width
257     }
258 
259     return Size(desiredWidth, desiredHeight)
260   }
261 
262   private fun loadDimensionPixelSize(@DimenRes resourceId: Int): Int {
263     if (resourceId == Resources.ID_NULL) return 0
264     return context.resources.getDimensionPixelSize(resourceId)
265   }
266 
267     private fun isRtl() = context.resources.configuration.layoutDirection == LAYOUT_DIRECTION_RTL
268 
269     /**
270    * The configuration for education view features:
271    *
272    * @property tooltipViewLayout Layout resource ID of the view to be used for education tooltip.
273    * @property tooltipViewGlobalCoordinates Global (screen) coordinates of the tip of the tooltip
274    *   arrow.
275    * @property tooltipText Text to be added to the TextView of tooltip.
276    * @property arrowDirection Direction of arrow of the tooltip.
277    * @property onEducationClickAction Lambda to be executed when the tooltip is clicked.
278    * @property onDismissAction Lambda to be executed when the tooltip is dismissed.
279    */
280   data class TooltipEducationViewConfig(
281       @LayoutRes val tooltipViewLayout: Int,
282       val tooltipColorScheme: TooltipColorScheme,
283       val tooltipViewGlobalCoordinates: Point,
284       val tooltipText: String,
285       val arrowDirection: TooltipArrowDirection,
286       val onEducationClickAction: () -> Unit,
287       val onDismissAction: () -> Unit,
288   )
289 
290   /**
291    * Color scheme of education view:
292    *
293    * @property container Color of the container of the tooltip.
294    * @property text Text color of the [TextView] of education tooltip.
295    * @property icon Color to be filled in tooltip's icon.
296    */
297   data class TooltipColorScheme(
298       @ColorInt val container: Int,
299       @ColorInt val text: Int,
300       @ColorInt val icon: Int,
301   )
302 
303   /** Direction of arrow of the tooltip */
304   enum class TooltipArrowDirection {
305     UP,
306     HORIZONTAL
307   }
308 }