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