2 * Copyright (C) 2017 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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
16 package com.android.systemui.statusbar.notification.row;
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;
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 androidx.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;
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.notification.NotificationEntryManager;
49 import com.android.systemui.statusbar.NotificationLockscreenUserManager;
50 import com.android.systemui.statusbar.NotificationPresenter;
51 import com.android.systemui.statusbar.notification.stack.NotificationListContainer;
52 import com.android.systemui.statusbar.phone.StatusBar;
54 import java.io.FileDescriptor;
55 import java.io.PrintWriter;
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.
61 public class NotificationGutsManager implements Dumpable {
62 private static final String TAG = "NotificationGutsManager";
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";
67 private final MetricsLogger mMetricsLogger = Dependency.get(MetricsLogger.class);
68 private final Context mContext;
69 private final AccessibilityManager mAccessibilityManager;
72 private final NotificationLockscreenUserManager mLockscreenUserManager =
73 Dependency.get(NotificationLockscreenUserManager.class);
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;
85 public NotificationGutsManager(Context context) {
87 Resources res = context.getResources();
89 mAccessibilityManager = (AccessibilityManager)
90 mContext.getSystemService(Context.ACCESSIBILITY_SERVICE);
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;
104 public String getKeyToRemoveOnGutsClosed() {
105 return mKeyToRemoveOnGutsClosed;
108 public void setKeyToRemoveOnGutsClosed(String keyToRemoveOnGutsClosed) {
109 mKeyToRemoveOnGutsClosed = keyToRemoveOnGutsClosed;
112 public void onDensityOrFontScaleChanged(ExpandableNotificationRow row) {
113 setExposedGuts(row.getGuts());
118 * Sends an intent to open the app settings for a particular package and optional
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());
130 mPresenter.startNotificationGutsIntent(intent, appUid, 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);
139 Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION);
140 intent.setData(Uri.fromParts("package", pkg, null));
141 mPresenter.startNotificationGutsIntent(intent, uid, row);
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);
150 public void bindGuts(final ExpandableNotificationRow row) {
151 bindGuts(row, mGutsMenuItem);
154 private void bindGuts(final ExpandableNotificationRow row,
155 NotificationMenuRowPlugin.MenuItem item) {
156 StatusBarNotification sbn = row.getStatusBarNotification();
159 row.setGutsView(item);
160 row.setTag(sbn.getPackageName());
161 row.getGuts().setClosedListener((NotificationGuts g) -> {
163 if (!g.willBeRemoved() && !row.isRemoved()) {
164 mListContainer.onHeightChanged(
165 row, !mPresenter.isPresenterFullyCollapsed() /* needsAnimation */);
167 if (mNotificationGutsExposed == g) {
168 mNotificationGutsExposed = null;
169 mGutsMenuItem = null;
171 String key = sbn.getKey();
172 if (key.equals(mKeyToRemoveOnGutsClosed)) {
173 mKeyToRemoveOnGutsClosed = null;
174 mEntryManager.removeNotification(key, mEntryManager.getLatestRankingMap());
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);
189 * Sets up the {@link NotificationSnooze} inside the notification row's guts.
191 * @param row view to set up the guts for
192 * @param notificationSnoozeView view to set up/bind within {@code row}
194 private void initializeSnoozeView(
195 final ExpandableNotificationRow row,
196 NotificationSnooze notificationSnoozeView) {
197 NotificationGuts guts = row.getGuts();
198 StatusBarNotification sbn = row.getStatusBarNotification();
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 */);
209 * Sets up the {@link AppOpsInfo} inside the notification row's guts.
211 * @param row view to set up the guts for
212 * @param appOpsInfoView view to set up/bind within {@code row}
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());
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);
229 if (!row.getEntry().mActiveAppOps.isEmpty()) {
230 appOpsInfoView.bindGuts(pmUser, onSettingsClick, sbn, row.getEntry().mActiveAppOps);
235 * Sets up the {@link NotificationInfo} inside the notification row's guts.
237 * @param row view to set up the guts for
238 * @param notificationInfoView view to set up/bind within {@code row}
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);
260 boolean isForBlockingHelper = row.isBlockingHelperShowing();
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);
273 notificationInfoView.bindNotification(
275 iNotificationManager,
277 row.getEntry().channel,
278 row.getNumUniqueChannels(),
283 row.getIsNonblockable(),
285 row.getEntry().userSentiment == USER_SENTIMENT_NEGATIVE);
286 } catch (RemoteException e) {
287 Log.e(TAG, e.toString());
292 * Closes guts or notification menus that might be visible and saves any changes.
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.
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);
307 mListContainer.resetExposedMenuView(false /* animate */, true /* force */);
312 * Returns the exposed NotificationGuts or null if none are exposed.
314 public NotificationGuts getExposedGuts() {
315 return mNotificationGutsExposed;
318 public void setExposedGuts(NotificationGuts guts) {
319 mNotificationGutsExposed = guts;
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.
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
333 public boolean openGuts(
337 NotificationMenuRowPlugin.MenuItem menuItem) {
338 if (!(view instanceof ExpandableNotificationRow)) {
342 if (view.getWindowToken() == null) {
343 Log.e(TAG, "Trying to show notification guts, but not attached to window");
347 final ExpandableNotificationRow row = (ExpandableNotificationRow) view;
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 */);
358 bindGuts(row, menuItem);
359 NotificationGuts guts = row.getGuts();
361 // Assume we are a status_bar_notification_row
363 // This view has no guts. Examples are the more card or the dismiss all view
367 mMetricsLogger.action(MetricsProto.MetricsEvent.ACTION_NOTE_CONTROLS);
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() {
375 if (row.getWindowToken() == null) {
376 Log.e(TAG, "Trying to show notification guts in post(), but not attached to "
380 closeAndSaveGuts(true /* removeLeavebehind */, true /* force */,
381 true /* removeControls */, -1 /* x */, -1 /* y */,
382 false /* resetMenu */);
383 guts.setVisibility(View.VISIBLE);
385 final boolean needsFalsingProtection =
386 (mPresenter.isPresenterLocked() &&
387 !mAccessibilityManager.isTouchExplorationEnabled());
390 !row.isBlockingHelperShowing(),
393 needsFalsingProtection,
396 row.closeRemoteInput();
397 mListContainer.onHeightChanged(row, true /* needsAnimation */);
398 mNotificationGutsExposed = guts;
399 mGutsMenuItem = menuItem;
406 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
407 pw.println("NotificationGutsManager state:");
408 pw.print(" mKeyToRemoveOnGutsClosed: ");
409 pw.println(mKeyToRemoveOnGutsClosed);
412 public interface OnSettingsClickListener {
413 void onClick(String key);