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