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