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.PointF
21 import android.view.KeyEvent
22 import android.view.LayoutInflater
23 import android.view.View
24 import android.view.ViewGroup
25 import android.widget.LinearLayout
26 import android.widget.TextView
27 import com.android.internal.util.ContrastColorUtil
28 import com.android.wm.shell.R
29 import com.android.wm.shell.animation.Interpolators
30 
31 /**
32  * User education view to highlight the collapsed stack of bubbles. Shown only the first time a user
33  * taps the stack.
34  */
35 class StackEducationView(
36     context: Context,
37     private val positioner: BubblePositioner,
38     private val manager: Manager
39 ) : LinearLayout(context) {
40 
41     companion object {
42         const val PREF_STACK_EDUCATION: String = "HasSeenBubblesOnboarding"
43         private const val ANIMATE_DURATION: Long = 200
44         private const val ANIMATE_DURATION_SHORT: Long = 40
45     }
46 
47     /** Callbacks to notify managers of [StackEducationView] about events. */
48     interface Manager {
49         /** Notifies whether backpress should be intercepted. */
updateWindowFlagsForBackpressnull50         fun updateWindowFlagsForBackpress(interceptBack: Boolean)
51     }
52 
53     private val view by lazy { requireViewById<View>(R.id.stack_education_layout) }
<lambda>null54     private val titleTextView by lazy { requireViewById<TextView>(R.id.stack_education_title) }
<lambda>null55     private val descTextView by lazy { requireViewById<TextView>(R.id.stack_education_description) }
56 
57     var isHiding = false
58         private set
59 
60     init {
61         LayoutInflater.from(context).inflate(R.layout.bubble_stack_user_education, this)
62 
63         visibility = View.GONE
64         elevation = resources.getDimensionPixelSize(R.dimen.bubble_elevation).toFloat()
65 
66         // BubbleStackView forces LTR by default
67         // since most of Bubble UI direction depends on positioning by the user.
68         // This view actually lays out differently in RTL, so we set layout LOCALE here.
69         layoutDirection = View.LAYOUT_DIRECTION_LOCALE
70     }
71 
setLayoutDirectionnull72     override fun setLayoutDirection(layoutDirection: Int) {
73         super.setLayoutDirection(layoutDirection)
74         setDrawableDirection(layoutDirection == LAYOUT_DIRECTION_LTR)
75     }
76 
onFinishInflatenull77     override fun onFinishInflate() {
78         super.onFinishInflate()
79         layoutDirection = resources.configuration.layoutDirection
80         setTextColor()
81     }
82 
onAttachedToWindownull83     override fun onAttachedToWindow() {
84         super.onAttachedToWindow()
85         setFocusableInTouchMode(true)
86         setOnKeyListener(object : OnKeyListener {
87             override fun onKey(v: View?, keyCode: Int, event: KeyEvent): Boolean {
88                 // if the event is a key down event on the enter button
89                 if (event.action == KeyEvent.ACTION_UP &&
90                         keyCode == KeyEvent.KEYCODE_BACK && !isHiding) {
91                     hide(false)
92                     return true
93                 }
94                 return false
95             }
96         })
97     }
98 
onDetachedFromWindownull99     override fun onDetachedFromWindow() {
100         super.onDetachedFromWindow()
101         setOnKeyListener(null)
102         manager.updateWindowFlagsForBackpress(false /* interceptBack */)
103     }
104 
setTextColornull105     private fun setTextColor() {
106         val ta = mContext.obtainStyledAttributes(intArrayOf(android.R.attr.colorAccent,
107             android.R.attr.textColorPrimaryInverse))
108         val bgColor = ta.getColor(0 /* index */, Color.BLACK)
109         var textColor = ta.getColor(1 /* index */, Color.WHITE)
110         ta.recycle()
111         textColor = ContrastColorUtil.ensureTextContrast(textColor, bgColor, true)
112         titleTextView.setTextColor(textColor)
113         descTextView.setTextColor(textColor)
114     }
115 
setDrawableDirectionnull116     private fun setDrawableDirection(isOnLeft: Boolean) {
117         view.setBackgroundResource(
118             if (isOnLeft) R.drawable.bubble_stack_user_education_bg
119             else R.drawable.bubble_stack_user_education_bg_rtl
120         )
121     }
122 
123     /**
124      * If necessary, shows the user education view for the bubble stack. This appears the first time
125      * a user taps on a bubble.
126      *
127      * @return true if user education was shown and wasn't showing before, false otherwise.
128      */
shownull129     fun show(stackPosition: PointF): Boolean {
130         isHiding = false
131         if (visibility == VISIBLE) return false
132 
133         manager.updateWindowFlagsForBackpress(true /* interceptBack */)
134         layoutParams.width =
135                 if (positioner.isLargeScreen || positioner.isLandscape)
136                     context.resources.getDimensionPixelSize(R.dimen.bubbles_user_education_width)
137                 else ViewGroup.LayoutParams.MATCH_PARENT
138 
139         val isStackOnLeft = positioner.isStackOnLeft(stackPosition)
140         (view.layoutParams as MarginLayoutParams).apply {
141             // Update the horizontal margins depending on the stack position
142             val edgeMargin =
143                 resources.getDimensionPixelSize(R.dimen.bubble_user_education_margin_horizontal)
144             leftMargin = if (isStackOnLeft) 0 else edgeMargin
145             rightMargin = if (isStackOnLeft) edgeMargin else 0
146         }
147 
148         val stackPadding =
149             context.resources.getDimensionPixelSize(R.dimen.bubble_user_education_stack_padding)
150         setAlpha(0f)
151         setVisibility(View.VISIBLE)
152         setDrawableDirection(isOnLeft = isStackOnLeft)
153         post {
154             requestFocus()
155             with(view) {
156                 if (isStackOnLeft) {
157                     setPadding(
158                         positioner.bubbleSize + stackPadding,
159                         paddingTop,
160                         paddingRight,
161                         paddingBottom
162                     )
163                     translationX = 0f
164                 } else {
165                     setPadding(
166                         paddingLeft,
167                         paddingTop,
168                         positioner.bubbleSize + stackPadding,
169                         paddingBottom
170                     )
171                     translationX = (positioner.screenRect.right - width - stackPadding).toFloat()
172                 }
173                 translationY = stackPosition.y + positioner.bubbleSize / 2 - getHeight() / 2
174             }
175             animate()
176                 .setDuration(ANIMATE_DURATION)
177                 .setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
178                 .alpha(1f)
179         }
180         updateStackEducationSeen()
181         return true
182     }
183 
184     /**
185      * If necessary, hides the stack education view.
186      *
187      * @param isExpanding if true this indicates the hide is happening due to the bubble being
188      *   expanded, false if due to a touch outside of the bubble stack.
189      */
hidenull190     fun hide(isExpanding: Boolean) {
191         if (visibility != VISIBLE || isHiding) return
192         isHiding = true
193 
194         manager.updateWindowFlagsForBackpress(false /* interceptBack */)
195         animate()
196             .alpha(0f)
197             .setDuration(if (isExpanding) ANIMATE_DURATION_SHORT else ANIMATE_DURATION)
198             .withEndAction { visibility = GONE }
199     }
200 
updateStackEducationSeennull201     private fun updateStackEducationSeen() {
202         context
203             .getSharedPreferences(context.packageName, Context.MODE_PRIVATE)
204             .edit()
205             .putBoolean(PREF_STACK_EDUCATION, true)
206             .apply()
207     }
208 }
209