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