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 104 @Inject NotificationGutsManager( Context context, VisualStabilityManager visualStabilityManager)105 public NotificationGutsManager( 106 Context context, 107 VisualStabilityManager visualStabilityManager) { 108 mContext = context; 109 mVisualStabilityManager = visualStabilityManager; 110 mAccessibilityManager = (AccessibilityManager) 111 mContext.getSystemService(Context.ACCESSIBILITY_SERVICE); 112 } 113 setUpWithPresenter(NotificationPresenter presenter, NotificationListContainer listContainer, CheckSaveListener checkSave, OnSettingsClickListener onSettingsClick)114 public void setUpWithPresenter(NotificationPresenter presenter, 115 NotificationListContainer listContainer, 116 CheckSaveListener checkSave, OnSettingsClickListener onSettingsClick) { 117 mPresenter = presenter; 118 mListContainer = listContainer; 119 mCheckSaveListener = checkSave; 120 mOnSettingsClickListener = onSettingsClick; 121 mStatusBar = SysUiServiceProvider.getComponent(mContext, StatusBar.class); 122 } 123 setNotificationActivityStarter( NotificationActivityStarter notificationActivityStarter)124 public void setNotificationActivityStarter( 125 NotificationActivityStarter notificationActivityStarter) { 126 mNotificationActivityStarter = notificationActivityStarter; 127 } 128 onDensityOrFontScaleChanged(NotificationEntry entry)129 public void onDensityOrFontScaleChanged(NotificationEntry entry) { 130 setExposedGuts(entry.getGuts()); 131 bindGuts(entry.getRow()); 132 } 133 134 /** 135 * Sends an intent to open the notification settings for a particular package and optional 136 * channel. 137 */ 138 public static final String EXTRA_SHOW_FRAGMENT_ARGUMENTS = ":settings:show_fragment_args"; startAppNotificationSettingsActivity(String packageName, final int appUid, final NotificationChannel channel, ExpandableNotificationRow row)139 private void startAppNotificationSettingsActivity(String packageName, final int appUid, 140 final NotificationChannel channel, ExpandableNotificationRow row) { 141 final Intent intent = new Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS); 142 intent.putExtra(Settings.EXTRA_APP_PACKAGE, packageName); 143 intent.putExtra(Settings.EXTRA_APP_UID, appUid); 144 145 if (channel != null) { 146 final Bundle args = new Bundle(); 147 intent.putExtra(EXTRA_FRAGMENT_ARG_KEY, channel.getId()); 148 args.putString(EXTRA_FRAGMENT_ARG_KEY, channel.getId()); 149 intent.putExtra(EXTRA_SHOW_FRAGMENT_ARGUMENTS, args); 150 } 151 mNotificationActivityStarter.startNotificationGutsIntent(intent, appUid, row); 152 } 153 startAppDetailsSettingsActivity(String packageName, final int appUid, final NotificationChannel channel, ExpandableNotificationRow row)154 private void startAppDetailsSettingsActivity(String packageName, final int appUid, 155 final NotificationChannel channel, ExpandableNotificationRow row) { 156 final Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); 157 intent.setData(Uri.fromParts("package", packageName, null)); 158 intent.putExtra(Settings.EXTRA_APP_PACKAGE, packageName); 159 intent.putExtra(Settings.EXTRA_APP_UID, appUid); 160 if (channel != null) { 161 intent.putExtra(EXTRA_FRAGMENT_ARG_KEY, channel.getId()); 162 } 163 mNotificationActivityStarter.startNotificationGutsIntent(intent, appUid, row); 164 } 165 startAppOpsSettingsActivity(String pkg, int uid, ArraySet<Integer> ops, ExpandableNotificationRow row)166 protected void startAppOpsSettingsActivity(String pkg, int uid, ArraySet<Integer> ops, 167 ExpandableNotificationRow row) { 168 if (ops.contains(OP_SYSTEM_ALERT_WINDOW)) { 169 if (ops.contains(OP_CAMERA) || ops.contains(OP_RECORD_AUDIO)) { 170 startAppDetailsSettingsActivity(pkg, uid, null, row); 171 } else { 172 Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION); 173 intent.setData(Uri.fromParts("package", pkg, null)); 174 mNotificationActivityStarter.startNotificationGutsIntent(intent, uid, row); 175 } 176 } else if (ops.contains(OP_CAMERA) || ops.contains(OP_RECORD_AUDIO)) { 177 Intent intent = new Intent(Intent.ACTION_MANAGE_APP_PERMISSIONS); 178 intent.putExtra(Intent.EXTRA_PACKAGE_NAME, pkg); 179 mNotificationActivityStarter.startNotificationGutsIntent(intent, uid, row); 180 } 181 } 182 bindGuts(final ExpandableNotificationRow row)183 private boolean bindGuts(final ExpandableNotificationRow row) { 184 row.ensureGutsInflated(); 185 return bindGuts(row, mGutsMenuItem); 186 } 187 188 @VisibleForTesting bindGuts(final ExpandableNotificationRow row, NotificationMenuRowPlugin.MenuItem item)189 protected boolean bindGuts(final ExpandableNotificationRow row, 190 NotificationMenuRowPlugin.MenuItem item) { 191 StatusBarNotification sbn = row.getStatusBarNotification(); 192 193 row.setGutsView(item); 194 row.setTag(sbn.getPackageName()); 195 row.getGuts().setClosedListener((NotificationGuts g) -> { 196 row.onGutsClosed(); 197 if (!g.willBeRemoved() && !row.isRemoved()) { 198 mListContainer.onHeightChanged( 199 row, !mPresenter.isPresenterFullyCollapsed() /* needsAnimation */); 200 } 201 if (mNotificationGutsExposed == g) { 202 mNotificationGutsExposed = null; 203 mGutsMenuItem = null; 204 } 205 String key = sbn.getKey(); 206 if (key.equals(mKeyToRemoveOnGutsClosed)) { 207 mKeyToRemoveOnGutsClosed = null; 208 if (mNotificationLifetimeFinishedCallback != null) { 209 mNotificationLifetimeFinishedCallback.onSafeToRemove(key); 210 } 211 } 212 }); 213 214 View gutsView = item.getGutsView(); 215 try { 216 if (gutsView instanceof NotificationSnooze) { 217 initializeSnoozeView(row, (NotificationSnooze) gutsView); 218 } else if (gutsView instanceof AppOpsInfo) { 219 initializeAppOpsInfo(row, (AppOpsInfo) gutsView); 220 } else if (gutsView instanceof NotificationInfo) { 221 initializeNotificationInfo(row, (NotificationInfo) gutsView); 222 } 223 return true; 224 } catch (Exception e) { 225 Log.e(TAG, "error binding guts", e); 226 return false; 227 } 228 } 229 230 /** 231 * Sets up the {@link NotificationSnooze} inside the notification row's guts. 232 * 233 * @param row view to set up the guts for 234 * @param notificationSnoozeView view to set up/bind within {@code row} 235 */ initializeSnoozeView( final ExpandableNotificationRow row, NotificationSnooze notificationSnoozeView)236 private void initializeSnoozeView( 237 final ExpandableNotificationRow row, 238 NotificationSnooze notificationSnoozeView) { 239 NotificationGuts guts = row.getGuts(); 240 StatusBarNotification sbn = row.getStatusBarNotification(); 241 242 notificationSnoozeView.setSnoozeListener(mListContainer.getSwipeActionHelper()); 243 notificationSnoozeView.setStatusBarNotification(sbn); 244 notificationSnoozeView.setSnoozeOptions(row.getEntry().snoozeCriteria); 245 guts.setHeightChangedListener((NotificationGuts g) -> { 246 mListContainer.onHeightChanged(row, row.isShown() /* needsAnimation */); 247 }); 248 } 249 250 /** 251 * Sets up the {@link AppOpsInfo} inside the notification row's guts. 252 * 253 * @param row view to set up the guts for 254 * @param appOpsInfoView view to set up/bind within {@code row} 255 */ initializeAppOpsInfo( final ExpandableNotificationRow row, AppOpsInfo appOpsInfoView)256 private void initializeAppOpsInfo( 257 final ExpandableNotificationRow row, 258 AppOpsInfo appOpsInfoView) { 259 NotificationGuts guts = row.getGuts(); 260 StatusBarNotification sbn = row.getStatusBarNotification(); 261 UserHandle userHandle = sbn.getUser(); 262 PackageManager pmUser = StatusBar.getPackageManagerForUser(mContext, 263 userHandle.getIdentifier()); 264 265 AppOpsInfo.OnSettingsClickListener onSettingsClick = 266 (View v, String pkg, int uid, ArraySet<Integer> ops) -> { 267 mMetricsLogger.action(MetricsProto.MetricsEvent.ACTION_OPS_GUTS_SETTINGS); 268 guts.resetFalsingCheck(); 269 startAppOpsSettingsActivity(pkg, uid, ops, row); 270 }; 271 if (!row.getEntry().mActiveAppOps.isEmpty()) { 272 appOpsInfoView.bindGuts(pmUser, onSettingsClick, sbn, row.getEntry().mActiveAppOps); 273 } 274 } 275 276 /** 277 * Sets up the {@link NotificationInfo} inside the notification row's guts. 278 * @param row view to set up the guts for 279 * @param notificationInfoView view to set up/bind within {@code row} 280 */ 281 @VisibleForTesting initializeNotificationInfo( final ExpandableNotificationRow row, NotificationInfo notificationInfoView)282 void initializeNotificationInfo( 283 final ExpandableNotificationRow row, 284 NotificationInfo notificationInfoView) throws Exception { 285 NotificationGuts guts = row.getGuts(); 286 StatusBarNotification sbn = row.getStatusBarNotification(); 287 String packageName = sbn.getPackageName(); 288 // Settings link is only valid for notifications that specify a non-system user 289 NotificationInfo.OnSettingsClickListener onSettingsClick = null; 290 UserHandle userHandle = sbn.getUser(); 291 PackageManager pmUser = StatusBar.getPackageManagerForUser( 292 mContext, userHandle.getIdentifier()); 293 INotificationManager iNotificationManager = INotificationManager.Stub.asInterface( 294 ServiceManager.getService(Context.NOTIFICATION_SERVICE)); 295 final NotificationInfo.OnAppSettingsClickListener onAppSettingsClick = 296 (View v, Intent intent) -> { 297 mMetricsLogger.action(MetricsProto.MetricsEvent.ACTION_APP_NOTE_SETTINGS); 298 guts.resetFalsingCheck(); 299 mNotificationActivityStarter.startNotificationGutsIntent(intent, sbn.getUid(), 300 row); 301 }; 302 boolean isForBlockingHelper = row.isBlockingHelperShowing(); 303 304 if (!userHandle.equals(UserHandle.ALL) 305 || mLockscreenUserManager.getCurrentUserId() == UserHandle.USER_SYSTEM) { 306 onSettingsClick = (View v, NotificationChannel channel, int appUid) -> { 307 mMetricsLogger.action(MetricsProto.MetricsEvent.ACTION_NOTE_INFO); 308 guts.resetFalsingCheck(); 309 mOnSettingsClickListener.onSettingsClick(sbn.getKey()); 310 startAppNotificationSettingsActivity(packageName, appUid, channel, row); 311 }; 312 } 313 314 notificationInfoView.bindNotification( 315 pmUser, 316 iNotificationManager, 317 mVisualStabilityManager, 318 packageName, 319 row.getEntry().channel, 320 row.getUniqueChannels(), 321 sbn, 322 mCheckSaveListener, 323 onSettingsClick, 324 onAppSettingsClick, 325 mDeviceProvisionedController.isDeviceProvisioned(), 326 row.getIsNonblockable(), 327 isForBlockingHelper, 328 row.getEntry().importance, 329 row.getEntry().isHighPriority()); 330 331 } 332 333 /** 334 * Closes guts or notification menus that might be visible and saves any changes. 335 * 336 * @param removeLeavebehinds true if leavebehinds (e.g. snooze) should be closed. 337 * @param force true if guts should be closed regardless of state (used for snooze only). 338 * @param removeControls true if controls (e.g. info) should be closed. 339 * @param x if closed based on touch location, this is the x touch location. 340 * @param y if closed based on touch location, this is the y touch location. 341 * @param resetMenu if any notification menus that might be revealed should be closed. 342 */ closeAndSaveGuts(boolean removeLeavebehinds, boolean force, boolean removeControls, int x, int y, boolean resetMenu)343 public void closeAndSaveGuts(boolean removeLeavebehinds, boolean force, boolean removeControls, 344 int x, int y, boolean resetMenu) { 345 if (mNotificationGutsExposed != null) { 346 mNotificationGutsExposed.closeControls(removeLeavebehinds, removeControls, x, y, force); 347 } 348 if (resetMenu) { 349 mListContainer.resetExposedMenuView(false /* animate */, true /* force */); 350 } 351 } 352 353 /** 354 * Returns the exposed NotificationGuts or null if none are exposed. 355 */ getExposedGuts()356 public NotificationGuts getExposedGuts() { 357 return mNotificationGutsExposed; 358 } 359 setExposedGuts(NotificationGuts guts)360 public void setExposedGuts(NotificationGuts guts) { 361 mNotificationGutsExposed = guts; 362 } 363 getNotificationLongClicker()364 public ExpandableNotificationRow.LongPressListener getNotificationLongClicker() { 365 return this::openGuts; 366 } 367 368 /** 369 * Opens guts on the given ExpandableNotificationRow {@code view}. This handles opening guts for 370 * the normal half-swipe and long-press use cases via a circular reveal. When the blocking 371 * helper needs to be shown on the row, this will skip the circular reveal. 372 * 373 * @param view ExpandableNotificationRow to open guts on 374 * @param x x coordinate of origin of circular reveal 375 * @param y y coordinate of origin of circular reveal 376 * @param menuItem MenuItem the guts should display 377 * @return true if guts was opened 378 */ openGuts( View view, int x, int y, NotificationMenuRowPlugin.MenuItem menuItem)379 public boolean openGuts( 380 View view, 381 int x, 382 int y, 383 NotificationMenuRowPlugin.MenuItem menuItem) { 384 if (menuItem.getGutsView() instanceof NotificationInfo) { 385 if (mStatusBarStateController instanceof StatusBarStateControllerImpl) { 386 ((StatusBarStateControllerImpl) mStatusBarStateController) 387 .setLeaveOpenOnKeyguardHide(true); 388 } 389 390 Runnable r = () -> Dependency.get(Dependency.MAIN_HANDLER).post( 391 () -> openGutsInternal(view, x, y, menuItem)); 392 393 mStatusBar.executeRunnableDismissingKeyguard( 394 r, 395 null /* cancelAction */, 396 false /* dismissShade */, 397 true /* afterKeyguardGone */, 398 true /* deferred */); 399 400 return true; 401 } 402 return openGutsInternal(view, x, y, menuItem); 403 } 404 405 @VisibleForTesting openGutsInternal( View view, int x, int y, NotificationMenuRowPlugin.MenuItem menuItem)406 boolean openGutsInternal( 407 View view, 408 int x, 409 int y, 410 NotificationMenuRowPlugin.MenuItem menuItem) { 411 412 if (!(view instanceof ExpandableNotificationRow)) { 413 return false; 414 } 415 416 if (view.getWindowToken() == null) { 417 Log.e(TAG, "Trying to show notification guts, but not attached to window"); 418 return false; 419 } 420 421 final ExpandableNotificationRow row = (ExpandableNotificationRow) view; 422 if (row.isDark()) { 423 return false; 424 } 425 view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); 426 if (row.areGutsExposed()) { 427 closeAndSaveGuts(false /* removeLeavebehind */, false /* force */, 428 true /* removeControls */, -1 /* x */, -1 /* y */, 429 true /* resetMenu */); 430 return false; 431 } 432 433 row.ensureGutsInflated(); 434 NotificationGuts guts = row.getGuts(); 435 mNotificationGutsExposed = guts; 436 if (!bindGuts(row, menuItem)) { 437 // exception occurred trying to fill in all the data, bail. 438 return false; 439 } 440 441 442 // Assume we are a status_bar_notification_row 443 if (guts == null) { 444 // This view has no guts. Examples are the more card or the dismiss all view 445 return false; 446 } 447 448 // ensure that it's laid but not visible until actually laid out 449 guts.setVisibility(View.INVISIBLE); 450 // Post to ensure the the guts are properly laid out. 451 guts.post(new Runnable() { 452 @Override 453 public void run() { 454 if (row.getWindowToken() == null) { 455 Log.e(TAG, "Trying to show notification guts in post(), but not attached to " 456 + "window"); 457 return; 458 } 459 guts.setVisibility(View.VISIBLE); 460 461 final boolean needsFalsingProtection = 462 (mStatusBarStateController.getState() == StatusBarState.KEYGUARD && 463 !mAccessibilityManager.isTouchExplorationEnabled()); 464 465 guts.openControls( 466 !row.isBlockingHelperShowing(), 467 x, 468 y, 469 needsFalsingProtection, 470 row::onGutsOpened); 471 472 row.closeRemoteInput(); 473 mListContainer.onHeightChanged(row, true /* needsAnimation */); 474 mGutsMenuItem = menuItem; 475 } 476 }); 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