1 /*
2  * Copyright (C) 2020 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
17 
18 import android.content.Context
19 import android.graphics.Color
20 import android.graphics.Rect
21 import android.graphics.drawable.ColorDrawable
22 import android.view.Gravity
23 import android.view.LayoutInflater
24 import android.view.View
25 import android.view.ViewGroup
26 import android.widget.Button
27 import android.widget.LinearLayout
28 import com.android.internal.R.color.system_neutral1_900
29 import com.android.wm.shell.R
30 import com.android.wm.shell.animation.Interpolators
31 
32 /**
33  * User education view to highlight the manage button that allows a user to configure the settings
34  * for the bubble. Shown only the first time a user expands a bubble.
35  */
36 class ManageEducationView(
37     context: Context,
38     private val positioner: BubblePositioner
39 ) : LinearLayout(context) {
40 
41     companion object {
42         const val PREF_MANAGED_EDUCATION: String = "HasSeenBubblesManageOnboarding"
43         private const val ANIMATE_DURATION: Long = 200
44     }
45 
<lambda>null46     private val manageView by lazy { requireViewById<ViewGroup>(R.id.manage_education_view) }
<lambda>null47     private val manageButton by lazy { requireViewById<Button>(R.id.manage_button) }
<lambda>null48     private val gotItButton by lazy { requireViewById<Button>(R.id.got_it) }
49 
50     private var isHiding = false
51     private var realManageButtonRect = Rect()
52     private var bubbleExpandedView: BubbleExpandedView? = null
53 
54     init {
55         LayoutInflater.from(context).inflate(R.layout.bubbles_manage_button_education, this)
56         visibility = View.GONE
57         elevation = resources.getDimensionPixelSize(R.dimen.bubble_elevation).toFloat()
58 
59         // BubbleStackView forces LTR by default
60         // since most of Bubble UI direction depends on positioning by the user.
61         // This view actually lays out differently in RTL, so we set layout LOCALE here.
62         layoutDirection = View.LAYOUT_DIRECTION_LOCALE
63     }
64 
setLayoutDirectionnull65     override fun setLayoutDirection(layoutDirection: Int) {
66         super.setLayoutDirection(layoutDirection)
67         setDrawableDirection(layoutDirection == LAYOUT_DIRECTION_LTR)
68     }
69 
onFinishInflatenull70     override fun onFinishInflate() {
71         super.onFinishInflate()
72         layoutDirection = resources.configuration.layoutDirection
73     }
74 
setButtonColornull75     private fun setButtonColor() {
76         val typedArray =
77             mContext.obtainStyledAttributes(
78                 intArrayOf(com.android.internal.R.attr.colorAccentPrimary)
79             )
80         val buttonColor = typedArray.getColor(0 /* index */, Color.TRANSPARENT)
81         typedArray.recycle()
82 
83         manageButton.setTextColor(mContext.getColor(system_neutral1_900))
84         manageButton.setBackgroundDrawable(ColorDrawable(buttonColor))
85         gotItButton.setBackgroundDrawable(ColorDrawable(buttonColor))
86     }
87 
setDrawableDirectionnull88     private fun setDrawableDirection(isOnLeft: Boolean) {
89         manageView.setBackgroundResource(
90             if (isOnLeft) R.drawable.bubble_stack_user_education_bg
91             else R.drawable.bubble_stack_user_education_bg_rtl
92         )
93     }
94 
95     /**
96      * If necessary, toggles the user education view for the manage button. This is shown when the
97      * bubble stack is expanded for the first time.
98      *
99      * @param expandedView the expandedView the user education is shown on top of.
100      * @param isStackOnLeft the bubble stack position on the screen
101      */
shownull102     fun show(expandedView: BubbleExpandedView, isStackOnLeft: Boolean) {
103         setButtonColor()
104         if (visibility == VISIBLE) return
105 
106         bubbleExpandedView = expandedView
107         expandedView.taskView?.setObscuredTouchRect(Rect(positioner.screenRect))
108 
109         alpha = 0f
110         visibility = View.VISIBLE
111         expandedView.getManageButtonBoundsOnScreen(realManageButtonRect)
112         layoutManageView(realManageButtonRect, expandedView.manageButtonMargin, isStackOnLeft)
113 
114         post {
115             manageButton.setOnClickListener {
116                 hide()
117                 expandedView.requireViewById<View>(R.id.manage_button).performClick()
118             }
119             gotItButton.setOnClickListener { hide() }
120             setOnClickListener { hide() }
121 
122             val offsetViewBounds = Rect()
123             manageButton.getDrawingRect(offsetViewBounds)
124             manageView.offsetDescendantRectToMyCoords(manageButton, offsetViewBounds)
125             translationY = (realManageButtonRect.top - offsetViewBounds.top).toFloat()
126             bringToFront()
127             animate()
128                 .setDuration(ANIMATE_DURATION)
129                 .setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
130                 .alpha(1f)
131         }
132         updateManageEducationSeen()
133     }
134 
135     /**
136      * On tablet the user education is aligned to the left or to right side depending on where the
137      * stack is positioned when collapsed. On phone the user education follows the layout direction.
138      *
139      * @param manageButtonRect the manage button rect on the screen
140      * @param manageButtonMargin the manage button margin
141      * @param isStackOnLeft the bubble stack position on the screen
142      */
layoutManageViewnull143     private fun layoutManageView(
144         manageButtonRect: Rect,
145         manageButtonMargin: Int,
146         isStackOnLeft: Boolean
147     ) {
148         val isLTR = resources.configuration.layoutDirection == LAYOUT_DIRECTION_LTR
149         val isPinnedLeft = if (positioner.isLargeScreen) isStackOnLeft else isLTR
150         val paddingHorizontal =
151             resources.getDimensionPixelSize(R.dimen.bubble_user_education_padding_horizontal)
152 
153         // The user education view background image direction
154         setDrawableDirection(isPinnedLeft)
155 
156         // The user education view layout gravity
157         gravity = if (isPinnedLeft) Gravity.LEFT else Gravity.RIGHT
158 
159         // The user education view width
160         manageView.layoutParams.width =
161             when {
162                 // Left-to-Right direction and the education is on the right side
163                 isLTR && !isPinnedLeft ->
164                     positioner.screenRect.right -
165                         (manageButtonRect.left - manageButtonMargin - paddingHorizontal)
166                 // Right-to-Left direction and the education is on the left side
167                 !isLTR && isPinnedLeft ->
168                     manageButtonRect.right + manageButtonMargin + paddingHorizontal
169                 // Large screen and the education position matches the layout direction
170                 positioner.isLargeScreen -> ViewGroup.LayoutParams.WRAP_CONTENT
171                 // Small screen, landscape orientation
172                 positioner.isLandscape ->
173                     resources.getDimensionPixelSize(R.dimen.bubbles_user_education_width)
174                 // Otherwise
175                 else -> ViewGroup.LayoutParams.MATCH_PARENT
176             }
177 
178         // The user education view margin on the opposite side of where it's pinned
179         (manageView.layoutParams as MarginLayoutParams).apply {
180             val edgeMargin =
181                 resources.getDimensionPixelSize(R.dimen.bubble_user_education_margin_horizontal)
182             leftMargin = if (isPinnedLeft) 0 else edgeMargin
183             rightMargin = if (isPinnedLeft) edgeMargin else 0
184         }
185 
186         // The user education view padding
187         manageView.apply {
188             val paddingLeft =
189                 if (isLTR && isPinnedLeft) {
190                     // Offset on the left to align with the manage button
191                     manageButtonRect.left - manageButtonMargin
192                 } else {
193                     // Use default padding
194                     paddingHorizontal
195                 }
196             val paddingRight =
197                 if (!isLTR && !isPinnedLeft) {
198                     // Offset on the right to align with the manage button
199                     positioner.screenRect.right - manageButtonRect.right - manageButtonMargin
200                 } else {
201                     // Use default padding
202                     paddingHorizontal
203                 }
204             setPadding(paddingLeft, paddingTop, paddingRight, paddingBottom)
205         }
206     }
207 
hidenull208     fun hide() {
209         bubbleExpandedView?.taskView?.setObscuredTouchRect(null)
210         if (visibility != VISIBLE || isHiding) return
211 
212         animate()
213             .withStartAction { isHiding = true }
214             .alpha(0f)
215             .setDuration(ANIMATE_DURATION)
216             .withEndAction {
217                 isHiding = false
218                 visibility = GONE
219             }
220     }
221 
updateManageEducationSeennull222     private fun updateManageEducationSeen() {
223         context
224             .getSharedPreferences(context.packageName, Context.MODE_PRIVATE)
225             .edit()
226             .putBoolean(PREF_MANAGED_EDUCATION, true)
227             .apply()
228     }
229 }
230