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; 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 import static android.service.notification.NotificationListenerService.Ranking 22 .USER_SENTIMENT_NEGATIVE; 23 24 import android.app.INotificationManager; 25 import android.app.NotificationChannel; 26 import android.content.Context; 27 import android.content.Intent; 28 import android.content.pm.PackageManager; 29 import android.content.res.Resources; 30 import android.net.Uri; 31 import android.os.RemoteException; 32 import android.os.ServiceManager; 33 import android.os.UserHandle; 34 import android.provider.Settings; 35 import android.service.notification.StatusBarNotification; 36 import android.support.annotation.VisibleForTesting; 37 import android.util.ArraySet; 38 import android.util.Log; 39 import android.view.HapticFeedbackConstants; 40 import android.view.View; 41 import android.view.accessibility.AccessibilityManager; 42 43 import com.android.internal.logging.MetricsLogger; 44 import com.android.internal.logging.nano.MetricsProto; 45 import com.android.systemui.Dependency; 46 import com.android.systemui.Dumpable; 47 import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin; 48 import com.android.systemui.statusbar.phone.StatusBar; 49 50 import java.io.FileDescriptor; 51 import java.io.PrintWriter; 52 import java.util.Collections; 53 import java.util.HashSet; 54 import java.util.List; 55 import java.util.Set; 56 57 /** 58 * Handles various NotificationGuts related tasks, such as binding guts to a row, opening and 59 * closing guts, and keeping track of the currently exposed notification guts. 60 */ 61 public class NotificationGutsManager implements Dumpable { 62 private static final String TAG = "NotificationGutsManager"; 63 64 // Must match constant in Settings. Used to highlight preferences when linking to Settings. 65 private static final String EXTRA_FRAGMENT_ARG_KEY = ":settings:fragment_args_key"; 66 67 private final MetricsLogger mMetricsLogger = Dependency.get(MetricsLogger.class); 68 private final Context mContext; 69 private final AccessibilityManager mAccessibilityManager; 70 71 // Dependencies: 72 private final NotificationLockscreenUserManager mLockscreenUserManager = 73 Dependency.get(NotificationLockscreenUserManager.class); 74 75 // which notification is currently being longpress-examined by the user 76 private NotificationGuts mNotificationGutsExposed; 77 private NotificationMenuRowPlugin.MenuItem mGutsMenuItem; 78 protected NotificationPresenter mPresenter; 79 protected NotificationEntryManager mEntryManager; 80 private NotificationListContainer mListContainer; 81 private NotificationInfo.CheckSaveListener mCheckSaveListener; 82 private OnSettingsClickListener mOnSettingsClickListener; 83 private String mKeyToRemoveOnGutsClosed; 84 NotificationGutsManager(Context context)85 public NotificationGutsManager(Context context) { 86 mContext = context; 87 Resources res = context.getResources(); 88 89 mAccessibilityManager = (AccessibilityManager) 90 mContext.getSystemService(Context.ACCESSIBILITY_SERVICE); 91 } 92 setUpWithPresenter(NotificationPresenter presenter, NotificationEntryManager entryManager, NotificationListContainer listContainer, NotificationInfo.CheckSaveListener checkSaveListener, OnSettingsClickListener onSettingsClickListener)93 public void setUpWithPresenter(NotificationPresenter presenter, 94 NotificationEntryManager entryManager, NotificationListContainer listContainer, 95 NotificationInfo.CheckSaveListener checkSaveListener, 96 OnSettingsClickListener onSettingsClickListener) { 97 mPresenter = presenter; 98 mEntryManager = entryManager; 99 mListContainer = listContainer; 100 mCheckSaveListener = checkSaveListener; 101 mOnSettingsClickListener = onSettingsClickListener; 102 } 103 getKeyToRemoveOnGutsClosed()104 public String getKeyToRemoveOnGutsClosed() { 105 return mKeyToRemoveOnGutsClosed; 106 } 107 setKeyToRemoveOnGutsClosed(String keyToRemoveOnGutsClosed)108 public void setKeyToRemoveOnGutsClosed(String keyToRemoveOnGutsClosed) { 109 mKeyToRemoveOnGutsClosed = keyToRemoveOnGutsClosed; 110 } 111 onDensityOrFontScaleChanged(ExpandableNotificationRow row)112 public void onDensityOrFontScaleChanged(ExpandableNotificationRow row) { 113 setExposedGuts(row.getGuts()); 114 bindGuts(row); 115 } 116 117 /** 118 * Sends an intent to open the app settings for a particular package and optional 119 * channel. 120 */ startAppNotificationSettingsActivity(String packageName, final int appUid, final NotificationChannel channel, ExpandableNotificationRow row)121 private void startAppNotificationSettingsActivity(String packageName, final int appUid, 122 final NotificationChannel channel, ExpandableNotificationRow row) { 123 final Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); 124 intent.setData(Uri.fromParts("package", packageName, null)); 125 intent.putExtra(Settings.EXTRA_APP_PACKAGE, packageName); 126 intent.putExtra(Settings.EXTRA_APP_UID, appUid); 127 if (channel != null) { 128 intent.putExtra(EXTRA_FRAGMENT_ARG_KEY, channel.getId()); 129 } 130 mPresenter.startNotificationGutsIntent(intent, appUid, row); 131 } 132 startAppOpsSettingsActivity(String pkg, int uid, ArraySet<Integer> ops, ExpandableNotificationRow row)133 protected void startAppOpsSettingsActivity(String pkg, int uid, ArraySet<Integer> ops, 134 ExpandableNotificationRow row) { 135 if (ops.contains(OP_SYSTEM_ALERT_WINDOW)) { 136 if (ops.contains(OP_CAMERA) || ops.contains(OP_RECORD_AUDIO)) { 137 startAppNotificationSettingsActivity(pkg, uid, null, row); 138 } else { 139 Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION); 140 intent.setData(Uri.fromParts("package", pkg, null)); 141 mPresenter.startNotificationGutsIntent(intent, uid, row); 142 } 143 } else if (ops.contains(OP_CAMERA) || ops.contains(OP_RECORD_AUDIO)) { 144 Intent intent = new Intent(Intent.ACTION_MANAGE_APP_PERMISSIONS); 145 intent.putExtra(Intent.EXTRA_PACKAGE_NAME, pkg); 146 mPresenter.startNotificationGutsIntent(intent, uid, row); 147 } 148 } 149 bindGuts(final ExpandableNotificationRow row)150 public void bindGuts(final ExpandableNotificationRow row) { 151 bindGuts(row, mGutsMenuItem); 152 } 153 bindGuts(final ExpandableNotificationRow row, NotificationMenuRowPlugin.MenuItem item)154 private void bindGuts(final ExpandableNotificationRow row, 155 NotificationMenuRowPlugin.MenuItem item) { 156 StatusBarNotification sbn = row.getStatusBarNotification(); 157 158 row.inflateGuts(); 159 row.setGutsView(item); 160 row.setTag(sbn.getPackageName()); 161 row.getGuts().setClosedListener((NotificationGuts g) -> { 162 row.onGutsClosed(); 163 if (!g.willBeRemoved() && !row.isRemoved()) { 164 mListContainer.onHeightChanged( 165 row, !mPresenter.isPresenterFullyCollapsed() /* needsAnimation */); 166 } 167 if (mNotificationGutsExposed == g) { 168 mNotificationGutsExposed = null; 169 mGutsMenuItem = null; 170 } 171 String key = sbn.getKey(); 172 if (key.equals(mKeyToRemoveOnGutsClosed)) { 173 mKeyToRemoveOnGutsClosed = null; 174 mEntryManager.removeNotification(key, mEntryManager.getLatestRankingMap()); 175 } 176 }); 177 178 View gutsView = item.getGutsView(); 179 if (gutsView instanceof NotificationSnooze) { 180 initializeSnoozeView(row, (NotificationSnooze) gutsView); 181 } else if (gutsView instanceof AppOpsInfo) { 182 initializeAppOpsInfo(row, (AppOpsInfo) gutsView); 183 } else if (gutsView instanceof NotificationInfo) { 184 initializeNotificationInfo(row, (NotificationInfo) gutsView); 185 } 186 } 187 188 /** 189 * Sets up the {@link NotificationSnooze} inside the notification row's guts. 190 * 191 * @param row view to set up the guts for 192 * @param notificationSnoozeView view to set up/bind within {@code row} 193 */ initializeSnoozeView( final ExpandableNotificationRow row, NotificationSnooze notificationSnoozeView)194 private void initializeSnoozeView( 195 final ExpandableNotificationRow row, 196 NotificationSnooze notificationSnoozeView) { 197 NotificationGuts guts = row.getGuts(); 198 StatusBarNotification sbn = row.getStatusBarNotification(); 199 200 notificationSnoozeView.setSnoozeListener(mListContainer.getSwipeActionHelper()); 201 notificationSnoozeView.setStatusBarNotification(sbn); 202 notificationSnoozeView.setSnoozeOptions(row.getEntry().snoozeCriteria); 203 guts.setHeightChangedListener((NotificationGuts g) -> { 204 mListContainer.onHeightChanged(row, row.isShown() /* needsAnimation */); 205 }); 206 } 207 208 /** 209 * Sets up the {@link AppOpsInfo} inside the notification row's guts. 210 * 211 * @param row view to set up the guts for 212 * @param appOpsInfoView view to set up/bind within {@code row} 213 */ initializeAppOpsInfo( final ExpandableNotificationRow row, AppOpsInfo appOpsInfoView)214 private void initializeAppOpsInfo( 215 final ExpandableNotificationRow row, 216 AppOpsInfo appOpsInfoView) { 217 NotificationGuts guts = row.getGuts(); 218 StatusBarNotification sbn = row.getStatusBarNotification(); 219 UserHandle userHandle = sbn.getUser(); 220 PackageManager pmUser = StatusBar.getPackageManagerForUser(mContext, 221 userHandle.getIdentifier()); 222 223 AppOpsInfo.OnSettingsClickListener onSettingsClick = 224 (View v, String pkg, int uid, ArraySet<Integer> ops) -> { 225 mMetricsLogger.action(MetricsProto.MetricsEvent.ACTION_OPS_GUTS_SETTINGS); 226 guts.resetFalsingCheck(); 227 startAppOpsSettingsActivity(pkg, uid, ops, row); 228 }; 229 if (!row.getEntry().mActiveAppOps.isEmpty()) { 230 appOpsInfoView.bindGuts(pmUser, onSettingsClick, sbn, row.getEntry().mActiveAppOps); 231 } 232 } 233 234 /** 235 * Sets up the {@link NotificationInfo} inside the notification row's guts. 236 * 237 * @param row view to set up the guts for 238 * @param notificationInfoView view to set up/bind within {@code row} 239 */ 240 @VisibleForTesting initializeNotificationInfo( final ExpandableNotificationRow row, NotificationInfo notificationInfoView)241 void initializeNotificationInfo( 242 final ExpandableNotificationRow row, 243 NotificationInfo notificationInfoView) { 244 NotificationGuts guts = row.getGuts(); 245 StatusBarNotification sbn = row.getStatusBarNotification(); 246 String packageName = sbn.getPackageName(); 247 // Settings link is only valid for notifications that specify a non-system user 248 NotificationInfo.OnSettingsClickListener onSettingsClick = null; 249 UserHandle userHandle = sbn.getUser(); 250 PackageManager pmUser = StatusBar.getPackageManagerForUser( 251 mContext, userHandle.getIdentifier()); 252 INotificationManager iNotificationManager = INotificationManager.Stub.asInterface( 253 ServiceManager.getService(Context.NOTIFICATION_SERVICE)); 254 final NotificationInfo.OnAppSettingsClickListener onAppSettingsClick = 255 (View v, Intent intent) -> { 256 mMetricsLogger.action(MetricsProto.MetricsEvent.ACTION_APP_NOTE_SETTINGS); 257 guts.resetFalsingCheck(); 258 mPresenter.startNotificationGutsIntent(intent, sbn.getUid(), row); 259 }; 260 boolean isForBlockingHelper = row.isBlockingHelperShowing(); 261 262 if (!userHandle.equals(UserHandle.ALL) 263 || mLockscreenUserManager.getCurrentUserId() == UserHandle.USER_SYSTEM) { 264 onSettingsClick = (View v, NotificationChannel channel, int appUid) -> { 265 mMetricsLogger.action(MetricsProto.MetricsEvent.ACTION_NOTE_INFO); 266 guts.resetFalsingCheck(); 267 mOnSettingsClickListener.onClick(sbn.getKey()); 268 startAppNotificationSettingsActivity(packageName, appUid, channel, row); 269 }; 270 } 271 272 try { 273 notificationInfoView.bindNotification( 274 pmUser, 275 iNotificationManager, 276 packageName, 277 row.getEntry().channel, 278 row.getNumUniqueChannels(), 279 sbn, 280 mCheckSaveListener, 281 onSettingsClick, 282 onAppSettingsClick, 283 row.getIsNonblockable(), 284 isForBlockingHelper, 285 row.getEntry().userSentiment == USER_SENTIMENT_NEGATIVE); 286 } catch (RemoteException e) { 287 Log.e(TAG, e.toString()); 288 } 289 } 290 291 /** 292 * Closes guts or notification menus that might be visible and saves any changes. 293 * 294 * @param removeLeavebehinds true if leavebehinds (e.g. snooze) should be closed. 295 * @param force true if guts should be closed regardless of state (used for snooze only). 296 * @param removeControls true if controls (e.g. info) should be closed. 297 * @param x if closed based on touch location, this is the x touch location. 298 * @param y if closed based on touch location, this is the y touch location. 299 * @param resetMenu if any notification menus that might be revealed should be closed. 300 */ closeAndSaveGuts(boolean removeLeavebehinds, boolean force, boolean removeControls, int x, int y, boolean resetMenu)301 public void closeAndSaveGuts(boolean removeLeavebehinds, boolean force, boolean removeControls, 302 int x, int y, boolean resetMenu) { 303 if (mNotificationGutsExposed != null) { 304 mNotificationGutsExposed.closeControls(removeLeavebehinds, removeControls, x, y, force); 305 } 306 if (resetMenu) { 307 mListContainer.resetExposedMenuView(false /* animate */, true /* force */); 308 } 309 } 310 311 /** 312 * Returns the exposed NotificationGuts or null if none are exposed. 313 */ getExposedGuts()314 public NotificationGuts getExposedGuts() { 315 return mNotificationGutsExposed; 316 } 317 setExposedGuts(NotificationGuts guts)318 public void setExposedGuts(NotificationGuts guts) { 319 mNotificationGutsExposed = guts; 320 } 321 322 /** 323 * Opens guts on the given ExpandableNotificationRow {@code view}. This handles opening guts for 324 * the normal half-swipe and long-press use cases via a circular reveal. When the blocking 325 * helper needs to be shown on the row, this will skip the circular reveal. 326 * 327 * @param view ExpandableNotificationRow to open guts on 328 * @param x x coordinate of origin of circular reveal 329 * @param y y coordinate of origin of circular reveal 330 * @param menuItem MenuItem the guts should display 331 * @return true if guts was opened 332 */ openGuts( View view, int x, int y, NotificationMenuRowPlugin.MenuItem menuItem)333 boolean openGuts( 334 View view, 335 int x, 336 int y, 337 NotificationMenuRowPlugin.MenuItem menuItem) { 338 if (!(view instanceof ExpandableNotificationRow)) { 339 return false; 340 } 341 342 if (view.getWindowToken() == null) { 343 Log.e(TAG, "Trying to show notification guts, but not attached to window"); 344 return false; 345 } 346 347 final ExpandableNotificationRow row = (ExpandableNotificationRow) view; 348 if (row.isDark()) { 349 return false; 350 } 351 view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); 352 if (row.areGutsExposed()) { 353 closeAndSaveGuts(false /* removeLeavebehind */, false /* force */, 354 true /* removeControls */, -1 /* x */, -1 /* y */, 355 true /* resetMenu */); 356 return false; 357 } 358 bindGuts(row, menuItem); 359 NotificationGuts guts = row.getGuts(); 360 361 // Assume we are a status_bar_notification_row 362 if (guts == null) { 363 // This view has no guts. Examples are the more card or the dismiss all view 364 return false; 365 } 366 367 mMetricsLogger.action(MetricsProto.MetricsEvent.ACTION_NOTE_CONTROLS); 368 369 // ensure that it's laid but not visible until actually laid out 370 guts.setVisibility(View.INVISIBLE); 371 // Post to ensure the the guts are properly laid out. 372 guts.post(new Runnable() { 373 @Override 374 public void run() { 375 if (row.getWindowToken() == null) { 376 Log.e(TAG, "Trying to show notification guts in post(), but not attached to " 377 + "window"); 378 return; 379 } 380 closeAndSaveGuts(true /* removeLeavebehind */, true /* force */, 381 true /* removeControls */, -1 /* x */, -1 /* y */, 382 false /* resetMenu */); 383 guts.setVisibility(View.VISIBLE); 384 385 final boolean needsFalsingProtection = 386 (mPresenter.isPresenterLocked() && 387 !mAccessibilityManager.isTouchExplorationEnabled()); 388 389 guts.openControls( 390 !row.isBlockingHelperShowing(), 391 x, 392 y, 393 needsFalsingProtection, 394 row::onGutsOpened); 395 396 row.closeRemoteInput(); 397 mListContainer.onHeightChanged(row, true /* needsAnimation */); 398 mNotificationGutsExposed = guts; 399 mGutsMenuItem = menuItem; 400 } 401 }); 402 return true; 403 } 404 405 @Override dump(FileDescriptor fd, PrintWriter pw, String[] args)406 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { 407 pw.println("NotificationGutsManager state:"); 408 pw.print(" mKeyToRemoveOnGutsClosed: "); 409 pw.println(mKeyToRemoveOnGutsClosed); 410 } 411 412 public interface OnSettingsClickListener { onClick(String key)413 void onClick(String key); 414 } 415 } 416