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