1 /* 2 * Copyright (C) 2017 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 package com.android.systemui.statusbar.notification.row; 17 18 import static android.app.AppOpsManager.OP_CAMERA; 19 import static android.app.AppOpsManager.OP_RECORD_AUDIO; 20 import static android.app.AppOpsManager.OP_SYSTEM_ALERT_WINDOW; 21 22 import android.app.INotificationManager; 23 import android.app.NotificationChannel; 24 import android.content.Context; 25 import android.content.Intent; 26 import android.content.pm.PackageManager; 27 import android.net.Uri; 28 import android.os.Bundle; 29 import android.os.ServiceManager; 30 import android.os.UserHandle; 31 import android.provider.Settings; 32 import android.service.notification.StatusBarNotification; 33 import android.util.ArraySet; 34 import android.util.Log; 35 import android.view.HapticFeedbackConstants; 36 import android.view.View; 37 import android.view.accessibility.AccessibilityManager; 38 39 import com.android.internal.annotations.VisibleForTesting; 40 import com.android.internal.logging.MetricsLogger; 41 import com.android.internal.logging.nano.MetricsProto; 42 import com.android.systemui.Dependency; 43 import com.android.systemui.Dumpable; 44 import com.android.systemui.SysUiServiceProvider; 45 import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin; 46 import com.android.systemui.plugins.statusbar.StatusBarStateController; 47 import com.android.systemui.statusbar.NotificationLifetimeExtender; 48 import com.android.systemui.statusbar.NotificationLockscreenUserManager; 49 import com.android.systemui.statusbar.NotificationPresenter; 50 import com.android.systemui.statusbar.StatusBarState; 51 import com.android.systemui.statusbar.StatusBarStateControllerImpl; 52 import com.android.systemui.statusbar.notification.NotificationActivityStarter; 53 import com.android.systemui.statusbar.notification.VisualStabilityManager; 54 import com.android.systemui.statusbar.notification.collection.NotificationEntry; 55 import com.android.systemui.statusbar.notification.row.NotificationInfo.CheckSaveListener; 56 import com.android.systemui.statusbar.notification.stack.NotificationListContainer; 57 import com.android.systemui.statusbar.phone.StatusBar; 58 import com.android.systemui.statusbar.policy.DeviceProvisionedController; 59 60 import java.io.FileDescriptor; 61 import java.io.PrintWriter; 62 63 import javax.inject.Inject; 64 import javax.inject.Singleton; 65 66 /** 67 * Handles various NotificationGuts related tasks, such as binding guts to a row, opening and 68 * closing guts, and keeping track of the currently exposed notification guts. 69 */ 70 @Singleton 71 public class NotificationGutsManager implements Dumpable, NotificationLifetimeExtender { 72 private static final String TAG = "NotificationGutsManager"; 73 74 // Must match constant in Settings. Used to highlight preferences when linking to Settings. 75 private static final String EXTRA_FRAGMENT_ARG_KEY = ":settings:fragment_args_key"; 76 77 private final MetricsLogger mMetricsLogger = Dependency.get(MetricsLogger.class); 78 private final Context mContext; 79 private final VisualStabilityManager mVisualStabilityManager; 80 private final AccessibilityManager mAccessibilityManager; 81 82 // Dependencies: 83 private final NotificationLockscreenUserManager mLockscreenUserManager = 84 Dependency.get(NotificationLockscreenUserManager.class); 85 private final StatusBarStateController mStatusBarStateController = 86 Dependency.get(StatusBarStateController.class); 87 private final DeviceProvisionedController mDeviceProvisionedController = 88 Dependency.get(DeviceProvisionedController.class); 89 90 // which notification is currently being longpress-examined by the user 91 private NotificationGuts mNotificationGutsExposed; 92 private NotificationMenuRowPlugin.MenuItem mGutsMenuItem; 93 private NotificationSafeToRemoveCallback mNotificationLifetimeFinishedCallback; 94 private NotificationPresenter mPresenter; 95 private NotificationActivityStarter mNotificationActivityStarter; 96 private NotificationListContainer mListContainer; 97 private CheckSaveListener mCheckSaveListener; 98 private OnSettingsClickListener mOnSettingsClickListener; 99 @VisibleForTesting 100 protected String mKeyToRemoveOnGutsClosed; 101 102 private StatusBar mStatusBar; 103 private Runnable mOpenRunnable; 104 105 @Inject NotificationGutsManager( Context context, VisualStabilityManager visualStabilityManager)106 public NotificationGutsManager( 107 Context context, 108 VisualStabilityManager visualStabilityManager) { 109 mContext = context; 110 mVisualStabilityManager = visualStabilityManager; 111 mAccessibilityManager = (AccessibilityManager) 112 mContext.getSystemService(Context.ACCESSIBILITY_SERVICE); 113 } 114 setUpWithPresenter(NotificationPresenter presenter, NotificationListContainer listContainer, CheckSaveListener checkSave, OnSettingsClickListener onSettingsClick)115 public void setUpWithPresenter(NotificationPresenter presenter, 116 NotificationListContainer listContainer, 117 CheckSaveListener checkSave, OnSettingsClickListener onSettingsClick) { 118 mPresenter = presenter; 119 mListContainer = listContainer; 120 mCheckSaveListener = checkSave; 121 mOnSettingsClickListener = onSettingsClick; 122 mStatusBar = SysUiServiceProvider.getComponent(mContext, StatusBar.class); 123 } 124 setNotificationActivityStarter( NotificationActivityStarter notificationActivityStarter)125 public void setNotificationActivityStarter( 126 NotificationActivityStarter notificationActivityStarter) { 127 mNotificationActivityStarter = notificationActivityStarter; 128 } 129 onDensityOrFontScaleChanged(NotificationEntry entry)130 public void onDensityOrFontScaleChanged(NotificationEntry entry) { 131 setExposedGuts(entry.getGuts()); 132 bindGuts(entry.getRow()); 133 } 134 135 /** 136 * Sends an intent to open the notification settings for a particular package and optional 137 * channel. 138 */ 139 public static final String EXTRA_SHOW_FRAGMENT_ARGUMENTS = ":settings:show_fragment_args"; startAppNotificationSettingsActivity(String packageName, final int appUid, final NotificationChannel channel, ExpandableNotificationRow row)140 private void startAppNotificationSettingsActivity(String packageName, final int appUid, 141 final NotificationChannel channel, ExpandableNotificationRow row) { 142 final Intent intent = new Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS); 143 intent.putExtra(Settings.EXTRA_APP_PACKAGE, packageName); 144 intent.putExtra(Settings.EXTRA_APP_UID, appUid); 145 146 if (channel != null) { 147 final Bundle args = new Bundle(); 148 intent.putExtra(EXTRA_FRAGMENT_ARG_KEY, channel.getId()); 149 args.putString(EXTRA_FRAGMENT_ARG_KEY, channel.getId()); 150 intent.putExtra(EXTRA_SHOW_FRAGMENT_ARGUMENTS, args); 151 } 152 mNotificationActivityStarter.startNotificationGutsIntent(intent, appUid, row); 153 } 154 startAppDetailsSettingsActivity(String packageName, final int appUid, final NotificationChannel channel, ExpandableNotificationRow row)155 private void startAppDetailsSettingsActivity(String packageName, final int appUid, 156 final NotificationChannel channel, ExpandableNotificationRow row) { 157 final Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); 158 intent.setData(Uri.fromParts("package", packageName, null)); 159 intent.putExtra(Settings.EXTRA_APP_PACKAGE, packageName); 160 intent.putExtra(Settings.EXTRA_APP_UID, appUid); 161 if (channel != null) { 162 intent.putExtra(EXTRA_FRAGMENT_ARG_KEY, channel.getId()); 163 } 164 mNotificationActivityStarter.startNotificationGutsIntent(intent, appUid, row); 165 } 166 startAppOpsSettingsActivity(String pkg, int uid, ArraySet<Integer> ops, ExpandableNotificationRow row)167 protected void startAppOpsSettingsActivity(String pkg, int uid, ArraySet<Integer> ops, 168 ExpandableNotificationRow row) { 169 if (ops.contains(OP_SYSTEM_ALERT_WINDOW)) { 170 if (ops.contains(OP_CAMERA) || ops.contains(OP_RECORD_AUDIO)) { 171 startAppDetailsSettingsActivity(pkg, uid, null, row); 172 } else { 173 Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION); 174 intent.setData(Uri.fromParts("package", pkg, null)); 175 mNotificationActivityStarter.startNotificationGutsIntent(intent, uid, row); 176 } 177 } else if (ops.contains(OP_CAMERA) || ops.contains(OP_RECORD_AUDIO)) { 178 Intent intent = new Intent(Intent.ACTION_MANAGE_APP_PERMISSIONS); 179 intent.putExtra(Intent.EXTRA_PACKAGE_NAME, pkg); 180 mNotificationActivityStarter.startNotificationGutsIntent(intent, uid, row); 181 } 182 } 183 bindGuts(final ExpandableNotificationRow row)184 private boolean bindGuts(final ExpandableNotificationRow row) { 185 row.ensureGutsInflated(); 186 return bindGuts(row, mGutsMenuItem); 187 } 188 189 @VisibleForTesting bindGuts(final ExpandableNotificationRow row, NotificationMenuRowPlugin.MenuItem item)190 protected boolean bindGuts(final ExpandableNotificationRow row, 191 NotificationMenuRowPlugin.MenuItem item) { 192 StatusBarNotification sbn = row.getStatusBarNotification(); 193 194 row.setGutsView(item); 195 row.setTag(sbn.getPackageName()); 196 row.getGuts().setClosedListener((NotificationGuts g) -> { 197 row.onGutsClosed(); 198 if (!g.willBeRemoved() && !row.isRemoved()) { 199 mListContainer.onHeightChanged( 200 row, !mPresenter.isPresenterFullyCollapsed() /* needsAnimation */); 201 } 202 if (mNotificationGutsExposed == g) { 203 mNotificationGutsExposed = null; 204 mGutsMenuItem = null; 205 } 206 String key = sbn.getKey(); 207 if (key.equals(mKeyToRemoveOnGutsClosed)) { 208 mKeyToRemoveOnGutsClosed = null; 209 if (mNotificationLifetimeFinishedCallback != null) { 210 mNotificationLifetimeFinishedCallback.onSafeToRemove(key); 211 } 212 } 213 }); 214 215 View gutsView = item.getGutsView(); 216 try { 217 if (gutsView instanceof NotificationSnooze) { 218 initializeSnoozeView(row, (NotificationSnooze) gutsView); 219 } else if (gutsView instanceof AppOpsInfo) { 220 initializeAppOpsInfo(row, (AppOpsInfo) gutsView); 221 } else if (gutsView instanceof NotificationInfo) { 222 initializeNotificationInfo(row, (NotificationInfo) gutsView); 223 } 224 return true; 225 } catch (Exception e) { 226 Log.e(TAG, "error binding guts", e); 227 return false; 228 } 229 } 230 231 /** 232 * Sets up the {@link NotificationSnooze} inside the notification row's guts. 233 * 234 * @param row view to set up the guts for 235 * @param notificationSnoozeView view to set up/bind within {@code row} 236 */ initializeSnoozeView( final ExpandableNotificationRow row, NotificationSnooze notificationSnoozeView)237 private void initializeSnoozeView( 238 final ExpandableNotificationRow row, 239 NotificationSnooze notificationSnoozeView) { 240 NotificationGuts guts = row.getGuts(); 241 StatusBarNotification sbn = row.getStatusBarNotification(); 242 243 notificationSnoozeView.setSnoozeListener(mListContainer.getSwipeActionHelper()); 244 notificationSnoozeView.setStatusBarNotification(sbn); 245 notificationSnoozeView.setSnoozeOptions(row.getEntry().snoozeCriteria); 246 guts.setHeightChangedListener((NotificationGuts g) -> { 247 mListContainer.onHeightChanged(row, row.isShown() /* needsAnimation */); 248 }); 249 } 250 251 /** 252 * Sets up the {@link AppOpsInfo} inside the notification row's guts. 253 * 254 * @param row view to set up the guts for 255 * @param appOpsInfoView view to set up/bind within {@code row} 256 */ initializeAppOpsInfo( final ExpandableNotificationRow row, AppOpsInfo appOpsInfoView)257 private void initializeAppOpsInfo( 258 final ExpandableNotificationRow row, 259 AppOpsInfo appOpsInfoView) { 260 NotificationGuts guts = row.getGuts(); 261 StatusBarNotification sbn = row.getStatusBarNotification(); 262 UserHandle userHandle = sbn.getUser(); 263 PackageManager pmUser = StatusBar.getPackageManagerForUser(mContext, 264 userHandle.getIdentifier()); 265 266 AppOpsInfo.OnSettingsClickListener onSettingsClick = 267 (View v, String pkg, int uid, ArraySet<Integer> ops) -> { 268 mMetricsLogger.action(MetricsProto.MetricsEvent.ACTION_OPS_GUTS_SETTINGS); 269 guts.resetFalsingCheck(); 270 startAppOpsSettingsActivity(pkg, uid, ops, row); 271 }; 272 if (!row.getEntry().mActiveAppOps.isEmpty()) { 273 appOpsInfoView.bindGuts(pmUser, onSettingsClick, sbn, row.getEntry().mActiveAppOps); 274 } 275 } 276 277 /** 278 * Sets up the {@link NotificationInfo} inside the notification row's guts. 279 * @param row view to set up the guts for 280 * @param notificationInfoView view to set up/bind within {@code row} 281 */ 282 @VisibleForTesting initializeNotificationInfo( final ExpandableNotificationRow row, NotificationInfo notificationInfoView)283 void initializeNotificationInfo( 284 final ExpandableNotificationRow row, 285 NotificationInfo notificationInfoView) throws Exception { 286 NotificationGuts guts = row.getGuts(); 287 StatusBarNotification sbn = row.getStatusBarNotification(); 288 String packageName = sbn.getPackageName(); 289 // Settings link is only valid for notifications that specify a non-system user 290 NotificationInfo.OnSettingsClickListener onSettingsClick = null; 291 UserHandle userHandle = sbn.getUser(); 292 PackageManager pmUser = StatusBar.getPackageManagerForUser( 293 mContext, userHandle.getIdentifier()); 294 INotificationManager iNotificationManager = INotificationManager.Stub.asInterface( 295 ServiceManager.getService(Context.NOTIFICATION_SERVICE)); 296 final NotificationInfo.OnAppSettingsClickListener onAppSettingsClick = 297 (View v, Intent intent) -> { 298 mMetricsLogger.action(MetricsProto.MetricsEvent.ACTION_APP_NOTE_SETTINGS); 299 guts.resetFalsingCheck(); 300 mNotificationActivityStarter.startNotificationGutsIntent(intent, sbn.getUid(), 301 row); 302 }; 303 boolean isForBlockingHelper = row.isBlockingHelperShowing(); 304 305 if (!userHandle.equals(UserHandle.ALL) 306 || mLockscreenUserManager.getCurrentUserId() == UserHandle.USER_SYSTEM) { 307 onSettingsClick = (View v, NotificationChannel channel, int appUid) -> { 308 mMetricsLogger.action(MetricsProto.MetricsEvent.ACTION_NOTE_INFO); 309 guts.resetFalsingCheck(); 310 mOnSettingsClickListener.onSettingsClick(sbn.getKey()); 311 startAppNotificationSettingsActivity(packageName, appUid, channel, row); 312 }; 313 } 314 315 notificationInfoView.bindNotification( 316 pmUser, 317 iNotificationManager, 318 mVisualStabilityManager, 319 packageName, 320 row.getEntry().channel, 321 row.getUniqueChannels(), 322 sbn, 323 mCheckSaveListener, 324 onSettingsClick, 325 onAppSettingsClick, 326 mDeviceProvisionedController.isDeviceProvisioned(), 327 row.getIsNonblockable(), 328 isForBlockingHelper, 329 row.getEntry().importance, 330 row.getEntry().isHighPriority()); 331 332 } 333 334 /** 335 * Closes guts or notification menus that might be visible and saves any changes. 336 * 337 * @param removeLeavebehinds true if leavebehinds (e.g. snooze) should be closed. 338 * @param force true if guts should be closed regardless of state (used for snooze only). 339 * @param removeControls true if controls (e.g. info) should be closed. 340 * @param x if closed based on touch location, this is the x touch location. 341 * @param y if closed based on touch location, this is the y touch location. 342 * @param resetMenu if any notification menus that might be revealed should be closed. 343 */ closeAndSaveGuts(boolean removeLeavebehinds, boolean force, boolean removeControls, int x, int y, boolean resetMenu)344 public void closeAndSaveGuts(boolean removeLeavebehinds, boolean force, boolean removeControls, 345 int x, int y, boolean resetMenu) { 346 if (mNotificationGutsExposed != null) { 347 mNotificationGutsExposed.removeCallbacks(mOpenRunnable); 348 mNotificationGutsExposed.closeControls(removeLeavebehinds, removeControls, x, y, force); 349 } 350 if (resetMenu) { 351 mListContainer.resetExposedMenuView(false /* animate */, true /* force */); 352 } 353 } 354 355 /** 356 * Returns the exposed NotificationGuts or null if none are exposed. 357 */ getExposedGuts()358 public NotificationGuts getExposedGuts() { 359 return mNotificationGutsExposed; 360 } 361 setExposedGuts(NotificationGuts guts)362 public void setExposedGuts(NotificationGuts guts) { 363 mNotificationGutsExposed = guts; 364 } 365 getNotificationLongClicker()366 public ExpandableNotificationRow.LongPressListener getNotificationLongClicker() { 367 return this::openGuts; 368 } 369 370 /** 371 * Opens guts on the given ExpandableNotificationRow {@code view}. This handles opening guts for 372 * the normal half-swipe and long-press use cases via a circular reveal. When the blocking 373 * helper needs to be shown on the row, this will skip the circular reveal. 374 * 375 * @param view ExpandableNotificationRow to open guts on 376 * @param x x coordinate of origin of circular reveal 377 * @param y y coordinate of origin of circular reveal 378 * @param menuItem MenuItem the guts should display 379 * @return true if guts was opened 380 */ openGuts( View view, int x, int y, NotificationMenuRowPlugin.MenuItem menuItem)381 public boolean openGuts( 382 View view, 383 int x, 384 int y, 385 NotificationMenuRowPlugin.MenuItem menuItem) { 386 if (menuItem.getGutsView() instanceof NotificationInfo) { 387 if (mStatusBarStateController instanceof StatusBarStateControllerImpl) { 388 ((StatusBarStateControllerImpl) mStatusBarStateController) 389 .setLeaveOpenOnKeyguardHide(true); 390 } 391 392 Runnable r = () -> Dependency.get(Dependency.MAIN_HANDLER).post( 393 () -> openGutsInternal(view, x, y, menuItem)); 394 395 mStatusBar.executeRunnableDismissingKeyguard( 396 r, 397 null /* cancelAction */, 398 false /* dismissShade */, 399 true /* afterKeyguardGone */, 400 true /* deferred */); 401 402 return true; 403 } 404 return openGutsInternal(view, x, y, menuItem); 405 } 406 407 @VisibleForTesting openGutsInternal( View view, int x, int y, NotificationMenuRowPlugin.MenuItem menuItem)408 boolean openGutsInternal( 409 View view, 410 int x, 411 int y, 412 NotificationMenuRowPlugin.MenuItem menuItem) { 413 414 if (!(view instanceof ExpandableNotificationRow)) { 415 return false; 416 } 417 418 if (view.getWindowToken() == null) { 419 Log.e(TAG, "Trying to show notification guts, but not attached to window"); 420 return false; 421 } 422 423 final ExpandableNotificationRow row = (ExpandableNotificationRow) view; 424 view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); 425 if (row.areGutsExposed()) { 426 closeAndSaveGuts(false /* removeLeavebehind */, false /* force */, 427 true /* removeControls */, -1 /* x */, -1 /* y */, 428 true /* resetMenu */); 429 return false; 430 } 431 432 row.ensureGutsInflated(); 433 NotificationGuts guts = row.getGuts(); 434 mNotificationGutsExposed = guts; 435 if (!bindGuts(row, menuItem)) { 436 // exception occurred trying to fill in all the data, bail. 437 return false; 438 } 439 440 441 // Assume we are a status_bar_notification_row 442 if (guts == null) { 443 // This view has no guts. Examples are the more card or the dismiss all view 444 return false; 445 } 446 447 // ensure that it's laid but not visible until actually laid out 448 guts.setVisibility(View.INVISIBLE); 449 // Post to ensure the the guts are properly laid out. 450 mOpenRunnable = new Runnable() { 451 @Override 452 public void run() { 453 if (row.getWindowToken() == null) { 454 Log.e(TAG, "Trying to show notification guts in post(), but not attached to " 455 + "window"); 456 return; 457 } 458 guts.setVisibility(View.VISIBLE); 459 460 final boolean needsFalsingProtection = 461 (mStatusBarStateController.getState() == StatusBarState.KEYGUARD && 462 !mAccessibilityManager.isTouchExplorationEnabled()); 463 464 guts.openControls( 465 !row.isBlockingHelperShowing(), 466 x, 467 y, 468 needsFalsingProtection, 469 row::onGutsOpened); 470 471 row.closeRemoteInput(); 472 mListContainer.onHeightChanged(row, true /* needsAnimation */); 473 mGutsMenuItem = menuItem; 474 } 475 }; 476 guts.post(mOpenRunnable); 477 return true; 478 } 479 480 @Override setCallback(NotificationSafeToRemoveCallback callback)481 public void setCallback(NotificationSafeToRemoveCallback callback) { 482 mNotificationLifetimeFinishedCallback = callback; 483 } 484 485 @Override shouldExtendLifetime(NotificationEntry entry)486 public boolean shouldExtendLifetime(NotificationEntry entry) { 487 return entry != null 488 &&(mNotificationGutsExposed != null 489 && entry.getGuts() != null 490 && mNotificationGutsExposed == entry.getGuts() 491 && !mNotificationGutsExposed.isLeavebehind()); 492 } 493 494 @Override setShouldManageLifetime(NotificationEntry entry, boolean shouldExtend)495 public void setShouldManageLifetime(NotificationEntry entry, boolean shouldExtend) { 496 if (shouldExtend) { 497 mKeyToRemoveOnGutsClosed = entry.key; 498 if (Log.isLoggable(TAG, Log.DEBUG)) { 499 Log.d(TAG, "Keeping notification because it's showing guts. " + entry.key); 500 } 501 } else { 502 if (mKeyToRemoveOnGutsClosed != null && mKeyToRemoveOnGutsClosed.equals(entry.key)) { 503 mKeyToRemoveOnGutsClosed = null; 504 if (Log.isLoggable(TAG, Log.DEBUG)) { 505 Log.d(TAG, "Notification that was kept for guts was updated. " + entry.key); 506 } 507 } 508 } 509 } 510 511 @Override dump(FileDescriptor fd, PrintWriter pw, String[] args)512 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { 513 pw.println("NotificationGutsManager state:"); 514 pw.print(" mKeyToRemoveOnGutsClosed: "); 515 pw.println(mKeyToRemoveOnGutsClosed); 516 } 517 518 public interface OnSettingsClickListener { onSettingsClick(String key)519 public void onSettingsClick(String key); 520 } 521 } 522