• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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