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