• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2020 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.settings.notification.history;
18 
19 import static android.provider.Settings.Secure.NOTIFICATION_HISTORY_ENABLED;
20 import static android.view.View.GONE;
21 import static android.view.View.VISIBLE;
22 
23 import static androidx.core.view.accessibility.AccessibilityEventCompat.TYPE_VIEW_ACCESSIBILITY_FOCUSED;
24 
25 import android.annotation.AttrRes;
26 import android.annotation.ColorInt;
27 import android.annotation.DrawableRes;
28 import android.app.ActionBar;
29 import android.app.ActivityManager;
30 import android.app.INotificationManager;
31 import android.content.ComponentName;
32 import android.content.Context;
33 import android.content.pm.PackageManager;
34 import android.content.res.Resources;
35 import android.content.res.TypedArray;
36 import android.os.Bundle;
37 import android.os.RemoteException;
38 import android.os.ServiceManager;
39 import android.os.UserHandle;
40 import android.os.UserManager;
41 import android.provider.Settings;
42 import android.service.notification.NotificationListenerService;
43 import android.service.notification.StatusBarNotification;
44 import android.util.Log;
45 import android.util.Slog;
46 import android.view.ContextThemeWrapper;
47 import android.view.LayoutInflater;
48 import android.view.View;
49 import android.view.ViewGroup;
50 import android.widget.CompoundButton.OnCheckedChangeListener;
51 import android.widget.ImageView;
52 import android.widget.TextView;
53 
54 import androidx.core.graphics.ColorUtils;
55 import androidx.recyclerview.widget.LinearLayoutManager;
56 import androidx.recyclerview.widget.RecyclerView;
57 
58 import com.android.internal.logging.UiEvent;
59 import com.android.internal.logging.UiEventLogger;
60 import com.android.internal.logging.UiEventLoggerImpl;
61 import com.android.internal.widget.NotificationExpandButton;
62 import com.android.settings.R;
63 import com.android.settings.notification.NotificationBackend;
64 import com.android.settingslib.collapsingtoolbar.CollapsingToolbarBaseActivity;
65 import com.android.settingslib.utils.StringUtil;
66 import com.android.settingslib.utils.ThreadUtils;
67 import com.android.settingslib.widget.MainSwitchBar;
68 
69 import java.util.ArrayList;
70 import java.util.Arrays;
71 import java.util.concurrent.CountDownLatch;
72 import java.util.concurrent.Future;
73 import java.util.concurrent.TimeUnit;
74 
75 public class NotificationHistoryActivity extends CollapsingToolbarBaseActivity {
76 
77     private static String TAG = "NotifHistory";
78     // MAX_RECENT_DISMISS_ITEM_COUNT needs to be less or equals than
79     // R.integer.config_notificationServiceArchiveSize, which is the Number of notifications kept
80     // in the notification service historical archive
81     private static final int MAX_RECENT_DISMISS_ITEM_COUNT = 50;
82     private static final int HISTORY_HOURS = 24;
83 
84     private ViewGroup mHistoryOn;
85     private ViewGroup mHistoryOff;
86     private ViewGroup mHistoryEmpty;
87     private ViewGroup mTodayView;
88     private ViewGroup mSnoozeView;
89     private ViewGroup mDismissView;
90     private MainSwitchBar mSwitchBar;
91 
92     private HistoryLoader mHistoryLoader;
93     private INotificationManager mNm;
94     private UserManager mUm;
95     private PackageManager mPm;
96     private CountDownLatch mCountdownLatch;
97     private Future mCountdownFuture;
98 
99     private UiEventLogger mUiEventLogger = new UiEventLoggerImpl();
100 
101     enum NotificationHistoryEvent implements UiEventLogger.UiEventEnum {
102         @UiEvent(doc = "User turned on notification history")
103         NOTIFICATION_HISTORY_ON(504),
104 
105         @UiEvent(doc = "User turned off notification history")
106         NOTIFICATION_HISTORY_OFF(505),
107 
108         @UiEvent(doc = "User opened notification history page")
109         NOTIFICATION_HISTORY_OPEN(506),
110 
111         @UiEvent(doc = "User closed notification history page")
112         NOTIFICATION_HISTORY_CLOSE(507),
113 
114         @UiEvent(doc = "User clicked on a notification history item in recently dismissed section")
115         NOTIFICATION_HISTORY_RECENT_ITEM_CLICK(508),
116 
117         @UiEvent(doc = "User clicked on a notification history item in snoozed section")
118         NOTIFICATION_HISTORY_SNOOZED_ITEM_CLICK(509),
119 
120         @UiEvent(doc = "User clicked to expand the notification history of a package (app)")
121         NOTIFICATION_HISTORY_PACKAGE_HISTORY_OPEN(510),
122 
123         @UiEvent(doc = "User clicked to close the notification history of a package (app)")
124         NOTIFICATION_HISTORY_PACKAGE_HISTORY_CLOSE(511),
125 
126         @UiEvent(doc = "User clicked on a notification history item in an expanded by-app section")
127         NOTIFICATION_HISTORY_OLDER_ITEM_CLICK(512),
128 
129         @UiEvent(doc = "User dismissed a notification history item in an expanded by-app section")
130         NOTIFICATION_HISTORY_OLDER_ITEM_DELETE(513);
131 
132         private int mId;
133 
NotificationHistoryEvent(int id)134         NotificationHistoryEvent(int id) {
135             mId = id;
136         }
137 
138         @Override
getId()139         public int getId() {
140             return mId;
141         }
142     }
143 
144     private HistoryLoader.OnHistoryLoaderListener mOnHistoryLoaderListener = notifications -> {
145         findViewById(R.id.today_list).setVisibility(
146                 notifications.isEmpty() ? GONE : VISIBLE);
147         mCountdownLatch.countDown();
148         View recyclerView = mTodayView.findViewById(R.id.apps);
149         recyclerView.setClipToOutline(true);
150         // for each package, new header and recycler view
151         for (int i = 0, notificationsSize = notifications.size(); i < notificationsSize; i++) {
152             NotificationHistoryPackage nhp = notifications.get(i);
153             View viewForPackage = LayoutInflater.from(this)
154                     .inflate(R.layout.notification_history_app_layout, null);
155 
156             int cornerType = ROUND_CORNER_CENTER;
157             if (i == (notificationsSize - 1)) {
158                 cornerType |= ROUND_CORNER_BOTTOM;
159             }
160             if (i == 0) {
161                 cornerType |= ROUND_CORNER_TOP;
162             }
163             int backgroundRes = NotificationHistoryActivity.getRoundCornerDrawableRes(cornerType);
164             viewForPackage.setBackgroundResource(backgroundRes);
165 
166             final View container = viewForPackage.findViewById(R.id.notification_list_wrapper);
167             container.setVisibility(GONE);
168             View header = viewForPackage.findViewById(R.id.app_header);
169             NotificationExpandButton expand = viewForPackage.findViewById(
170                     com.android.internal.R.id.expand_button);
171             int textColor = obtainThemeColor(android.R.attr.textColorPrimary);
172             int backgroundColor = obtainThemeColor(android.R.attr.colorBackgroundFloating);
173             int pillColor = ColorUtils.blendARGB(textColor, backgroundColor, 0.9f);
174             expand.setDefaultPillColor(pillColor);
175             expand.setDefaultTextColor(textColor);
176             expand.setExpanded(false);
177             header.setStateDescription(container.getVisibility() == VISIBLE
178                     ? getString(R.string.condition_expand_hide)
179                     : getString(R.string.condition_expand_show));
180             int finalI = i;
181             header.setOnClickListener(v -> {
182                 container.setVisibility(container.getVisibility() == VISIBLE
183                         ? GONE : VISIBLE);
184                 expand.setExpanded(container.getVisibility() == VISIBLE);
185                 header.setStateDescription(container.getVisibility() == VISIBLE
186                         ? getString(R.string.condition_expand_hide)
187                         : getString(R.string.condition_expand_show));
188                 header.sendAccessibilityEvent(TYPE_VIEW_ACCESSIBILITY_FOCUSED);
189                 mUiEventLogger.logWithPosition((container.getVisibility() == VISIBLE)
190                                 ? NotificationHistoryEvent.NOTIFICATION_HISTORY_PACKAGE_HISTORY_OPEN
191                               : NotificationHistoryEvent.NOTIFICATION_HISTORY_PACKAGE_HISTORY_CLOSE,
192                         nhp.uid, nhp.pkgName, finalI);
193             });
194 
195             TextView label = viewForPackage.findViewById(R.id.label);
196             label.setText(nhp.label != null ? nhp.label : nhp.pkgName);
197             label.setContentDescription(mUm.getBadgedLabelForUser(label.getText(),
198                     UserHandle.getUserHandleForUid(nhp.uid)));
199             ImageView icon = viewForPackage.findViewById(R.id.icon);
200             icon.setImageDrawable(nhp.icon);
201 
202             TextView count = viewForPackage.findViewById(R.id.count);
203             count.setText(StringUtil.getIcuPluralsString(this, nhp.notifications.size(),
204                     R.string.notification_history_count));
205 
206             final NotificationHistoryRecyclerView rv =
207                     viewForPackage.findViewById(R.id.notification_list);
208             rv.setAdapter(new NotificationHistoryAdapter(mNm, rv,
209                     newCount -> {
210                         count.setText(StringUtil.getIcuPluralsString(this, newCount,
211                                 R.string.notification_history_count));
212                         if (newCount == 0) {
213                             viewForPackage.setVisibility(GONE);
214                         }
215                     }, mUiEventLogger));
216             ((NotificationHistoryAdapter) rv.getAdapter()).onRebuildComplete(
217                     new ArrayList<>(nhp.notifications));
218 
219             mTodayView.addView(viewForPackage);
220         }
221     };
222 
223     @Override
onCreate(Bundle savedInstanceState)224     protected void onCreate(Bundle savedInstanceState) {
225         super.onCreate(savedInstanceState);
226         setTitle(R.string.notification_history);
227         setContentView(R.layout.notification_history);
228         mTodayView = findViewById(R.id.apps);
229         mSnoozeView = findViewById(R.id.snoozed_list);
230         mDismissView = findViewById(R.id.recently_dismissed_list);
231         mHistoryOff = findViewById(R.id.history_off);
232         mHistoryOn = findViewById(R.id.history_on);
233         mHistoryEmpty = findViewById(R.id.history_on_empty);
234         mSwitchBar = findViewById(R.id.main_switch_bar);
235         ((TextView) findViewById(R.id.today_header)).setText(
236                 getString(R.string.notification_history_today, HISTORY_HOURS));
237 
238         ActionBar actionBar = getActionBar();
239         if (actionBar != null) {
240             actionBar.setDisplayHomeAsUpEnabled(true);
241             actionBar.setHomeButtonEnabled(true);
242             actionBar.setDisplayShowTitleEnabled(true);
243         }
244     }
245 
246     @Override
onResume()247     protected void onResume() {
248         super.onResume();
249 
250         mPm = getPackageManager();
251         mUm = getSystemService(UserManager.class);
252         // wait for history loading and recent/snooze loading
253         mCountdownLatch = new CountDownLatch(2);
254 
255         mTodayView.removeAllViews();
256         mHistoryLoader = new HistoryLoader(this, new NotificationBackend(), mPm);
257         mHistoryLoader.load(mOnHistoryLoaderListener);
258 
259         mNm = INotificationManager.Stub.asInterface(
260                 ServiceManager.getService(Context.NOTIFICATION_SERVICE));
261         try {
262             mListener.registerAsSystemService(this, new ComponentName(getPackageName(),
263                     this.getClass().getCanonicalName()), ActivityManager.getCurrentUser());
264         } catch (RemoteException e) {
265             Log.e(TAG, "Cannot register listener", e);
266         }
267 
268         bindSwitch();
269 
270         mCountdownFuture = ThreadUtils.postOnBackgroundThread(() -> {
271             try {
272                 mCountdownLatch.await(2, TimeUnit.SECONDS);
273             } catch (InterruptedException e) {
274                 Slog.e(TAG, "timed out waiting for loading", e);
275             }
276             ThreadUtils.postOnMainThread(() -> {
277                 if (mSwitchBar.isChecked()
278                         && findViewById(R.id.today_list).getVisibility() == GONE
279                         && mSnoozeView.getVisibility() == GONE
280                         && mDismissView.getVisibility() == GONE) {
281                     mHistoryOn.setVisibility(GONE);
282                     mHistoryEmpty.setVisibility(VISIBLE);
283                 }
284             });
285         });
286 
287         mUiEventLogger.log(NotificationHistoryEvent.NOTIFICATION_HISTORY_OPEN);
288     }
289 
290     @Override
onPause()291     public void onPause() {
292         try {
293             mListener.unregisterAsSystemService();
294         } catch (RemoteException e) {
295             Log.e(TAG, "Cannot unregister listener", e);
296         }
297         mUiEventLogger.log(NotificationHistoryEvent.NOTIFICATION_HISTORY_CLOSE);
298         super.onPause();
299     }
300 
301     @Override
onDestroy()302     public void onDestroy() {
303         if (mCountdownFuture != null) {
304             mCountdownFuture.cancel(true);
305         }
306         super.onDestroy();
307     }
308 
309     public static final int ROUND_CORNER_CENTER = 1;
310     public static final int ROUND_CORNER_TOP = 1 << 1;
311     public static final int ROUND_CORNER_BOTTOM = 1 << 2;
312 
getRoundCornerDrawableRes(int cornerType)313     public static @DrawableRes int getRoundCornerDrawableRes(int cornerType) {
314 
315         if ((cornerType & ROUND_CORNER_CENTER) == 0) {
316             return 0;
317         }
318 
319         if (((cornerType & ROUND_CORNER_TOP) != 0) && ((cornerType & ROUND_CORNER_BOTTOM) == 0)) {
320             // the first
321             return com.android.settingslib.widget.theme.R.drawable.settingslib_round_background_top;
322         } else if (((cornerType & ROUND_CORNER_BOTTOM) != 0)
323                 && ((cornerType & ROUND_CORNER_TOP) == 0)) {
324             // the last
325             return com.android.settingslib.widget.theme.R.drawable.settingslib_round_background_bottom;
326         } else if (((cornerType & ROUND_CORNER_TOP) != 0)
327                 && ((cornerType & ROUND_CORNER_BOTTOM) != 0)) {
328             // the only one preference
329             return com.android.settingslib.widget.theme.R.drawable.settingslib_round_background;
330         } else {
331             // in the center
332             return com.android.settingslib.widget.theme.R.drawable.settingslib_round_background_center;
333         }
334     }
335 
obtainThemeColor(@ttrRes int attrRes)336     private @ColorInt int obtainThemeColor(@AttrRes int attrRes) {
337         Resources.Theme theme = new ContextThemeWrapper(this,
338                 android.R.style.Theme_DeviceDefault_DayNight).getTheme();
339         try (TypedArray ta = theme.obtainStyledAttributes(new int[]{attrRes})) {
340             return ta == null ? 0 : ta.getColor(0, 0);
341         }
342     }
343 
bindSwitch()344     private void bindSwitch() {
345         if (mSwitchBar != null) {
346             mSwitchBar.show();
347             mSwitchBar.setTitle(getString(R.string.notification_history_toggle));
348             try {
349                 mSwitchBar.addOnSwitchChangeListener(mOnSwitchClickListener);
350             } catch (IllegalStateException e) {
351                 // an exception is thrown if you try to add the listener twice
352             }
353             mSwitchBar.setChecked(Settings.Secure.getInt(getContentResolver(),
354                     NOTIFICATION_HISTORY_ENABLED, 0) == 1);
355             toggleViews(mSwitchBar.isChecked());
356         }
357     }
358 
toggleViews(boolean isChecked)359     private void toggleViews(boolean isChecked) {
360         if (isChecked) {
361             mHistoryOff.setVisibility(GONE);
362             mHistoryOn.setVisibility(VISIBLE);
363         } else {
364             mHistoryOn.setVisibility(GONE);
365             mHistoryOff.setVisibility(VISIBLE);
366             mTodayView.removeAllViews();
367         }
368         mHistoryEmpty.setVisibility(GONE);
369     }
370 
371     private final OnCheckedChangeListener mOnSwitchClickListener =
372             (switchView, isChecked) -> {
373                 int oldState = 0;
374                 try {
375                     oldState = Settings.Secure.getInt(getContentResolver(),
376                             NOTIFICATION_HISTORY_ENABLED);
377                 } catch (Settings.SettingNotFoundException ignored) {
378                 }
379                 final int newState = isChecked ? 1 : 0;
380                 if (oldState != newState) {
381                     Settings.Secure.putInt(
382                             getContentResolver(), NOTIFICATION_HISTORY_ENABLED, newState);
383                     mUiEventLogger.log(isChecked ? NotificationHistoryEvent.NOTIFICATION_HISTORY_ON
384                             : NotificationHistoryEvent.NOTIFICATION_HISTORY_OFF);
385                     Log.d(TAG, "onSwitchChange history to " + isChecked);
386                 }
387                 // Reset UI visibility to ensure it matches real state.
388                 mHistoryOn.setVisibility(GONE);
389                 if (isChecked) {
390                     mHistoryEmpty.setVisibility(VISIBLE);
391                     mHistoryOff.setVisibility(GONE);
392                 } else {
393                     mHistoryOff.setVisibility(VISIBLE);
394                     mHistoryEmpty.setVisibility(GONE);
395                 }
396                 mTodayView.removeAllViews();
397             };
398 
399     private final NotificationListenerService mListener = new NotificationListenerService() {
400         private RecyclerView mDismissedRv;
401         private RecyclerView mSnoozedRv;
402 
403         @Override
404         public void onListenerConnected() {
405             StatusBarNotification[] snoozed = null;
406             StatusBarNotification[] dismissed = null;
407             try {
408                 snoozed = getSnoozedNotifications();
409                 dismissed = mNm.getHistoricalNotificationsWithAttribution(
410                         NotificationHistoryActivity.this.getPackageName(),
411                         NotificationHistoryActivity.this.getAttributionTag(),
412                         MAX_RECENT_DISMISS_ITEM_COUNT, false);
413             } catch (SecurityException | RemoteException e) {
414                 Log.d(TAG, "OnPaused called while trying to retrieve notifications");
415             }
416 
417             mSnoozedRv = mSnoozeView.findViewById(R.id.notification_list);
418             LinearLayoutManager lm = new LinearLayoutManager(NotificationHistoryActivity.this);
419             mSnoozedRv.setLayoutManager(lm);
420             mSnoozedRv.setAdapter(
421                     new NotificationSbnAdapter(NotificationHistoryActivity.this, mPm, mUm,
422                             true, mUiEventLogger));
423             mSnoozedRv.setNestedScrollingEnabled(false);
424 
425             if (snoozed == null || snoozed.length == 0) {
426                 mSnoozeView.setVisibility(GONE);
427             } else {
428                 ((NotificationSbnAdapter) mSnoozedRv.getAdapter()).onRebuildComplete(
429                         new ArrayList<>(Arrays.asList(snoozed)));
430             }
431 
432             mDismissedRv = mDismissView.findViewById(R.id.notification_list);
433             LinearLayoutManager dismissLm =
434                     new LinearLayoutManager(NotificationHistoryActivity.this);
435             mDismissedRv.setLayoutManager(dismissLm);
436             mDismissedRv.setAdapter(
437                     new NotificationSbnAdapter(NotificationHistoryActivity.this, mPm, mUm,
438                             false, mUiEventLogger));
439             mDismissedRv.setNestedScrollingEnabled(false);
440 
441             if (dismissed == null || dismissed.length == 0) {
442                 mDismissView.setVisibility(GONE);
443             } else {
444                 mDismissView.setVisibility(VISIBLE);
445                 ((NotificationSbnAdapter) mDismissedRv.getAdapter()).onRebuildComplete(
446                         new ArrayList<>(Arrays.asList(dismissed)));
447             }
448 
449             mCountdownLatch.countDown();
450         }
451 
452         @Override
453         public void onNotificationPosted(StatusBarNotification sbn) {
454             // making lint happy
455         }
456 
457         @Override
458         public void onNotificationRemoved(StatusBarNotification sbn, RankingMap rankingMap,
459                 int reason) {
460             if (reason == REASON_SNOOZED) {
461                 ((NotificationSbnAdapter) mSnoozedRv.getAdapter()).addSbn(sbn);
462                 mSnoozeView.setVisibility(VISIBLE);
463             } else {
464                 ((NotificationSbnAdapter) mDismissedRv.getAdapter()).addSbn(sbn);
465                 mDismissView.setVisibility(VISIBLE);
466             }
467         }
468     };
469 }
470