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.systemui.accessibility.floatingmenu
18 
19 import android.animation.ObjectAnimator
20 import android.content.Context
21 import android.graphics.Color
22 import android.graphics.drawable.GradientDrawable
23 import android.util.ArrayMap
24 import android.util.IntProperty
25 import android.util.Log
26 import android.view.Gravity
27 import android.view.View
28 import android.view.ViewGroup
29 import android.view.WindowInsets
30 import android.view.WindowManager
31 import android.widget.FrameLayout
32 import android.widget.LinearLayout
33 import android.widget.Space
34 import androidx.annotation.ColorRes
35 import androidx.annotation.DimenRes
36 import androidx.annotation.DrawableRes
37 import androidx.core.content.ContextCompat
38 import androidx.dynamicanimation.animation.DynamicAnimation
39 import androidx.dynamicanimation.animation.SpringForce.DAMPING_RATIO_LOW_BOUNCY
40 import androidx.dynamicanimation.animation.SpringForce.STIFFNESS_LOW
41 import com.android.wm.shell.R
42 import com.android.wm.shell.common.bubbles.DismissCircleView
43 import com.android.wm.shell.common.bubbles.DismissView
44 import com.android.wm.shell.shared.animation.PhysicsAnimator
45 
46 /**
47  * View that handles interactions between DismissCircleView and BubbleStackView.
48  *
49  * @note [setup] method should be called after initialisation
50  */
51 class DragToInteractView(context: Context) : FrameLayout(context) {
52     /**
53      * The configuration is used to provide module specific resource ids
54      *
55      * @see [setup] method
56      */
57     data class Config(
58         /** dimen resource id of the dismiss target circle view size */
59         @DimenRes val targetSizeResId: Int,
60         /** dimen resource id of the icon size in the dismiss target */
61         @DimenRes val iconSizeResId: Int,
62         /** dimen resource id of the bottom margin for the dismiss target */
63         @DimenRes var bottomMarginResId: Int,
64         /** dimen resource id of the height for dismiss area gradient */
65         @DimenRes val floatingGradientHeightResId: Int,
66         /** color resource id of the dismiss area gradient color */
67         @ColorRes val floatingGradientColorResId: Int,
68         /** drawable resource id of the dismiss target background */
69         @DrawableRes val backgroundResId: Int,
70         /** drawable resource id of the icon for the dismiss target */
71         @DrawableRes val iconResId: Int
72     )
73 
74     companion object {
75         private const val SHOULD_SETUP = "The view isn't ready. Should be called after `setup`"
76         private val TAG = DragToInteractView::class.simpleName
77     }
78 
79     // START DragToInteractView modification
80     // We could technically access each DismissCircleView from their Animator,
81     // but the animators only store a weak reference to their targets. This is safer.
82     var interactMap = ArrayMap<Int, Pair<DismissCircleView, PhysicsAnimator<DismissCircleView>>>()
83     // END DragToInteractView modification
84     var isShowing = false
85     var config: Config? = null
86 
87     private val spring = PhysicsAnimator.SpringConfig(STIFFNESS_LOW, DAMPING_RATIO_LOW_BOUNCY)
88     private val INTERACT_SCRIM_FADE_MS = 200L
89     private var wm: WindowManager =
90         context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
91     private var gradientDrawable: GradientDrawable? = null
92 
93     private val GRADIENT_ALPHA: IntProperty<GradientDrawable> =
94         object : IntProperty<GradientDrawable>("alpha") {
setValuenull95             override fun setValue(d: GradientDrawable, percent: Int) {
96                 d.alpha = percent
97             }
getnull98             override fun get(d: GradientDrawable): Int {
99                 return d.alpha
100             }
101         }
102 
103     init {
104         clipToPadding = false
105         clipChildren = false
106         visibility = View.INVISIBLE
107 
108         // START DragToInteractView modification
109         // Resources included within implementation as we aren't concerned with decoupling them.
110         setup(
111             Config(
112                 targetSizeResId = R.dimen.dismiss_circle_size,
113                 iconSizeResId = R.dimen.dismiss_target_x_size,
114                 bottomMarginResId = R.dimen.floating_dismiss_bottom_margin,
115                 floatingGradientHeightResId = R.dimen.floating_dismiss_gradient_height,
116                 floatingGradientColorResId = android.R.color.system_neutral1_900,
117                 backgroundResId = R.drawable.dismiss_circle_background,
118                 iconResId = R.drawable.pip_ic_close_white
119             )
120         )
121 
122         // Ensure this is unfocusable & uninteractable
123         isClickable = false
124         isFocusable = false
125         importantForAccessibility = IMPORTANT_FOR_ACCESSIBILITY_NO
126 
127         // END DragToInteractView modification
128     }
129 
130     /**
131      * Sets up view with the provided resource ids.
132      *
133      * Decouples resource dependency in order to be used externally (e.g. Launcher). Usually called
134      * with default params in module specific extension:
135      *
136      * @see [DismissView.setup] in DismissViewExt.kt
137      */
setupnull138     fun setup(config: Config) {
139         this.config = config
140 
141         // Setup layout
142         layoutParams =
143             LayoutParams(
144                 ViewGroup.LayoutParams.MATCH_PARENT,
145                 resources.getDimensionPixelSize(config.floatingGradientHeightResId),
146                 Gravity.BOTTOM
147             )
148         updatePadding()
149 
150         // Setup gradient
151         gradientDrawable = createGradient(color = config.floatingGradientColorResId)
152         background = gradientDrawable
153 
154         // START DragToInteractView modification
155 
156         // Setup LinearLayout. Added to organize multiple circles.
157         val linearLayout = LinearLayout(context)
158         linearLayout.layoutParams =
159             LinearLayout.LayoutParams(
160                 ViewGroup.LayoutParams.MATCH_PARENT,
161                 ViewGroup.LayoutParams.MATCH_PARENT
162             )
163         linearLayout.weightSum = 0f
164         addView(linearLayout)
165 
166         // Setup DismissCircleView. Code block replaced with repeatable functions
167         addSpace(linearLayout)
168         addCircle(
169             config,
170             com.android.systemui.res.R.id.action_remove_menu,
171             R.drawable.pip_ic_close_white,
172             linearLayout
173         )
174         addCircle(
175             config,
176             com.android.systemui.res.R.id.action_edit,
177             com.android.systemui.res.R.drawable.ic_screenshot_edit,
178             linearLayout
179         )
180         // END DragToInteractView modification
181     }
182 
183     /** Animates this view in. */
shownull184     fun show() {
185         if (isShowing) return
186         val gradientDrawable = checkExists(gradientDrawable) ?: return
187         isShowing = true
188         visibility = View.VISIBLE
189         val alphaAnim =
190             ObjectAnimator.ofInt(gradientDrawable, GRADIENT_ALPHA, gradientDrawable.alpha, 255)
191         alphaAnim.duration = INTERACT_SCRIM_FADE_MS
192         alphaAnim.start()
193 
194         // START DragToInteractView modification
195         interactMap.forEach {
196             val animator = it.value.second
197             animator.cancel()
198             animator.spring(DynamicAnimation.TRANSLATION_Y, 0f, spring).start()
199         }
200         // END DragToInteractView modification
201     }
202 
203     /**
204      * Animates this view out, as well as the circle that encircles the bubbles, if they were
205      * dragged into the target and encircled.
206      */
hidenull207     fun hide() {
208         if (!isShowing) return
209         val gradientDrawable = checkExists(gradientDrawable) ?: return
210         isShowing = false
211         val alphaAnim =
212             ObjectAnimator.ofInt(gradientDrawable, GRADIENT_ALPHA, gradientDrawable.alpha, 0)
213         alphaAnim.duration = INTERACT_SCRIM_FADE_MS
214         alphaAnim.start()
215 
216         // START DragToInteractView modification
217         interactMap.forEach {
218             val animator = it.value.second
219             animator
220                 .spring(DynamicAnimation.TRANSLATION_Y, height.toFloat(), spring)
221                 .withEndActions({ visibility = View.INVISIBLE })
222                 .start()
223         }
224         // END DragToInteractView modification
225     }
226 
227     /** Cancels the animator for the dismiss target. */
cancelAnimatorsnull228     fun cancelAnimators() {
229         // START DragToInteractView modification
230         interactMap.forEach {
231             val animator = it.value.second
232             animator.cancel()
233         }
234         // END DragToInteractView modification
235     }
236 
updateResourcesnull237     fun updateResources() {
238         val config = checkExists(config) ?: return
239         updatePadding()
240         layoutParams.height = resources.getDimensionPixelSize(config.floatingGradientHeightResId)
241         val targetSize = resources.getDimensionPixelSize(config.targetSizeResId)
242 
243         // START DragToInteractView modification
244         interactMap.forEach {
245             val circle = it.value.first
246             circle.layoutParams.width = targetSize
247             circle.layoutParams.height = targetSize
248             circle.requestLayout()
249         }
250         // END DragToInteractView modification
251     }
252 
createGradientnull253     private fun createGradient(@ColorRes color: Int): GradientDrawable {
254         val gradientColor = ContextCompat.getColor(context, color)
255         val alpha = 0.7f * 255
256         val gradientColorWithAlpha =
257             Color.argb(
258                 alpha.toInt(),
259                 Color.red(gradientColor),
260                 Color.green(gradientColor),
261                 Color.blue(gradientColor)
262             )
263         val gd =
264             GradientDrawable(
265                 GradientDrawable.Orientation.BOTTOM_TOP,
266                 intArrayOf(gradientColorWithAlpha, Color.TRANSPARENT)
267             )
268         gd.setDither(true)
269         gd.alpha = 0
270         return gd
271     }
272 
updatePaddingnull273     private fun updatePadding() {
274         val config = checkExists(config) ?: return
275         val insets: WindowInsets = wm.currentWindowMetrics.windowInsets
276         val navInset = insets.getInsetsIgnoringVisibility(WindowInsets.Type.navigationBars())
277         setPadding(
278             0,
279             0,
280             0,
281             navInset.bottom + resources.getDimensionPixelSize(config.bottomMarginResId)
282         )
283     }
284 
285     /**
286      * Checks if the value is set up and exists, if not logs an exception. Used for convenient
287      * logging in case `setup` wasn't called before
288      *
289      * @return value provided as argument
290      */
checkExistsnull291     private fun <T> checkExists(value: T?): T? {
292         if (value == null) Log.e(TAG, SHOULD_SETUP)
293         return value
294     }
295 
296     // START DragToInteractView modification
addSpacenull297     private fun addSpace(parent: LinearLayout) {
298         val space = Space(context)
299         // Spaces are weighted equally to space out circles evenly
300         space.layoutParams =
301             LinearLayout.LayoutParams(
302                 ViewGroup.LayoutParams.WRAP_CONTENT,
303                 ViewGroup.LayoutParams.WRAP_CONTENT,
304                 1f
305             )
306         parent.addView(space)
307         parent.weightSum = parent.weightSum + 1f
308     }
309 
addCirclenull310     private fun addCircle(config: Config, id: Int, iconResId: Int, parent: LinearLayout) {
311         val targetSize = resources.getDimensionPixelSize(config.targetSizeResId)
312         val circleLayoutParams = LinearLayout.LayoutParams(targetSize, targetSize, 0f)
313         circleLayoutParams.gravity = Gravity.BOTTOM or Gravity.CENTER_HORIZONTAL
314         val circle = DismissCircleView(context)
315         circle.id = id
316         circle.setup(config.backgroundResId, iconResId, config.iconSizeResId)
317         circle.layoutParams = circleLayoutParams
318 
319         // Initial position with circle offscreen so it's animated up
320         circle.translationY =
321             resources.getDimensionPixelSize(config.floatingGradientHeightResId).toFloat()
322 
323         interactMap[circle.id] = Pair(circle, PhysicsAnimator.getInstance(circle))
324         parent.addView(circle)
325         addSpace(parent)
326     }
327     // END DragToInteractView modification
328 }
329