1 /*
2  * Copyright (C) 2021 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.quickstep.views
18 
19 import android.animation.AnimatorSet
20 import android.animation.ObjectAnimator
21 import android.content.Context
22 import android.graphics.Rect
23 import android.graphics.drawable.ShapeDrawable
24 import android.graphics.drawable.shapes.RectShape
25 import android.util.AttributeSet
26 import android.view.Gravity
27 import android.view.MotionEvent
28 import android.view.View
29 import android.view.ViewGroup
30 import android.widget.FrameLayout
31 import android.widget.LinearLayout
32 import com.android.launcher3.BaseDraggingActivity
33 import com.android.launcher3.DeviceProfile
34 import com.android.launcher3.InsettableFrameLayout
35 import com.android.launcher3.R
36 import com.android.launcher3.popup.ArrowPopup
37 import com.android.launcher3.popup.RoundedArrowDrawable
38 import com.android.launcher3.popup.SystemShortcut
39 import com.android.launcher3.util.Themes
40 import com.android.quickstep.KtR
41 import com.android.quickstep.TaskOverlayFactory
42 import com.android.quickstep.views.TaskView.TaskIdAttributeContainer
43 
44 class TaskMenuViewWithArrow<T : BaseDraggingActivity> : ArrowPopup<T> {
45     companion object {
46         const val TAG = "TaskMenuViewWithArrow"
47 
48         fun showForTask(
49             taskContainer: TaskIdAttributeContainer,
50             alignSecondRow: Boolean = false
51         ): Boolean {
52             val activity = BaseDraggingActivity
53                 .fromContext<BaseDraggingActivity>(taskContainer.taskView.context)
54             val taskMenuViewWithArrow = activity.layoutInflater
55                 .inflate(
56                     KtR.layout.task_menu_with_arrow,
57                     activity.dragLayer,
58                     false
59                 ) as TaskMenuViewWithArrow<*>
60 
61             return taskMenuViewWithArrow.populateAndShowForTask(taskContainer, alignSecondRow)
62         }
63     }
64 
65     constructor(context: Context) : super(context)
66     constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
67     constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(
68         context,
69         attrs,
70         defStyleAttr
71     )
72 
73     init {
74         clipToOutline = true
75 
76         shouldScaleArrow = true
77         // This synchronizes the arrow and menu to open at the same time
78         OPEN_CHILD_FADE_START_DELAY = OPEN_FADE_START_DELAY
79         OPEN_CHILD_FADE_DURATION = OPEN_FADE_DURATION
80         CLOSE_FADE_START_DELAY = CLOSE_CHILD_FADE_START_DELAY
81         CLOSE_FADE_DURATION = CLOSE_CHILD_FADE_DURATION
82     }
83 
84     private var alignSecondRow: Boolean = false
85     private val extraSpaceForSecondRowAlignment: Int
86         get() = if (alignSecondRow) optionMeasuredHeight else 0
87     private val menuWidth = context.resources.getDimensionPixelSize(R.dimen.task_menu_width_grid)
88 
89     private lateinit var taskView: TaskView
90     private lateinit var optionLayout: LinearLayout
91     private lateinit var taskContainer: TaskIdAttributeContainer
92 
93     private var optionMeasuredHeight = 0
94     private val arrowHorizontalPadding: Int
95         get() = if (taskView.isFocusedTask)
96             resources.getDimensionPixelSize(KtR.dimen.task_menu_horizontal_padding)
97         else
98             0
99 
100     private var iconView: IconView? = null
101     private var scrim: View? = null
102     private val scrimAlpha = 0.8f
103 
104     override fun isOfType(type: Int): Boolean = type and TYPE_TASK_MENU != 0
105 
106     override fun getTargetObjectLocation(outPos: Rect?) {
107         popupContainer.getDescendantRectRelativeToSelf(taskContainer.iconView, outPos)
108     }
109 
110     override fun onControllerInterceptTouchEvent(ev: MotionEvent?): Boolean {
111         if (ev?.action == MotionEvent.ACTION_DOWN) {
112             if (!popupContainer.isEventOverView(this, ev)) {
113                 close(true)
114                 return true
115             }
116         }
117         return false
118     }
119 
120     override fun onFinishInflate() {
121         super.onFinishInflate()
122         optionLayout = findViewById(KtR.id.menu_option_layout)
123     }
124 
125     private fun populateAndShowForTask(
126         taskContainer: TaskIdAttributeContainer,
127         alignSecondRow: Boolean
128     ): Boolean {
129         if (isAttachedToWindow) {
130             return false
131         }
132 
133         taskView = taskContainer.taskView
134         this.taskContainer = taskContainer
135         this.alignSecondRow = alignSecondRow
136         if (!populateMenu()) return false
137         addScrim()
138         show()
139         return true
140     }
141 
142     private fun addScrim() {
143         scrim = View(context).apply {
144             layoutParams = FrameLayout.LayoutParams(
145                 FrameLayout.LayoutParams.MATCH_PARENT,
146                 FrameLayout.LayoutParams.MATCH_PARENT
147             )
148             setBackgroundColor(Themes.getAttrColor(context, R.attr.overviewScrimColor))
149             alpha = 0f
150         }
151         popupContainer.addView(scrim)
152     }
153 
154     /** @return true if successfully able to populate task view menu, false otherwise
155      */
156     private fun populateMenu(): Boolean {
157         // Icon may not be loaded
158         if (taskContainer.task.icon == null) return false
159 
160         addMenuOptions()
161         return true
162     }
163 
164     private fun addMenuOptions() {
165         // Add the options
166         TaskOverlayFactory
167             .getEnabledShortcuts(taskView, mActivityContext.deviceProfile, taskContainer)
168             .forEach { this.addMenuOption(it) }
169 
170         // Add the spaces between items
171         val divider = ShapeDrawable(RectShape())
172         divider.paint.color = resources.getColor(android.R.color.transparent)
173         val dividerSpacing = resources.getDimension(KtR.dimen.task_menu_spacing).toInt()
174         optionLayout.showDividers = SHOW_DIVIDER_MIDDLE
175 
176         // Set the orientation, which makes the menu show
177         val recentsView: RecentsView<*, *> = mActivityContext.getOverviewPanel()
178         val orientationHandler = recentsView.pagedOrientationHandler
179         val deviceProfile: DeviceProfile = mActivityContext.deviceProfile
180         orientationHandler.setTaskOptionsMenuLayoutOrientation(
181             deviceProfile,
182             optionLayout,
183             dividerSpacing,
184             divider
185         )
186     }
187 
188     private fun addMenuOption(menuOption: SystemShortcut<*>) {
189         val menuOptionView = mActivityContext.layoutInflater.inflate(
190             KtR.layout.task_view_menu_option, this, false
191         ) as LinearLayout
192         menuOption.setIconAndLabelFor(
193             menuOptionView.findViewById(R.id.icon),
194             menuOptionView.findViewById(R.id.text)
195         )
196         val lp = menuOptionView.layoutParams as LayoutParams
197         lp.width = menuWidth
198         menuOptionView.setOnClickListener { view: View? -> menuOption.onClick(view) }
199         optionLayout.addView(menuOptionView)
200     }
201 
202     override fun assignMarginsAndBackgrounds(viewGroup: ViewGroup) {
203         assignMarginsAndBackgrounds(
204             this,
205             Themes.getAttrColor(context, com.android.internal.R.attr.colorSurface)
206         )
207     }
208 
209     override fun onCreateOpenAnimation(anim: AnimatorSet) {
210         scrim?.let {
211             anim.play(
212                 ObjectAnimator.ofFloat(it, View.ALPHA, 0f, scrimAlpha)
213                     .setDuration(OPEN_DURATION.toLong())
214             )
215         }
216     }
217 
218     override fun onCreateCloseAnimation(anim: AnimatorSet) {
219         scrim?.let {
220             anim.play(
221                 ObjectAnimator.ofFloat(it, View.ALPHA, scrimAlpha, 0f)
222                     .setDuration(CLOSE_DURATION.toLong())
223             )
224         }
225     }
226 
227     override fun closeComplete() {
228         super.closeComplete()
229         popupContainer.removeView(scrim)
230         popupContainer.removeView(iconView)
231     }
232 
233     /**
234      * Copy the iconView from taskView to dragLayer so it can stay on top of the scrim.
235      * It needs to be called after [getTargetObjectLocation] because [mTempRect] needs to be
236      * populated.
237      */
238     private fun copyIconToDragLayer(insets: Rect) {
239         iconView = IconView(context).apply {
240             layoutParams = FrameLayout.LayoutParams(
241                 taskContainer.iconView.width,
242                 taskContainer.iconView.height
243             )
244             x = mTempRect.left.toFloat() - insets.left
245             y = mTempRect.top.toFloat() - insets.top
246             drawable = taskContainer.iconView.drawable
247             setDrawableSize(
248                 taskContainer.iconView.drawableWidth,
249                 taskContainer.iconView.drawableHeight
250             )
251         }
252 
253         popupContainer.addView(iconView)
254     }
255 
256     /**
257      * Orients this container to the left or right of the given icon, aligning with the first option
258      * or second.
259      *
260      * These are the preferred orientations, in order (RTL prefers right-aligned over left):
261      * - Right and first option aligned
262      * - Right and second option aligned
263      * - Left and first option aligned
264      * - Left and second option aligned
265      *
266      * So we always align right if there is enough horizontal space
267      */
268     override fun orientAboutObject() {
269         measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED)
270         // Needed for offsets later
271         optionMeasuredHeight = optionLayout.getChildAt(0).measuredHeight
272         val extraHorizontalSpace = (mArrowHeight + mArrowOffsetVertical + arrowHorizontalPadding)
273 
274         val widthWithArrow = measuredWidth + paddingLeft + paddingRight + extraHorizontalSpace
275         getTargetObjectLocation(mTempRect)
276         val dragLayer: InsettableFrameLayout = popupContainer
277         val insets = dragLayer.insets
278 
279         copyIconToDragLayer(insets)
280 
281         // Put this menu to the right of the icon if there is space,
282         // which means the arrow is left aligned with the menu
283         val rightAlignedMenuStartX = mTempRect.left - widthWithArrow
284         val leftAlignedMenuStartX = mTempRect.right + extraHorizontalSpace
285         mIsLeftAligned = if (mIsRtl) {
286             rightAlignedMenuStartX + insets.left < 0
287         } else {
288             leftAlignedMenuStartX + (widthWithArrow - extraHorizontalSpace) + insets.left <
289                     dragLayer.width - insets.right
290         }
291 
292         var menuStartX = if (mIsLeftAligned) leftAlignedMenuStartX else rightAlignedMenuStartX
293 
294         // Offset y so that the arrow and row are center-aligned with the original icon.
295         val iconHeight = mTempRect.height()
296         val yOffset = (optionMeasuredHeight - iconHeight) / 2
297         var menuStartY = mTempRect.top - yOffset - extraSpaceForSecondRowAlignment
298 
299         // Insets are added later, so subtract them now.
300         menuStartX -= insets.left
301         menuStartY -= insets.top
302 
303         x = menuStartX.toFloat()
304         y = menuStartY.toFloat()
305 
306         val lp = layoutParams as FrameLayout.LayoutParams
307         val arrowLp = mArrow.layoutParams as FrameLayout.LayoutParams
308         lp.gravity = Gravity.TOP
309         arrowLp.gravity = lp.gravity
310     }
311 
312     override fun addArrow() {
313         popupContainer.addView(mArrow)
314         mArrow.x = getArrowX()
315         mArrow.y = y + (optionMeasuredHeight / 2) - (mArrowHeight / 2) +
316                 extraSpaceForSecondRowAlignment
317 
318         updateArrowColor()
319 
320         // This is inverted (x = height, y = width) because the arrow is rotated
321         mArrow.pivotX = if (mIsLeftAligned) 0f else mArrowHeight.toFloat()
322         mArrow.pivotY = 0f
323     }
324 
325     private fun getArrowX(): Float {
326         return if (mIsLeftAligned)
327             x - mArrowHeight
328         else
329             x + measuredWidth + mArrowOffsetVertical
330     }
331 
332     override fun updateArrowColor() {
333         mArrow.background = RoundedArrowDrawable(
334             mArrowWidth.toFloat(),
335             mArrowHeight.toFloat(),
336             mArrowPointRadius.toFloat(),
337             mIsLeftAligned,
338             mArrowColor
339         )
340         elevation = mElevation
341         mArrow.elevation = mElevation
342     }
343 
344 }