• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 package com.android.car.notification;
2 
3 import android.animation.Animator;
4 import android.animation.AnimatorInflater;
5 import android.animation.AnimatorListenerAdapter;
6 import android.animation.AnimatorSet;
7 import android.animation.ObjectAnimator;
8 import android.app.ActivityManager;
9 import android.car.drivingstate.CarUxRestrictions;
10 import android.car.drivingstate.CarUxRestrictionsManager;
11 import android.content.Context;
12 import android.content.Intent;
13 import android.graphics.Rect;
14 import android.os.Handler;
15 import android.os.UserHandle;
16 import android.provider.Settings;
17 import android.util.AttributeSet;
18 import android.view.KeyEvent;
19 import android.view.View;
20 import android.widget.Button;
21 import android.widget.TextView;
22 
23 import androidx.annotation.NonNull;
24 import androidx.constraintlayout.widget.ConstraintLayout;
25 import androidx.recyclerview.widget.DefaultItemAnimator;
26 import androidx.recyclerview.widget.LinearLayoutManager;
27 import androidx.recyclerview.widget.RecyclerView;
28 import androidx.recyclerview.widget.RecyclerView.OnScrollListener;
29 
30 import com.android.car.uxr.UxrContentLimiterImpl;
31 import com.android.internal.statusbar.IStatusBarService;
32 
33 import java.util.ArrayList;
34 import java.util.HashSet;
35 import java.util.List;
36 import java.util.Set;
37 import java.util.TreeMap;
38 
39 
40 /**
41  * Layout that contains Car Notifications.
42  *
43  * It does some extra setup in the onFinishInflate method because it may not get used from an
44  * activity where one would normally attach RecyclerViews
45  */
46 public class CarNotificationView extends ConstraintLayout
47         implements CarUxRestrictionsManager.OnUxRestrictionsChangedListener {
48     public static final String TAG = "CarNotificationView";
49 
50     private CarNotificationViewAdapter mAdapter;
51     private Context mContext;
52     private LinearLayoutManager mLayoutManager;
53     private NotificationClickHandlerFactory mClickHandlerFactory;
54     private NotificationDataManager mNotificationDataManager;
55     private boolean mIsClearAllActive = false;
56     private List<NotificationGroup> mNotifications;
57     private UxrContentLimiterImpl mUxrContentLimiter;
58     private KeyEventHandler mKeyEventHandler;
59     private RecyclerView mListView;
60     private Button mManageButton;
61     private TextView mEmptyNotificationHeaderText;
62 
CarNotificationView(Context context, AttributeSet attrs)63     public CarNotificationView(Context context, AttributeSet attrs) {
64         super(context, attrs);
65         mContext = context;
66     }
67 
68     /**
69      * Attaches the CarNotificationViewAdapter and CarNotificationItemTouchListener to the
70      * notification list.
71      */
72     @Override
onFinishInflate()73     protected void onFinishInflate() {
74         super.onFinishInflate();
75         mListView = findViewById(R.id.notifications);
76 
77         mListView.setClipChildren(false);
78         mLayoutManager = new LinearLayoutManager(mContext);
79         mListView.setLayoutManager(mLayoutManager);
80         mListView.addItemDecoration(new TopAndBottomOffsetDecoration(
81                 mContext.getResources().getDimensionPixelSize(R.dimen.item_spacing)));
82         mListView.addItemDecoration(new ItemSpacingDecoration(
83                 mContext.getResources().getDimensionPixelSize(R.dimen.item_spacing)));
84         mAdapter = new CarNotificationViewAdapter(mContext, /* isGroupNotificationAdapter= */
85                 false, this::startClearAllNotifications);
86         mListView.setAdapter(mAdapter);
87 
88         mUxrContentLimiter = new UxrContentLimiterImpl(mContext, R.xml.uxr_config);
89         mUxrContentLimiter.setAdapter(mAdapter);
90         mUxrContentLimiter.start();
91 
92         mListView.addOnItemTouchListener(new CarNotificationItemTouchListener(mContext, mAdapter));
93 
94         mListView.addOnScrollListener(new OnScrollListener() {
95             @Override
96             public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
97                 super.onScrollStateChanged(recyclerView, newState);
98                 // RecyclerView is not currently scrolling.
99                 if (newState == RecyclerView.SCROLL_STATE_IDLE) {
100                     setVisibleNotificationsAsSeen();
101                 }
102             }
103         });
104         mListView.setItemAnimator(new DefaultItemAnimator(){
105             @Override
106             public boolean animateChange(RecyclerView.ViewHolder oldHolder, RecyclerView.ViewHolder
107                     newHolder, int fromX, int fromY, int toX, int toY) {
108                 // return without animation to prevent flashing on notification update.
109                 dispatchChangeFinished(oldHolder, /* oldItem= */ true);
110                 dispatchChangeFinished(newHolder, /* oldItem= */ false);
111                 return true;
112             }
113         });
114 
115         Button clearAllButton = findViewById(R.id.clear_all_button);
116         mEmptyNotificationHeaderText = findViewById(R.id.empty_notification_text);
117         mManageButton = findViewById(R.id.manage_button);
118         mManageButton.setOnClickListener(this::manageButtonOnClickListener);
119 
120         if (clearAllButton != null) {
121             clearAllButton.setOnClickListener(v -> startClearAllNotifications());
122         }
123     }
124 
125     @Override
dispatchKeyEvent(KeyEvent event)126     public boolean dispatchKeyEvent(KeyEvent event) {
127         if (super.dispatchKeyEvent(event)) {
128             return true;
129         }
130 
131         if (mKeyEventHandler != null) {
132             return mKeyEventHandler.dispatchKeyEvent(event);
133         }
134 
135         return false;
136     }
137 
138     /** Sets a {@link KeyEventHandler} to help interact with the notification panel. */
setKeyEventHandler(KeyEventHandler keyEventHandler)139     public void setKeyEventHandler(KeyEventHandler keyEventHandler) {
140         mKeyEventHandler = keyEventHandler;
141     }
142 
143     /**
144      * Updates notifications and update views.
145      */
setNotifications(List<NotificationGroup> notifications)146     public void setNotifications(List<NotificationGroup> notifications) {
147         mNotifications = notifications;
148         mAdapter.setNotifications(notifications, /* setRecyclerViewListHeaderAndFooter= */ true);
149 
150         if (mAdapter.hasNotifications()) {
151             mListView.setVisibility(View.VISIBLE);
152             mEmptyNotificationHeaderText.setVisibility(View.GONE);
153             mManageButton.setVisibility(View.GONE);
154         } else {
155             mListView.setVisibility(View.GONE);
156             mEmptyNotificationHeaderText.setVisibility(View.VISIBLE);
157             mManageButton.setVisibility(View.VISIBLE);
158         }
159     }
160 
161     /**
162      * Collapses all expanded groups and empties notifications being cleared set.
163      */
resetState()164     public void resetState() {
165         mAdapter.collapseAllGroups();
166         mAdapter.setChildNotificationsBeingCleared(new HashSet());
167     }
168 
169     @Override
onUxRestrictionsChanged(CarUxRestrictions restrictionInfo)170     public void onUxRestrictionsChanged(CarUxRestrictions restrictionInfo) {
171         mAdapter.setCarUxRestrictions(restrictionInfo);
172     }
173 
174     /**
175      * Sets the NotificationClickHandlerFactory that allows for a hook to run a block off code
176      * when  the notification is clicked. This is useful to dismiss a screen after
177      * a notification list clicked.
178      */
setClickHandlerFactory(NotificationClickHandlerFactory clickHandlerFactory)179     public void setClickHandlerFactory(NotificationClickHandlerFactory clickHandlerFactory) {
180         mClickHandlerFactory = clickHandlerFactory;
181         mAdapter.setClickHandlerFactory(clickHandlerFactory);
182     }
183 
184     /**
185      * Sets NotificationDataManager that handles additional states for notifications such as "seen",
186      * and muting a messaging type notification.
187      *
188      * @param notificationDataManager An instance of NotificationDataManager.
189      */
setNotificationDataManager(NotificationDataManager notificationDataManager)190     public void setNotificationDataManager(NotificationDataManager notificationDataManager) {
191         mNotificationDataManager = notificationDataManager;
192         mAdapter.setNotificationDataManager(notificationDataManager);
193     }
194 
195     /**
196      * A {@link RecyclerView.ItemDecoration} that will add a top offset to the first item and bottom
197      * offset to the last item in the RecyclerView it is added to.
198      */
199     private static class TopAndBottomOffsetDecoration extends RecyclerView.ItemDecoration {
200         private int mTopAndBottomOffset;
201 
TopAndBottomOffsetDecoration(int topOffset)202         private TopAndBottomOffsetDecoration(int topOffset) {
203             mTopAndBottomOffset = topOffset;
204         }
205 
206         @Override
getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state)207         public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
208                 RecyclerView.State state) {
209             super.getItemOffsets(outRect, view, parent, state);
210             int position = parent.getChildAdapterPosition(view);
211 
212             if (position == 0) {
213                 outRect.top = mTopAndBottomOffset;
214             }
215             if (position == state.getItemCount() - 1) {
216                 outRect.bottom = mTopAndBottomOffset;
217             }
218         }
219     }
220 
221     /**
222      * Identifies dismissible notifications views and animates them out in the order
223      * specified in config. Calls finishClearNotifications on animation end.
224      */
startClearAllNotifications()225     private void startClearAllNotifications() {
226         // Prevent invoking the click listeners again until the current clear all flow is complete.
227         if (mIsClearAllActive) {
228             return;
229         }
230         mIsClearAllActive = true;
231 
232         List<NotificationGroup> dismissibleNotifications = getAllDismissibleNotifications();
233         List<View> dismissibleNotificationViews = getNotificationViews(dismissibleNotifications);
234 
235         if (dismissibleNotificationViews.isEmpty()) {
236             finishClearAllNotifications(dismissibleNotifications);
237             return;
238         }
239 
240         registerChildNotificationsBeingCleared(dismissibleNotifications);
241         AnimatorSet animatorSet = createDismissAnimation(dismissibleNotificationViews);
242         animatorSet.addListener(new AnimatorListenerAdapter() {
243             @Override
244             public void onAnimationEnd(Animator animator) {
245                 finishClearAllNotifications(dismissibleNotifications);
246             }
247         });
248         animatorSet.start();
249     }
250 
251     /**
252      * Returns a List of all Notification Groups that are dismissible.
253      */
getAllDismissibleNotifications()254     private List<NotificationGroup> getAllDismissibleNotifications() {
255         List<NotificationGroup> notifications = new ArrayList<>();
256         mNotifications.forEach(notificationGroup -> {
257             if (notificationGroup.isDismissible()) {
258                 notifications.add(notificationGroup);
259             }
260         });
261         return notifications;
262     }
263 
264     /**
265      * Returns the Views that are bound to the provided notifications, sorted so that their
266      * positions are in the ascending order.
267      *
268      * <p>Note: Provided notifications might not have Views bound to them.</p>
269      */
getNotificationViews(List<NotificationGroup> notifications)270     private List<View> getNotificationViews(List<NotificationGroup> notifications) {
271         Set notificationIds = new HashSet();
272         notifications.forEach(notificationGroup -> {
273             long id = notificationGroup.isGroup() ? notificationGroup.getGroupKey().hashCode() :
274                     notificationGroup.getSingleNotification().getKey().hashCode();
275             notificationIds.add(id);
276         });
277 
278         TreeMap<Integer, View> notificationViews = new TreeMap<>();
279         for (int i = 0; i < mListView.getChildCount(); i++) {
280             View currentChildView = mListView.getChildAt(i);
281             RecyclerView.ViewHolder holder = mListView.getChildViewHolder(currentChildView);
282             int position = holder.getLayoutPosition();
283             if (notificationIds.contains(mAdapter.getItemId(position))) {
284                 notificationViews.put(position, currentChildView);
285             }
286         }
287         List<View> notificationViewsSorted = new ArrayList<>(notificationViews.values());
288 
289         return notificationViewsSorted;
290     }
291 
292     /**
293      *  Register child notifications being cleared to prevent them from appearing briefly while
294      *  clear all flow is still processing.
295      */
registerChildNotificationsBeingCleared( List<NotificationGroup> groupNotificationsBeingCleared)296     private void registerChildNotificationsBeingCleared(
297             List<NotificationGroup> groupNotificationsBeingCleared) {
298         HashSet<AlertEntry> childNotificationsBeingCleared = new HashSet<>();
299         groupNotificationsBeingCleared.forEach(notificationGroup -> {
300             notificationGroup.getChildNotifications().forEach(notification -> {
301                 childNotificationsBeingCleared.add(notification);
302             });
303         });
304         mAdapter.setChildNotificationsBeingCleared(childNotificationsBeingCleared);
305     }
306 
307     /**
308      * Returns {@link AnimatorSet} for dismissing notifications from the clear all event.
309      */
createDismissAnimation(List<View> dismissibleNotificationViews)310     private AnimatorSet createDismissAnimation(List<View> dismissibleNotificationViews) {
311         ArrayList<Animator> animators = new ArrayList<>();
312         boolean dismissFromBottomUp = getContext().getResources().getBoolean(
313                 R.bool.config_clearAllNotificationsAnimationFromBottomUp);
314         int delayInterval = getContext().getResources().getInteger(
315                 R.integer.clear_all_notifications_animation_delay_interval_ms);
316         for (int i = 0; i < dismissibleNotificationViews.size(); i++) {
317             View currentView = dismissibleNotificationViews.get(i);
318             ObjectAnimator animator = (ObjectAnimator) AnimatorInflater.loadAnimator(mContext,
319                     R.animator.clear_all_animate_out);
320             animator.setTarget(currentView);
321 
322             /*
323              * Each animator is assigned a different start delay value in order to generate the
324              * animation effect of dismissing notifications one by one.
325              * Therefore, the delay calculation depends on whether the notifications are
326              * dismissed from bottom up or from top down.
327              */
328             int delayMultiplier = dismissFromBottomUp ? dismissibleNotificationViews.size() - i : i;
329             int delay = delayInterval * delayMultiplier;
330 
331             animator.setStartDelay(delay);
332             animators.add(animator);
333         }
334         ObjectAnimator[] animatorsArray = animators.toArray(new ObjectAnimator[animators.size()]);
335 
336         AnimatorSet animatorSet = new AnimatorSet();
337         animatorSet.playTogether(animatorsArray);
338 
339         return animatorSet;
340     }
341 
342     /**
343      * Clears the provided notifications with {@link IStatusBarService} and optionally collapses the
344      * shade panel.
345      */
finishClearAllNotifications(List<NotificationGroup> dismissibleNotifications)346     private void finishClearAllNotifications(List<NotificationGroup> dismissibleNotifications) {
347         boolean collapsePanel = getContext().getResources().getBoolean(
348                 R.bool.config_collapseShadePanelAfterClearAllNotifications);
349         int collapsePanelDelay = getContext().getResources().getInteger(
350                 R.integer.delay_between_clear_all_notifications_end_and_collapse_shade_panel_ms);
351 
352         mClickHandlerFactory.clearNotifications(dismissibleNotifications);
353 
354         if (collapsePanel) {
355             Handler handler = getHandler();
356             if (handler != null) {
357                 handler.postDelayed(() -> {
358                     mClickHandlerFactory.collapsePanel();
359                 }, collapsePanelDelay);
360             }
361         }
362 
363         mIsClearAllActive = false;
364     }
365 
366     /**
367      * A {@link RecyclerView.ItemDecoration} that will add spacing between each item in the
368      * RecyclerView that it is added to.
369      */
370     private static class ItemSpacingDecoration extends RecyclerView.ItemDecoration {
371         private int mItemSpacing;
372 
ItemSpacingDecoration(int itemSpacing)373         private ItemSpacingDecoration(int itemSpacing) {
374             mItemSpacing = itemSpacing;
375         }
376 
377         @Override
getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state)378         public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
379                 RecyclerView.State state) {
380             super.getItemOffsets(outRect, view, parent, state);
381             int position = parent.getChildAdapterPosition(view);
382 
383             // Skip offset for last item.
384             if (position == state.getItemCount() - 1) {
385                 return;
386             }
387 
388             outRect.bottom = mItemSpacing;
389         }
390     }
391 
392     /**
393      * Sets currently visible notifications as "seen".
394      */
setVisibleNotificationsAsSeen()395     public void setVisibleNotificationsAsSeen() {
396         int firstVisible = mLayoutManager.findFirstVisibleItemPosition();
397         int lastVisible = mLayoutManager.findLastVisibleItemPosition();
398 
399         // No visible items are found.
400         if (firstVisible == RecyclerView.NO_POSITION) return;
401 
402         mAdapter.setNotificationsAsSeen(firstVisible, lastVisible);
403     }
404 
manageButtonOnClickListener(View v)405     private void manageButtonOnClickListener(View v) {
406         Intent intent = new Intent(Settings.ACTION_NOTIFICATION_SETTINGS);
407         intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK
408                 | Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
409         intent.addCategory(Intent.CATEGORY_DEFAULT);
410         mContext.startActivityAsUser(intent, UserHandle.of(ActivityManager.getCurrentUser()));
411 
412         if (mClickHandlerFactory != null) mClickHandlerFactory.collapsePanel();
413     }
414 
415     /** An interface to help interact with the notification panel. */
416     public interface KeyEventHandler {
417         /** Allows handling of a {@link KeyEvent} if it isn't already handled by the superclass. */
dispatchKeyEvent(KeyEvent event)418         boolean dispatchKeyEvent(KeyEvent event);
419     }
420 }
421