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