1 /*
2  * Copyright (C) 2021 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 package com.android.launcher3.notification;
17 
18 import static com.android.launcher3.anim.Interpolators.scrollInterpolatorForVelocity;
19 import static com.android.launcher3.touch.SingleAxisSwipeDetector.HORIZONTAL;
20 
21 import android.animation.Animator;
22 import android.animation.AnimatorSet;
23 import android.animation.ObjectAnimator;
24 import android.content.Context;
25 import android.graphics.Rect;
26 import android.util.AttributeSet;
27 import android.util.FloatProperty;
28 import android.view.MotionEvent;
29 import android.view.View;
30 import android.widget.FrameLayout;
31 
32 import com.android.launcher3.R;
33 import com.android.launcher3.anim.AnimationSuccessListener;
34 import com.android.launcher3.popup.PopupContainerWithArrow;
35 import com.android.launcher3.touch.BaseSwipeDetector;
36 import com.android.launcher3.touch.OverScroll;
37 import com.android.launcher3.touch.SingleAxisSwipeDetector;
38 
39 import java.util.ArrayList;
40 import java.util.Iterator;
41 import java.util.List;
42 
43 /**
44  * Class to manage the notification UI in a {@link PopupContainerWithArrow}.
45  *
46  * - Has two {@link NotificationMainView} that represent the top two notifications
47  * - Handles dismissing a notification
48  */
49 public class NotificationContainer extends FrameLayout implements SingleAxisSwipeDetector.Listener {
50 
51     private static final FloatProperty<NotificationContainer> DRAG_TRANSLATION_X =
52             new FloatProperty<NotificationContainer>("notificationProgress") {
53                 @Override
54                 public void setValue(NotificationContainer view, float transX) {
55                     view.setDragTranslationX(transX);
56                 }
57 
58                 @Override
59                 public Float get(NotificationContainer view) {
60                     return view.mDragTranslationX;
61                 }
62             };
63 
64     private static final Rect sTempRect = new Rect();
65 
66     private final SingleAxisSwipeDetector mSwipeDetector;
67     private final List<NotificationInfo> mNotificationInfos = new ArrayList<>();
68     private boolean mIgnoreTouch = false;
69 
70     private final ObjectAnimator mContentTranslateAnimator;
71     private float mDragTranslationX = 0;
72 
73     private final NotificationMainView mPrimaryView;
74     private final NotificationMainView mSecondaryView;
75     private PopupContainerWithArrow mPopupContainer;
76 
NotificationContainer(Context context)77     public NotificationContainer(Context context) {
78         this(context, null, 0);
79     }
80 
NotificationContainer(Context context, AttributeSet attrs)81     public NotificationContainer(Context context, AttributeSet attrs) {
82         this(context, attrs, 0);
83     }
84 
NotificationContainer(Context context, AttributeSet attrs, int defStyleAttr)85     public NotificationContainer(Context context, AttributeSet attrs, int defStyleAttr) {
86         super(context, attrs, defStyleAttr);
87         mSwipeDetector = new SingleAxisSwipeDetector(getContext(), this, HORIZONTAL);
88         mSwipeDetector.setDetectableScrollConditions(SingleAxisSwipeDetector.DIRECTION_BOTH, false);
89         mContentTranslateAnimator = ObjectAnimator.ofFloat(this, DRAG_TRANSLATION_X, 0);
90 
91         mPrimaryView = (NotificationMainView) View.inflate(getContext(),
92                 R.layout.notification_content, null);
93         mSecondaryView = (NotificationMainView) View.inflate(getContext(),
94                 R.layout.notification_content, null);
95         mSecondaryView.setAlpha(0);
96 
97         addView(mSecondaryView);
98         addView(mPrimaryView);
99 
100     }
101 
setPopupView(PopupContainerWithArrow popupView)102     public void setPopupView(PopupContainerWithArrow popupView) {
103         mPopupContainer = popupView;
104     }
105 
106     /**
107      * Returns true if we should intercept the swipe.
108      */
onInterceptSwipeEvent(MotionEvent ev)109     public boolean onInterceptSwipeEvent(MotionEvent ev) {
110         if (ev.getAction() == MotionEvent.ACTION_DOWN) {
111             sTempRect.set(getLeft(), getTop(), getRight(), getBottom());
112             mIgnoreTouch = !sTempRect.contains((int) ev.getX(), (int) ev.getY());
113             if (!mIgnoreTouch) {
114                 mPopupContainer.getParent().requestDisallowInterceptTouchEvent(true);
115             }
116         }
117         if (mIgnoreTouch) {
118             return false;
119         }
120         if (mPrimaryView.getNotificationInfo() == null) {
121             // The notification hasn't been populated yet.
122             return false;
123         }
124 
125         mSwipeDetector.onTouchEvent(ev);
126         return mSwipeDetector.isDraggingOrSettling();
127     }
128 
129     /**
130      * Returns true when we should handle the swipe.
131      */
onSwipeEvent(MotionEvent ev)132     public boolean onSwipeEvent(MotionEvent ev) {
133         if (mIgnoreTouch) {
134             return false;
135         }
136         if (mPrimaryView.getNotificationInfo() == null) {
137             // The notification hasn't been populated yet.
138             return false;
139         }
140         return mSwipeDetector.onTouchEvent(ev);
141     }
142 
143     /**
144      * Applies the list of @param notificationInfos to this container.
145      */
applyNotificationInfos(final List<NotificationInfo> notificationInfos)146     public void applyNotificationInfos(final List<NotificationInfo> notificationInfos) {
147         mNotificationInfos.clear();
148         if (notificationInfos.isEmpty()) {
149             mPrimaryView.applyNotificationInfo(null);
150             mSecondaryView.applyNotificationInfo(null);
151             return;
152         }
153         mNotificationInfos.addAll(notificationInfos);
154 
155         NotificationInfo mainNotification = notificationInfos.get(0);
156         mPrimaryView.applyNotificationInfo(mainNotification);
157         mSecondaryView.applyNotificationInfo(notificationInfos.size() > 1
158                 ? notificationInfos.get(1)
159                 : null);
160     }
161 
162     /**
163      * Trims the notifications.
164      * @param notificationKeys List of all valid notification keys.
165      */
trimNotifications(final List<String> notificationKeys)166     public void trimNotifications(final List<String> notificationKeys) {
167         Iterator<NotificationInfo> iterator = mNotificationInfos.iterator();
168         while (iterator.hasNext()) {
169             if (!notificationKeys.contains(iterator.next().notificationKey)) {
170                 iterator.remove();
171             }
172         }
173 
174         NotificationInfo primaryInfo = mNotificationInfos.size() > 0
175                 ? mNotificationInfos.get(0)
176                 : null;
177         NotificationInfo secondaryInfo = mNotificationInfos.size() > 1
178                 ? mNotificationInfos.get(1)
179                 : null;
180 
181         mPrimaryView.applyNotificationInfo(primaryInfo);
182         mSecondaryView.applyNotificationInfo(secondaryInfo);
183 
184         mPrimaryView.onPrimaryDrag(0);
185         mSecondaryView.onSecondaryDrag(0);
186     }
187 
setDragTranslationX(float translationX)188     private void setDragTranslationX(float translationX) {
189         mDragTranslationX = translationX;
190 
191         float progress = translationX / getWidth();
192         mPrimaryView.onPrimaryDrag(progress);
193         if (mSecondaryView.getNotificationInfo() == null) {
194             mSecondaryView.setAlpha(0f);
195         } else {
196             mSecondaryView.onSecondaryDrag(progress);
197         }
198     }
199 
200     // SingleAxisSwipeDetector.Listener's
201     @Override
onDragStart(boolean start, float startDisplacement)202     public void onDragStart(boolean start, float startDisplacement) {
203         mPopupContainer.showArrow(false);
204     }
205 
206     @Override
onDrag(float displacement)207     public boolean onDrag(float displacement) {
208         if (!mPrimaryView.canChildBeDismissed()) {
209             displacement = OverScroll.dampedScroll(displacement, getWidth());
210         }
211 
212         float progress = displacement / getWidth();
213         mPrimaryView.onPrimaryDrag(progress);
214         if (mSecondaryView.getNotificationInfo() == null) {
215             mSecondaryView.setAlpha(0f);
216         } else {
217             mSecondaryView.onSecondaryDrag(progress);
218         }
219         mContentTranslateAnimator.cancel();
220         return true;
221     }
222 
223     @Override
onDragEnd(float velocity)224     public void onDragEnd(float velocity) {
225         final boolean willExit;
226         final float endTranslation;
227         final float startTranslation = mPrimaryView.getTranslationX();
228         final float width = getWidth();
229 
230         if (!mPrimaryView.canChildBeDismissed()) {
231             willExit = false;
232             endTranslation = 0;
233         } else if (mSwipeDetector.isFling(velocity)) {
234             willExit = true;
235             endTranslation = velocity < 0 ? -width : width;
236         } else if (Math.abs(startTranslation) > width / 2f) {
237             willExit = true;
238             endTranslation = (startTranslation < 0 ? -width : width);
239         } else {
240             willExit = false;
241             endTranslation = 0;
242         }
243 
244         long duration = BaseSwipeDetector.calculateDuration(velocity,
245                 (endTranslation - startTranslation) / width);
246 
247         mContentTranslateAnimator.removeAllListeners();
248         mContentTranslateAnimator.setDuration(duration)
249                 .setInterpolator(scrollInterpolatorForVelocity(velocity));
250         mContentTranslateAnimator.setFloatValues(startTranslation, endTranslation);
251 
252         NotificationMainView current = mPrimaryView;
253         mContentTranslateAnimator.addListener(new AnimationSuccessListener() {
254             @Override
255             public void onAnimationSuccess(Animator animator) {
256                 mSwipeDetector.finishedScrolling();
257                 if (willExit) {
258                     current.onChildDismissed();
259                 }
260                 mPopupContainer.showArrow(true);
261             }
262         });
263         mContentTranslateAnimator.start();
264     }
265 
266     /**
267      * Animates the background color to a new color.
268      * @param color The color to change to.
269      * @param animatorSetOut The AnimatorSet where we add the color animator to.
270      */
updateBackgroundColor(int color, AnimatorSet animatorSetOut)271     public void updateBackgroundColor(int color, AnimatorSet animatorSetOut) {
272         mPrimaryView.updateBackgroundColor(color, animatorSetOut);
273         mSecondaryView.updateBackgroundColor(color, animatorSetOut);
274     }
275 
276     /**
277      * Updates the header with a new @param notificationCount.
278      */
updateHeader(int notificationCount)279     public void updateHeader(int notificationCount) {
280         mPrimaryView.updateHeader(notificationCount);
281         mSecondaryView.updateHeader(notificationCount - 1);
282     }
283 }
284