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