1 /* 2 * Copyright (C) 2014 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.footer.ui.view; 18 19 import static android.graphics.PorterDuff.Mode.SRC_ATOP; 20 21 import static com.android.systemui.Flags.notificationFooterBackgroundTintOptimization; 22 import static com.android.systemui.Flags.notificationShadeBlur; 23 import static com.android.systemui.util.ColorUtilKt.hexColorString; 24 25 import android.annotation.ColorInt; 26 import android.annotation.DrawableRes; 27 import android.annotation.StringRes; 28 import android.annotation.SuppressLint; 29 import android.content.Context; 30 import android.content.res.ColorStateList; 31 import android.content.res.Configuration; 32 import android.content.res.Resources; 33 import android.graphics.Color; 34 import android.graphics.ColorFilter; 35 import android.graphics.PorterDuffColorFilter; 36 import android.graphics.drawable.Drawable; 37 import android.util.AttributeSet; 38 import android.util.IndentingPrintWriter; 39 import android.view.View; 40 import android.widget.TextView; 41 42 import androidx.annotation.NonNull; 43 44 import com.android.internal.graphics.ColorUtils; 45 import com.android.systemui.common.shared.colors.SurfaceEffectColors; 46 import com.android.systemui.res.R; 47 import com.android.systemui.scene.shared.flag.SceneContainerFlag; 48 import com.android.systemui.statusbar.notification.ColorUpdateLogger; 49 import com.android.systemui.statusbar.notification.footer.shared.NotifRedesignFooter; 50 import com.android.systemui.statusbar.notification.row.FooterViewButton; 51 import com.android.systemui.statusbar.notification.row.StackScrollerDecorView; 52 import com.android.systemui.statusbar.notification.stack.AnimationProperties; 53 import com.android.systemui.statusbar.notification.stack.ExpandableViewState; 54 import com.android.systemui.statusbar.notification.stack.ViewState; 55 import com.android.systemui.util.DrawableDumpKt; 56 import com.android.systemui.util.DumpUtilsKt; 57 58 import java.io.PrintWriter; 59 import java.util.function.Consumer; 60 61 public class FooterView extends StackScrollerDecorView { 62 private static final String TAG = "FooterView"; 63 64 private FooterViewButton mClearAllButton; 65 private FooterViewButton mManageOrHistoryButton; 66 // The settings & history buttons replace the single manage/history button in the redesign 67 private FooterViewButton mSettingsButton; 68 private FooterViewButton mHistoryButton; 69 private boolean mShouldBeHidden; 70 private boolean mIsBlurSupported; 71 72 // Footer label 73 private TextView mSeenNotifsFooterTextView; 74 75 private @StringRes int mClearAllButtonTextId; 76 private @StringRes int mClearAllButtonDescriptionId; 77 private @StringRes int mManageOrHistoryButtonTextId; 78 private @StringRes int mManageOrHistoryButtonDescriptionId; 79 private @StringRes int mMessageStringId; 80 private @DrawableRes int mMessageIconId; 81 82 private OnClickListener mClearAllButtonClickListener; 83 FooterView(Context context, AttributeSet attrs)84 public FooterView(Context context, AttributeSet attrs) { 85 super(context, attrs); 86 } 87 88 @Override findContentView()89 protected View findContentView() { 90 return findViewById(R.id.content); 91 } 92 findSecondaryView()93 protected View findSecondaryView() { 94 return findViewById(R.id.dismiss_text); 95 } 96 97 /** Whether the "Clear all" button is currently visible. */ isClearAllButtonVisible()98 public boolean isClearAllButtonVisible() { 99 return isSecondaryVisible(); 100 } 101 102 /** See {@link this#setClearAllButtonVisible(boolean, boolean, Consumer)}. */ setClearAllButtonVisible(boolean visible, boolean animate)103 public void setClearAllButtonVisible(boolean visible, boolean animate) { 104 setClearAllButtonVisible(visible, animate, /* onAnimationEnded = */ null); 105 } 106 107 /** 108 * Set the visibility of the "Manage"/"History" button to {@code visible}. This is replaced by 109 * two separate buttons in the redesign. 110 */ setManageOrHistoryButtonVisible(boolean visible)111 public void setManageOrHistoryButtonVisible(boolean visible) { 112 NotifRedesignFooter.assertInLegacyMode(); 113 mManageOrHistoryButton.setVisibility(visible ? View.VISIBLE : View.GONE); 114 } 115 116 /** Set the visibility of the Settings button to {@code visible}. */ setSettingsButtonVisible(boolean visible)117 public void setSettingsButtonVisible(boolean visible) { 118 if (NotifRedesignFooter.isUnexpectedlyInLegacyMode()) { 119 return; 120 } 121 mSettingsButton.setVisibility(visible ? View.VISIBLE : View.GONE); 122 } 123 124 /** Set the visibility of the History button to {@code visible}. */ setHistoryButtonVisible(boolean visible)125 public void setHistoryButtonVisible(boolean visible) { 126 if (NotifRedesignFooter.isUnexpectedlyInLegacyMode()) { 127 return; 128 } 129 mHistoryButton.setVisibility(visible ? View.VISIBLE : View.GONE); 130 } 131 132 /** 133 * Set the visibility of the "Clear all" button to {@code visible}. Animate the change if 134 * {@code animate} is true. 135 */ setClearAllButtonVisible(boolean visible, boolean animate, Consumer<Boolean> onAnimationEnded)136 public void setClearAllButtonVisible(boolean visible, boolean animate, 137 Consumer<Boolean> onAnimationEnded) { 138 setSecondaryVisible(visible, animate, onAnimationEnded); 139 } 140 141 /** See {@link this#setShouldBeHidden} below. */ shouldBeHidden()142 public boolean shouldBeHidden() { 143 return mShouldBeHidden; 144 } 145 146 /** 147 * Whether this view's visibility should be set to INVISIBLE. Note that this is different from 148 * the {@link StackScrollerDecorView#setVisible} method, which in turn handles visibility 149 * transitions between VISIBLE and GONE. 150 */ setShouldBeHidden(boolean hide)151 public void setShouldBeHidden(boolean hide) { 152 mShouldBeHidden = hide; 153 } 154 155 @Override dump(PrintWriter pwOriginal, String[] args)156 public void dump(PrintWriter pwOriginal, String[] args) { 157 IndentingPrintWriter pw = DumpUtilsKt.asIndenting(pwOriginal); 158 super.dump(pw, args); 159 DumpUtilsKt.withIncreasedIndent(pw, () -> { 160 // TODO: b/375010573 - update dumps for redesign 161 pw.println("visibility: " + DumpUtilsKt.visibilityString(getVisibility())); 162 if (mManageOrHistoryButton != null) 163 pw.println("mManageOrHistoryButton visibility: " 164 + DumpUtilsKt.visibilityString(mManageOrHistoryButton.getVisibility())); 165 if (mClearAllButton != null) 166 pw.println("mClearAllButton visibility: " 167 + DumpUtilsKt.visibilityString(mClearAllButton.getVisibility())); 168 }); 169 } 170 171 /** Set the text label for the "Clear all" button. */ setClearAllButtonText(@tringRes int textId)172 public void setClearAllButtonText(@StringRes int textId) { 173 if (mClearAllButtonTextId == textId) { 174 return; // nothing changed 175 } 176 mClearAllButtonTextId = textId; 177 updateClearAllButtonText(); 178 } 179 updateClearAllButtonText()180 private void updateClearAllButtonText() { 181 if (mClearAllButtonTextId == 0) { 182 return; // not initialized yet 183 } 184 mClearAllButton.setText(getContext().getString(mClearAllButtonTextId)); 185 } 186 187 /** Set the accessibility content description for the "Clear all" button. */ setClearAllButtonDescription(@tringRes int contentDescriptionId)188 public void setClearAllButtonDescription(@StringRes int contentDescriptionId) { 189 if (mClearAllButtonDescriptionId == contentDescriptionId) { 190 return; // nothing changed 191 } 192 mClearAllButtonDescriptionId = contentDescriptionId; 193 updateClearAllButtonDescription(); 194 } 195 updateClearAllButtonDescription()196 private void updateClearAllButtonDescription() { 197 if (mClearAllButtonDescriptionId == 0) { 198 return; // not initialized yet 199 } 200 mClearAllButton.setContentDescription(getContext().getString(mClearAllButtonDescriptionId)); 201 } 202 203 /** Set the text label for the "Manage"/"History" button. */ setManageOrHistoryButtonText(@tringRes int textId)204 public void setManageOrHistoryButtonText(@StringRes int textId) { 205 NotifRedesignFooter.assertInLegacyMode(); 206 if (mManageOrHistoryButtonTextId == textId) { 207 return; // nothing changed 208 } 209 mManageOrHistoryButtonTextId = textId; 210 updateManageOrHistoryButtonText(); 211 } 212 updateManageOrHistoryButtonText()213 private void updateManageOrHistoryButtonText() { 214 NotifRedesignFooter.assertInLegacyMode(); 215 if (mManageOrHistoryButtonTextId == 0) { 216 return; // not initialized yet 217 } 218 mManageOrHistoryButton.setText(getContext().getString(mManageOrHistoryButtonTextId)); 219 } 220 221 /** Set the accessibility content description for the "Clear all" button. */ setManageOrHistoryButtonDescription(@tringRes int contentDescriptionId)222 public void setManageOrHistoryButtonDescription(@StringRes int contentDescriptionId) { 223 NotifRedesignFooter.assertInLegacyMode(); 224 if (mManageOrHistoryButtonDescriptionId == contentDescriptionId) { 225 return; // nothing changed 226 } 227 mManageOrHistoryButtonDescriptionId = contentDescriptionId; 228 updateManageOrHistoryButtonDescription(); 229 } 230 updateManageOrHistoryButtonDescription()231 private void updateManageOrHistoryButtonDescription() { 232 NotifRedesignFooter.assertInLegacyMode(); 233 if (mManageOrHistoryButtonDescriptionId == 0) { 234 return; // not initialized yet 235 } 236 mManageOrHistoryButton.setContentDescription( 237 getContext().getString(mManageOrHistoryButtonDescriptionId)); 238 } 239 240 /** Set the string for a message to be shown instead of the buttons. */ setMessageString(@tringRes int messageId)241 public void setMessageString(@StringRes int messageId) { 242 if (mMessageStringId == messageId) { 243 return; // nothing changed 244 } 245 mMessageStringId = messageId; 246 updateMessageString(); 247 } 248 updateMessageString()249 private void updateMessageString() { 250 if (mMessageStringId == 0) { 251 return; // not initialized yet 252 } 253 String messageString = getContext().getString(mMessageStringId); 254 mSeenNotifsFooterTextView.setText(messageString); 255 } 256 257 /** Set the icon to be shown before the message (see {@link #setMessageString(int)}). */ setMessageIcon(@rawableRes int iconId)258 public void setMessageIcon(@DrawableRes int iconId) { 259 if (mMessageIconId == iconId) { 260 return; // nothing changed 261 } 262 mMessageIconId = iconId; 263 updateMessageIcon(); 264 } 265 updateMessageIcon()266 private void updateMessageIcon() { 267 if (mMessageIconId == 0) { 268 return; // not initialized yet 269 } 270 int unlockIconSize = getResources() 271 .getDimensionPixelSize(R.dimen.notifications_unseen_footer_icon_size); 272 @SuppressLint("UseCompatLoadingForDrawables") 273 Drawable messageIcon = getContext().getDrawable(mMessageIconId); 274 if (messageIcon != null) { 275 messageIcon.setBounds(0, 0, unlockIconSize, unlockIconSize); 276 mSeenNotifsFooterTextView 277 .setCompoundDrawablesRelative(messageIcon, null, null, null); 278 } 279 } 280 281 @Override onFinishInflate()282 protected void onFinishInflate() { 283 ColorUpdateLogger colorUpdateLogger = ColorUpdateLogger.getInstance(); 284 if (colorUpdateLogger != null) { 285 colorUpdateLogger.logTriggerEvent("Footer.onFinishInflate()"); 286 } 287 super.onFinishInflate(); 288 mClearAllButton = (FooterViewButton) findSecondaryView(); 289 if (NotifRedesignFooter.isEnabled()) { 290 mSettingsButton = findViewById(R.id.settings_button); 291 mHistoryButton = findViewById(R.id.history_button); 292 } else { 293 mManageOrHistoryButton = findViewById(R.id.manage_text); 294 } 295 mSeenNotifsFooterTextView = findViewById(R.id.unlock_prompt_footer); 296 updateContent(); 297 updateColors(); 298 } 299 300 /** Show a message instead of the footer buttons. */ setFooterLabelVisible(boolean isVisible)301 public void setFooterLabelVisible(boolean isVisible) { 302 // Note: hiding the buttons is handled in the FooterViewModel 303 if (isVisible) { 304 mSeenNotifsFooterTextView.setVisibility(View.VISIBLE); 305 } else { 306 mSeenNotifsFooterTextView.setVisibility(View.GONE); 307 } 308 } 309 310 /** Set onClickListener for the notification settings button. */ setSettingsButtonClickListener(OnClickListener listener)311 public void setSettingsButtonClickListener(OnClickListener listener) { 312 if (NotifRedesignFooter.isUnexpectedlyInLegacyMode()) { 313 return; 314 } 315 mSettingsButton.setOnClickListener(listener); 316 } 317 318 /** Set onClickListener for the notification history button. */ setHistoryButtonClickListener(OnClickListener listener)319 public void setHistoryButtonClickListener(OnClickListener listener) { 320 if (NotifRedesignFooter.isUnexpectedlyInLegacyMode()) { 321 return; 322 } 323 mHistoryButton.setOnClickListener(listener); 324 } 325 326 /** 327 * Set onClickListener for the manage/history button. This is replaced by two separate buttons 328 * in the redesign. 329 */ setManageButtonClickListener(OnClickListener listener)330 public void setManageButtonClickListener(OnClickListener listener) { 331 NotifRedesignFooter.assertInLegacyMode(); 332 mManageOrHistoryButton.setOnClickListener(listener); 333 } 334 335 /** Set onClickListener for the clear all (end) button. */ setClearAllButtonClickListener(OnClickListener listener)336 public void setClearAllButtonClickListener(OnClickListener listener) { 337 if (mClearAllButtonClickListener == listener) return; 338 mClearAllButtonClickListener = listener; 339 mClearAllButton.setOnClickListener(listener); 340 } 341 342 /** 343 * Whether the touch is outside the Clear all button. 344 */ isOnEmptySpace(float touchX, float touchY)345 public boolean isOnEmptySpace(float touchX, float touchY) { 346 SceneContainerFlag.assertInLegacyMode(); 347 return touchX < mContent.getX() 348 || touchX > mContent.getX() + mContent.getWidth() 349 || touchY < mContent.getY() 350 || touchY > mContent.getY() + mContent.getHeight(); 351 } 352 updateContent()353 private void updateContent() { 354 updateClearAllButtonText(); 355 updateClearAllButtonDescription(); 356 357 if (!NotifRedesignFooter.isEnabled()) { 358 updateManageOrHistoryButtonText(); 359 updateManageOrHistoryButtonDescription(); 360 } 361 362 updateMessageString(); 363 updateMessageIcon(); 364 } 365 366 @Override onConfigurationChanged(Configuration newConfig)367 protected void onConfigurationChanged(Configuration newConfig) { 368 ColorUpdateLogger colorUpdateLogger = ColorUpdateLogger.getInstance(); 369 if (colorUpdateLogger != null) { 370 colorUpdateLogger.logTriggerEvent("Footer.onConfigurationChanged()"); 371 } 372 super.onConfigurationChanged(newConfig); 373 updateColors(); 374 updateContent(); 375 } 376 377 /** 378 * Update the text and background colors for the current color palette and night mode setting. 379 */ updateColors()380 public void updateColors() { 381 Resources.Theme theme = mContext.getTheme(); 382 final @ColorInt int onSurface = mContext.getColor( 383 com.android.internal.R.color.materialColorOnSurface); 384 // Same resource, separate drawables to prevent touch effects from showing on the wrong 385 // button. 386 final Drawable clearAllBg = theme.getDrawable(R.drawable.notif_footer_btn_background); 387 final Drawable settingsBg = theme.getDrawable(R.drawable.notif_footer_btn_background); 388 final Drawable historyBg = NotifRedesignFooter.isEnabled() 389 ? theme.getDrawable(R.drawable.notif_footer_btn_background) : null; 390 final @ColorInt int scHigh; 391 392 if (!notificationFooterBackgroundTintOptimization()) { 393 if (notificationShadeBlur()) { 394 if (mIsBlurSupported) { 395 Color backgroundColor = Color.valueOf( 396 SurfaceEffectColors.surfaceEffect1(getContext())); 397 scHigh = ColorUtils.setAlphaComponent(backgroundColor.toArgb(), 0xFF); 398 // Apply alpha on background drawables. 399 int backgroundAlpha = (int) (backgroundColor.alpha() * 0xFF); 400 clearAllBg.setAlpha(backgroundAlpha); 401 settingsBg.setAlpha(backgroundAlpha); 402 if (historyBg != null) { 403 historyBg.setAlpha(backgroundAlpha); 404 } 405 } else { 406 scHigh = mContext.getColor( 407 com.android.internal.R.color.materialColorSurfaceContainer); 408 } 409 } else { 410 scHigh = mContext.getColor( 411 com.android.internal.R.color.materialColorSurfaceContainerHigh); 412 } 413 if (scHigh != 0) { 414 final ColorFilter bgColorFilter = new PorterDuffColorFilter(scHigh, SRC_ATOP); 415 clearAllBg.setColorFilter(bgColorFilter); 416 settingsBg.setColorFilter(bgColorFilter); 417 if (NotifRedesignFooter.isEnabled()) { 418 historyBg.setColorFilter(bgColorFilter); 419 } 420 } 421 } else { 422 scHigh = 0; 423 } 424 mClearAllButton.setBackground(clearAllBg); 425 mClearAllButton.setTextColor(onSurface); 426 if (NotifRedesignFooter.isEnabled()) { 427 mSettingsButton.setBackground(settingsBg); 428 mSettingsButton.setCompoundDrawableTintList(ColorStateList.valueOf(onSurface)); 429 430 mHistoryButton.setBackground(historyBg); 431 mHistoryButton.setCompoundDrawableTintList(ColorStateList.valueOf(onSurface)); 432 } else { 433 mManageOrHistoryButton.setBackground(settingsBg); 434 mManageOrHistoryButton.setTextColor(onSurface); 435 } 436 mSeenNotifsFooterTextView.setTextColor(onSurface); 437 mSeenNotifsFooterTextView.setCompoundDrawableTintList(ColorStateList.valueOf(onSurface)); 438 ColorUpdateLogger colorUpdateLogger = ColorUpdateLogger.getInstance(); 439 if (colorUpdateLogger != null) { 440 colorUpdateLogger.logEvent("Footer.updateColors()", 441 "textColor(onSurface)=" + hexColorString(onSurface) 442 + " backgroundTint(surfaceContainerHigh)=" + hexColorString(scHigh) 443 + " background=" + DrawableDumpKt.dumpToString(settingsBg)); 444 } 445 } 446 setIsBlurSupported(boolean isBlurSupported)447 public void setIsBlurSupported(boolean isBlurSupported) { 448 if (notificationShadeBlur()) { 449 if (mIsBlurSupported == isBlurSupported) { 450 return; 451 } 452 mIsBlurSupported = isBlurSupported; 453 updateColors(); 454 } 455 } 456 457 @Override 458 @NonNull createExpandableViewState()459 public ExpandableViewState createExpandableViewState() { 460 return new FooterViewState(); 461 } 462 463 public class FooterViewState extends ExpandableViewState { 464 /** 465 * used to hide the content of the footer to animate. 466 * #hide is applied without animation, but #hideContent has animation. 467 */ 468 public boolean hideContent; 469 470 /** 471 * When true, skip animating Y on the next #animateTo. 472 * Once true, remains true until reset in #animateTo. 473 */ 474 public boolean resetY = false; 475 476 @Override copyFrom(ViewState viewState)477 public void copyFrom(ViewState viewState) { 478 super.copyFrom(viewState); 479 if (viewState instanceof FooterViewState) { 480 hideContent = ((FooterViewState) viewState).hideContent; 481 } 482 } 483 484 @Override applyToView(View view)485 public void applyToView(View view) { 486 super.applyToView(view); 487 if (view instanceof FooterView) { 488 FooterView footerView = (FooterView) view; 489 footerView.setContentVisibleAnimated(!hideContent); 490 } 491 } 492 493 @Override animateTo(View child, AnimationProperties properties)494 public void animateTo(View child, AnimationProperties properties) { 495 if (child instanceof FooterView) { 496 // Must set animateY=false before super.animateTo, which checks for animateY 497 if (resetY) { 498 properties.getAnimationFilter().animateY = false; 499 resetY = false; 500 } 501 } 502 super.animateTo(child, properties); 503 } 504 } 505 } 506