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