1 /*
2  * Copyright (C) 2017 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.launcher3.notification;
18 
19 import static com.android.launcher3.Utilities.mapToRange;
20 import static com.android.launcher3.anim.Interpolators.LINEAR;
21 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_NOTIFICATION_DISMISSED;
22 
23 import android.animation.AnimatorSet;
24 import android.animation.ValueAnimator;
25 import android.annotation.TargetApi;
26 import android.content.Context;
27 import android.graphics.Outline;
28 import android.graphics.Rect;
29 import android.graphics.drawable.GradientDrawable;
30 import android.os.Build;
31 import android.text.TextUtils;
32 import android.util.AttributeSet;
33 import android.view.View;
34 import android.view.ViewGroup;
35 import android.view.ViewOutlineProvider;
36 import android.widget.LinearLayout;
37 import android.widget.TextView;
38 
39 import androidx.annotation.Nullable;
40 
41 import com.android.launcher3.Launcher;
42 import com.android.launcher3.R;
43 import com.android.launcher3.Utilities;
44 import com.android.launcher3.model.data.ItemInfo;
45 import com.android.launcher3.util.Themes;
46 
47 /**
48  * A {@link android.widget.FrameLayout} that contains a single notification,
49  * e.g. icon + title + text.
50  */
51 @TargetApi(Build.VERSION_CODES.N)
52 public class NotificationMainView extends LinearLayout {
53 
54     // This is used only to track the notification view, so that it can be properly logged.
55     public static final ItemInfo NOTIFICATION_ITEM_INFO = new ItemInfo();
56 
57     // Value when the primary notification main view will be gone (zero alpha).
58     private static final float PRIMARY_GONE_PROGRESS = 0.7f;
59     private static final float PRIMARY_MIN_PROGRESS = 0.40f;
60     private static final float PRIMARY_MAX_PROGRESS = 0.60f;
61     private static final float SECONDARY_MIN_PROGRESS = 0.30f;
62     private static final float SECONDARY_MAX_PROGRESS = 0.50f;
63     private static final float SECONDARY_CONTENT_MAX_PROGRESS = 0.6f;
64 
65     private NotificationInfo mNotificationInfo;
66     private int mBackgroundColor;
67     private TextView mTitleView;
68     private TextView mTextView;
69     private View mIconView;
70 
71     private View mHeader;
72     private View mMainView;
73 
74     private TextView mHeaderCount;
75     private final Rect mOutline = new Rect();
76 
77     // Space between notifications during swipe
78     private final int mNotificationSpace;
79     private final int mMaxTransX;
80     private final int mMaxElevation;
81 
82     private final GradientDrawable mBackground;
83 
NotificationMainView(Context context)84     public NotificationMainView(Context context) {
85         this(context, null, 0);
86     }
87 
NotificationMainView(Context context, AttributeSet attrs)88     public NotificationMainView(Context context, AttributeSet attrs) {
89         this(context, attrs, 0);
90     }
91 
NotificationMainView(Context context, AttributeSet attrs, int defStyle)92     public NotificationMainView(Context context, AttributeSet attrs, int defStyle) {
93         this(context, attrs, defStyle, 0);
94     }
95 
NotificationMainView(Context context, AttributeSet attrs, int defStyle, int defStylRes)96     public NotificationMainView(Context context, AttributeSet attrs, int defStyle, int defStylRes) {
97         super(context, attrs, defStyle, defStylRes);
98 
99         float outlineRadius = Themes.getDialogCornerRadius(context);
100 
101         mBackground = new GradientDrawable();
102         mBackground.setColor(Themes.getAttrColor(context, R.attr.popupColorPrimary));
103         mBackground.setCornerRadius(outlineRadius);
104         setBackground(mBackground);
105 
106         mMaxElevation = getResources().getDimensionPixelSize(R.dimen.deep_shortcuts_elevation);
107         setElevation(mMaxElevation);
108 
109         mMaxTransX = getResources().getDimensionPixelSize(R.dimen.notification_max_trans);
110         mNotificationSpace = getResources().getDimensionPixelSize(R.dimen.notification_space);
111 
112         setClipToOutline(true);
113         setOutlineProvider(new ViewOutlineProvider() {
114             @Override
115             public void getOutline(View view, Outline outline) {
116                 outline.setRoundRect(mOutline, outlineRadius);
117             }
118         });
119     }
120 
121     /**
122      * Updates the header text.
123      * @param notificationCount The number of notifications.
124      */
updateHeader(int notificationCount)125     public void updateHeader(int notificationCount) {
126         final String text;
127         final int visibility;
128         if (notificationCount <= 1) {
129             text = "";
130             visibility = View.INVISIBLE;
131         } else {
132             text = String.valueOf(notificationCount);
133             visibility = View.VISIBLE;
134 
135         }
136         mHeaderCount.setText(text);
137         mHeaderCount.setVisibility(visibility);
138     }
139 
140     @Override
onFinishInflate()141     protected void onFinishInflate() {
142         super.onFinishInflate();
143 
144         ViewGroup textAndBackground = findViewById(R.id.text_and_background);
145         mTitleView = textAndBackground.findViewById(R.id.title);
146         mTextView = textAndBackground.findViewById(R.id.text);
147         mIconView = findViewById(R.id.popup_item_icon);
148         mHeaderCount = findViewById(R.id.notification_count);
149 
150         mHeader = findViewById(R.id.header);
151         mMainView = findViewById(R.id.main_view);
152     }
153 
154     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)155     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
156         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
157         mOutline.set(0, 0, getWidth(), getHeight());
158         invalidateOutline();
159     }
160 
updateBackgroundColor(int color)161     private void updateBackgroundColor(int color) {
162         mBackgroundColor = color;
163         mBackground.setColor(color);
164         if (mNotificationInfo != null) {
165             mIconView.setBackground(mNotificationInfo.getIconForBackground(getContext(),
166                     mBackgroundColor));
167         }
168     }
169 
170     /**
171      * Animates the background color to a new color.
172      * @param color The color to change to.
173      * @param animatorSetOut The AnimatorSet where we add the color animator to.
174      */
updateBackgroundColor(int color, AnimatorSet animatorSetOut)175     public void updateBackgroundColor(int color, AnimatorSet animatorSetOut) {
176         int oldColor = mBackgroundColor;
177         ValueAnimator colors = ValueAnimator.ofArgb(oldColor, color);
178         colors.addUpdateListener(valueAnimator -> {
179             int newColor = (int) valueAnimator.getAnimatedValue();
180             updateBackgroundColor(newColor);
181         });
182         animatorSetOut.play(colors);
183     }
184 
185     /**
186      * Sets the content of this view, animating it after a new icon shifts up if necessary.
187      */
applyNotificationInfo(NotificationInfo notificationInfo)188     public void applyNotificationInfo(NotificationInfo notificationInfo) {
189         mNotificationInfo = notificationInfo;
190         if (notificationInfo == null) {
191             return;
192         }
193         NotificationListener listener = NotificationListener.getInstanceIfConnected();
194         if (listener != null) {
195             listener.setNotificationsShown(new String[] {mNotificationInfo.notificationKey});
196         }
197         CharSequence title = mNotificationInfo.title;
198         CharSequence text = mNotificationInfo.text;
199         if (!TextUtils.isEmpty(title) && !TextUtils.isEmpty(text)) {
200             mTitleView.setText(title.toString());
201             mTextView.setText(text.toString());
202         } else {
203             mTitleView.setMaxLines(2);
204             mTitleView.setText(TextUtils.isEmpty(title) ? text.toString() : title.toString());
205             mTextView.setVisibility(GONE);
206         }
207         mIconView.setBackground(mNotificationInfo.getIconForBackground(getContext(),
208                 mBackgroundColor));
209         if (mNotificationInfo.intent != null) {
210             setOnClickListener(mNotificationInfo);
211         }
212 
213         // Add a stub ItemInfo so that logging populates the correct container and item types
214         // instead of DEFAULT_CONTAINERTYPE and DEFAULT_ITEMTYPE, respectively.
215         setTag(NOTIFICATION_ITEM_INFO);
216     }
217 
218     /**
219      * Sets the alpha of only the child views.
220      */
setContentAlpha(float alpha)221     public void setContentAlpha(float alpha) {
222         mHeader.setAlpha(alpha);
223         mMainView.setAlpha(alpha);
224     }
225 
226     /**
227      * Sets the translation of only the child views.
228      */
setContentTranslationX(float transX)229     public void setContentTranslationX(float transX) {
230         mHeader.setTranslationX(transX);
231         mMainView.setTranslationX(transX);
232     }
233 
234     /**
235      * Updates the alpha, content alpha, and elevation of this view.
236      *
237      * @param progress Range from [0, 1] or [-1, 0]
238      *                 When 0: Full alpha
239      *                 When 1/-1: zero alpha
240      */
onPrimaryDrag(float progress)241     public void onPrimaryDrag(float progress) {
242         float absProgress = Math.abs(progress);
243         final int width = getWidth();
244 
245         float min = PRIMARY_MIN_PROGRESS;
246         float max = PRIMARY_MAX_PROGRESS;
247 
248         if (absProgress < min) {
249             setAlpha(1f);
250             setContentAlpha(1);
251             setElevation(mMaxElevation);
252         } else if (absProgress < max) {
253             setAlpha(1f);
254             setContentAlpha(mapToRange(absProgress, min, max, 1f, 0f, LINEAR));
255             setElevation(Utilities.mapToRange(absProgress, min, max, mMaxElevation, 0, LINEAR));
256         } else {
257             setAlpha(mapToRange(absProgress, max, PRIMARY_GONE_PROGRESS, 1f, 0f, LINEAR));
258             setContentAlpha(0f);
259             setElevation(0f);
260         }
261 
262         setTranslationX(width * progress);
263     }
264 
265     /**
266      * Updates the alpha, content alpha, elevation, and clipping of this view.
267      * @param progress Range from [0, 1] or [-1, 0]
268       *                 When 0: Smallest clipping, zero alpha
269       *                 When 1/-1: Full clip, full alpha
270      */
onSecondaryDrag(float progress)271     public void onSecondaryDrag(float progress) {
272         final float absProgress = Math.abs(progress);
273 
274         float min = SECONDARY_MIN_PROGRESS;
275         float max = SECONDARY_MAX_PROGRESS;
276         float contentMax = SECONDARY_CONTENT_MAX_PROGRESS;
277 
278         if (absProgress < min) {
279             setAlpha(0f);
280             setContentAlpha(0);
281             setElevation(0f);
282         } else if (absProgress < max) {
283             setAlpha(mapToRange(absProgress, min, max, 0, 1f, LINEAR));
284             setContentAlpha(0f);
285             setElevation(0f);
286         } else {
287             setAlpha(1f);
288             setContentAlpha(absProgress > contentMax
289                     ? 1f
290                     : mapToRange(absProgress, max, contentMax, 0, 1f, LINEAR));
291             setElevation(Utilities.mapToRange(absProgress, max, 1, 0, mMaxElevation, LINEAR));
292         }
293 
294         final int width = getWidth();
295         int crop = (int) (width * absProgress);
296         int space = (int) (absProgress > PRIMARY_GONE_PROGRESS
297                 ? mapToRange(absProgress, PRIMARY_GONE_PROGRESS, 1f, mNotificationSpace, 0, LINEAR)
298                 : mNotificationSpace);
299         if (progress < 0) {
300             mOutline.left = Math.max(0, getWidth() - crop + space);
301             mOutline.right = getWidth();
302         } else {
303             mOutline.right = Math.min(getWidth(), crop - space);
304             mOutline.left = 0;
305         }
306 
307         float contentTransX = mMaxTransX * (1f - absProgress);
308         setContentTranslationX(progress < 0
309                 ? contentTransX
310                 : -contentTransX);
311         invalidateOutline();
312     }
313 
314     public @Nullable NotificationInfo getNotificationInfo() {
315         return mNotificationInfo;
316     }
317 
318     public boolean canChildBeDismissed() {
319         return mNotificationInfo != null && mNotificationInfo.dismissable;
320     }
321 
322     public void onChildDismissed() {
323         Launcher launcher = Launcher.getLauncher(getContext());
324         launcher.getPopupDataProvider().cancelNotification(
325                 mNotificationInfo.notificationKey);
326         launcher.getStatsLogManager().logger().log(LAUNCHER_NOTIFICATION_DISMISSED);
327     }
328 }
329