• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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