1 /*
<lambda>null2  * 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 
17 package com.android.systemui.statusbar.notification.row
18 
19 import android.animation.Animator
20 import android.animation.AnimatorListenerAdapter
21 import android.animation.AnimatorSet
22 import android.animation.ValueAnimator
23 import android.app.Dialog
24 import android.content.Context
25 import android.graphics.Color
26 import android.graphics.PixelFormat
27 import android.graphics.drawable.ColorDrawable
28 import android.graphics.drawable.Drawable
29 import android.graphics.drawable.GradientDrawable
30 import android.text.SpannableStringBuilder
31 import android.text.style.BulletSpan
32 import android.view.Gravity
33 import android.view.View
34 import android.view.ViewGroup
35 import android.view.ViewGroup.LayoutParams.MATCH_PARENT
36 import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
37 import android.view.Window
38 import android.view.WindowInsets.Type.statusBars
39 import android.view.WindowManager
40 import android.view.animation.Interpolator
41 import android.view.animation.PathInterpolator
42 import android.widget.ImageView
43 import android.widget.TextView
44 import com.android.systemui.Interpolators.LINEAR_OUT_SLOW_IN
45 import com.android.systemui.Prefs
46 import com.android.systemui.R
47 import com.android.systemui.statusbar.notification.row.NotificationConversationInfo.OnConversationSettingsClickListener
48 import javax.inject.Inject
49 
50 
51 /**
52  * Controller to handle presenting the priority conversations onboarding dialog
53  */
54 class PriorityOnboardingDialogController @Inject constructor(
55         val view: View,
56         val context: Context,
57         private val ignoresDnd: Boolean,
58         private val showsAsBubble: Boolean,
59         val icon : Drawable,
60         private val onConversationSettingsClickListener : OnConversationSettingsClickListener,
61         val badge : Drawable
62 ) {
63 
64     private lateinit var dialog: Dialog
65     private val OVERSHOOT: Interpolator = PathInterpolator(0.4f, 0f, 0.2f, 1.4f)
66     private val IMPORTANCE_ANIM_DELAY = 150L
67     private val IMPORTANCE_ANIM_GROW_DURATION = 250L
68     private val IMPORTANCE_ANIM_SHRINK_DURATION = 200L
69     private val IMPORTANCE_ANIM_SHRINK_DELAY = 25L
70 
71     fun init() {
72         initDialog()
73     }
74 
75     fun show() {
76         dialog.show()
77     }
78 
79     private fun done() {
80         // Log that the user has seen the onboarding
81         Prefs.putBoolean(context, Prefs.Key.HAS_SEEN_PRIORITY_ONBOARDING, true)
82         dialog.dismiss()
83     }
84 
85     private fun settings() {
86         // Log that the user has seen the onboarding
87         Prefs.putBoolean(context, Prefs.Key.HAS_SEEN_PRIORITY_ONBOARDING, true)
88         dialog.dismiss()
89         onConversationSettingsClickListener?.onClick()
90     }
91 
92     class Builder @Inject constructor() {
93         private lateinit var view: View
94         private lateinit var context: Context
95         private var ignoresDnd = false
96         private var showAsBubble = false
97         private lateinit var icon: Drawable
98         private lateinit var onConversationSettingsClickListener
99                 : OnConversationSettingsClickListener
100         private lateinit var badge : Drawable
101 
102         fun setView(v: View): Builder {
103             view = v
104             return this
105         }
106 
107         fun setContext(c: Context): Builder {
108             context = c
109             return this
110         }
111 
112         fun setIgnoresDnd(ignore: Boolean): Builder {
113             ignoresDnd = ignore
114             return this
115         }
116 
117         fun setShowsAsBubble(bubble: Boolean): Builder {
118             showAsBubble = bubble
119             return this
120         }
121 
122         fun setIcon(draw : Drawable) : Builder {
123             icon = draw
124             return this
125         }
126         fun setBadge(badge : Drawable) : Builder {
127             this.badge = badge
128             return this
129         }
130 
131         fun setOnSettingsClick(onClick : OnConversationSettingsClickListener) : Builder {
132             onConversationSettingsClickListener = onClick
133             return this
134         }
135 
136         fun build(): PriorityOnboardingDialogController {
137             val controller = PriorityOnboardingDialogController(
138                     view, context, ignoresDnd, showAsBubble, icon,
139                     onConversationSettingsClickListener, badge)
140             return controller
141         }
142     }
143 
144     private fun initDialog() {
145         dialog = Dialog(context)
146 
147         if (dialog.window == null) {
148             throw IllegalStateException("Need a window for the onboarding dialog to show")
149         }
150 
151         dialog.window?.requestFeature(Window.FEATURE_NO_TITLE)
152         // Prevent a11y readers from reading the first element in the dialog twice
153         dialog.setTitle("\u00A0")
154         dialog.apply {
155             setContentView(view)
156             setCanceledOnTouchOutside(true)
157 
158             findViewById<TextView>(R.id.done_button)?.setOnClickListener {
159                 done()
160             }
161 
162             findViewById<TextView>(R.id.settings_button)?.setOnClickListener {
163                 settings()
164             }
165 
166             findViewById<ImageView>(R.id.conversation_icon)?.setImageDrawable(icon)
167             findViewById<ImageView>(R.id.icon)?.setImageDrawable(badge)
168             val mImportanceRingView = findViewById<ImageView>(R.id.conversation_icon_badge_ring)
169             val conversationIconBadgeBg = findViewById<ImageView>(R.id.conversation_icon_badge_bg)
170 
171             val ring: GradientDrawable = mImportanceRingView.drawable as GradientDrawable
172             ring.mutate()
173             val bg = conversationIconBadgeBg.drawable as GradientDrawable
174             bg.mutate()
175             val ringColor = context.getResources()
176                     .getColor(com.android.internal.R.color.conversation_important_highlight)
177             val standardThickness = context.resources.getDimensionPixelSize(
178                     com.android.internal.R.dimen.importance_ring_stroke_width)
179             val largeThickness = context.resources.getDimensionPixelSize(
180                     com.android.internal.R.dimen.importance_ring_anim_max_stroke_width)
181             val standardSize = context.resources.getDimensionPixelSize(
182                     com.android.internal.R.dimen.importance_ring_size)
183             val baseSize = standardSize - standardThickness * 2
184             val largeSize = baseSize + largeThickness * 2
185             val bgSize = context.resources.getDimensionPixelSize(
186                     com.android.internal.R.dimen.conversation_icon_size_badged)
187 
188             val animatorUpdateListener: ValueAnimator.AnimatorUpdateListener
189                     = ValueAnimator.AnimatorUpdateListener { animation ->
190                 val strokeWidth = animation.animatedValue as Int
191                 ring.setStroke(strokeWidth, ringColor)
192                 val newSize = baseSize + strokeWidth * 2
193                 ring.setSize(newSize, newSize)
194                 mImportanceRingView.invalidate()
195             }
196 
197             val growAnimation: ValueAnimator = ValueAnimator.ofInt(0, largeThickness)
198             growAnimation.interpolator = LINEAR_OUT_SLOW_IN
199             growAnimation.duration = IMPORTANCE_ANIM_GROW_DURATION
200             growAnimation.addUpdateListener(animatorUpdateListener)
201 
202             val shrinkAnimation: ValueAnimator
203                     = ValueAnimator.ofInt(largeThickness, standardThickness)
204             shrinkAnimation.duration = IMPORTANCE_ANIM_SHRINK_DURATION
205             shrinkAnimation.startDelay = IMPORTANCE_ANIM_SHRINK_DELAY
206             shrinkAnimation.interpolator = OVERSHOOT
207             shrinkAnimation.addUpdateListener(animatorUpdateListener)
208             shrinkAnimation.addListener(object : AnimatorListenerAdapter() {
209                 override fun onAnimationStart(animation: Animator?) {
210                     // Shrink the badge bg so that it doesn't peek behind the animation
211                     bg.setSize(baseSize, baseSize);
212                     conversationIconBadgeBg.invalidate();
213                 }
214 
215                 override fun onAnimationEnd(animation: Animator?) {
216                     // Reset bg back to normal size
217                     bg.setSize(bgSize, bgSize);
218                     conversationIconBadgeBg.invalidate();
219 
220                 }
221             })
222 
223             val anims = AnimatorSet()
224             anims.startDelay = IMPORTANCE_ANIM_DELAY
225             anims.playSequentially(growAnimation, shrinkAnimation)
226 
227             val gapWidth = dialog.context.getResources().getDimensionPixelSize(
228                     R.dimen.conversation_onboarding_bullet_gap_width)
229             val description = SpannableStringBuilder()
230             description.append(context.getText(R.string.priority_onboarding_show_at_top_text),
231                     BulletSpan(gapWidth),  /* flags */0)
232             description.append(System.lineSeparator())
233             description.append(context.getText(R.string.priority_onboarding_show_avatar_text),
234                     BulletSpan(gapWidth),  /* flags */0)
235             if (showsAsBubble) {
236                 description.append(System.lineSeparator())
237                 description.append(context.getText(
238                         R.string.priority_onboarding_appear_as_bubble_text),
239                         BulletSpan(gapWidth),  /* flags */0)
240             }
241             if (ignoresDnd) {
242                 description.append(System.lineSeparator())
243                 description.append(context.getText(R.string.priority_onboarding_ignores_dnd_text),
244                         BulletSpan(gapWidth),  /* flags */0)
245             }
246             findViewById<TextView>(R.id.behaviors).setText(description)
247 
248             window?.apply {
249                 setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
250                 addFlags(wmFlags)
251                 setType(WindowManager.LayoutParams.TYPE_STATUS_BAR_SUB_PANEL)
252                 setWindowAnimations(com.android.internal.R.style.Animation_InputMethod)
253 
254                 attributes = attributes.apply {
255                     format = PixelFormat.TRANSLUCENT
256                     title = PriorityOnboardingDialogController::class.java.simpleName
257                     gravity = Gravity.BOTTOM or Gravity.CENTER_HORIZONTAL
258                     fitInsetsTypes = attributes.fitInsetsTypes and statusBars().inv()
259                     width = MATCH_PARENT
260                     height = WRAP_CONTENT
261                 }
262             }
263             anims.start()
264         }
265     }
266 
267     private val wmFlags = (WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS
268             or WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH
269             or WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED)
270 }
271