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 )
41     : LinearLayout(context) {
42 
43     private val TAG = if (BubbleDebugConfig.TAG_WITH_CLASS_NAME) "BubbleStackEducationView"
44         else BubbleDebugConfig.TAG_BUBBLES
45 
46     private val ANIMATE_DURATION: Long = 200
47     private val ANIMATE_DURATION_SHORT: Long = 40
48 
49     private val positioner: BubblePositioner = positioner
50     private val controller: BubbleController = controller
51 
52     private val view by lazy { findViewById<View>(R.id.stack_education_layout) }
53     private val titleTextView by lazy { findViewById<TextView>(R.id.stack_education_title) }
54     private val descTextView by lazy { findViewById<TextView>(R.id.stack_education_description) }
55 
56     private var isHiding = false
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, 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)
133             context.resources.getDimensionPixelSize(
134                     R.dimen.bubbles_user_education_width_large_screen)
135         else ViewGroup.LayoutParams.MATCH_PARENT
136 
137         setAlpha(0f)
138         setVisibility(View.VISIBLE)
139         post {
140             requestFocus()
141             with(view) {
142                 if (resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_LTR) {
143                     setPadding(positioner.bubbleSize + paddingRight, paddingTop, paddingRight,
144                             paddingBottom)
145                 } else {
146                     setPadding(paddingLeft, paddingTop, positioner.bubbleSize + paddingLeft,
147                             paddingBottom)
148                 }
149                 translationY = stackPosition.y + positioner.bubbleSize / 2 - getHeight() / 2
150             }
151             animate()
152                 .setDuration(ANIMATE_DURATION)
153                 .setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
154                 .alpha(1f)
155         }
156         setShouldShow(false)
157         return true
158     }
159 
160     /**
161      * If necessary, hides the stack education view.
162      *
163      * @param isExpanding if true this indicates the hide is happening due to the bubble being
164      *                      expanded, false if due to a touch outside of the bubble stack.
165      */
166     fun hide(isExpanding: Boolean) {
167         if (visibility != VISIBLE || isHiding) return
168         isHiding = true
169 
170         controller.updateWindowFlagsForBackpress(false /* interceptBack */)
171         animate()
172             .alpha(0f)
173             .setDuration(if (isExpanding) ANIMATE_DURATION_SHORT else ANIMATE_DURATION)
174             .withEndAction { visibility = GONE }
175     }
176 
177     private fun setShouldShow(shouldShow: Boolean) {
178         context.getSharedPreferences(context.packageName, Context.MODE_PRIVATE)
179                 .edit().putBoolean(PREF_STACK_EDUCATION, !shouldShow).apply()
180     }
181 }
182 
183 const val PREF_STACK_EDUCATION: String = "HasSeenBubblesOnboarding"