1 /*
2  * Copyright (C) 2018 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 
17 package com.android.systemui.statusbar.phone;
18 
19 import static android.service.notification.NotificationListenerService.REASON_CLICK;
20 
21 import static com.android.systemui.statusbar.phone.StatusBar.getActivityOptions;
22 
23 import android.app.ActivityManager;
24 import android.app.ActivityTaskManager;
25 import android.app.KeyguardManager;
26 import android.app.Notification;
27 import android.app.NotificationManager;
28 import android.app.PendingIntent;
29 import android.app.TaskStackBuilder;
30 import android.content.Context;
31 import android.content.Intent;
32 import android.os.AsyncTask;
33 import android.os.Handler;
34 import android.os.Looper;
35 import android.os.RemoteException;
36 import android.os.UserHandle;
37 import android.service.dreams.IDreamManager;
38 import android.service.notification.StatusBarNotification;
39 import android.text.TextUtils;
40 import android.util.EventLog;
41 import android.util.Log;
42 import android.view.RemoteAnimationAdapter;
43 
44 import com.android.internal.logging.MetricsLogger;
45 import com.android.internal.statusbar.IStatusBarService;
46 import com.android.internal.statusbar.NotificationVisibility;
47 import com.android.internal.widget.LockPatternUtils;
48 import com.android.systemui.ActivityIntentHelper;
49 import com.android.systemui.Dependency;
50 import com.android.systemui.EventLogTags;
51 import com.android.systemui.UiOffloadThread;
52 import com.android.systemui.assist.AssistManager;
53 import com.android.systemui.bubbles.BubbleController;
54 import com.android.systemui.plugins.ActivityStarter;
55 import com.android.systemui.plugins.statusbar.StatusBarStateController;
56 import com.android.systemui.statusbar.CommandQueue;
57 import com.android.systemui.statusbar.NotificationLockscreenUserManager;
58 import com.android.systemui.statusbar.NotificationPresenter;
59 import com.android.systemui.statusbar.NotificationRemoteInputManager;
60 import com.android.systemui.statusbar.RemoteInputController;
61 import com.android.systemui.statusbar.StatusBarState;
62 import com.android.systemui.statusbar.notification.ActivityLaunchAnimator;
63 import com.android.systemui.statusbar.notification.NotificationActivityStarter;
64 import com.android.systemui.statusbar.notification.NotificationEntryListener;
65 import com.android.systemui.statusbar.notification.NotificationEntryManager;
66 import com.android.systemui.statusbar.notification.NotificationInterruptionStateProvider;
67 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
68 import com.android.systemui.statusbar.notification.logging.NotificationLogger;
69 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
70 import com.android.systemui.statusbar.policy.HeadsUpUtil;
71 import com.android.systemui.statusbar.policy.KeyguardMonitor;
72 
73 /**
74  * Status bar implementation of {@link NotificationActivityStarter}.
75  */
76 public class StatusBarNotificationActivityStarter implements NotificationActivityStarter {
77 
78     private static final String TAG = "NotificationClickHandler";
79     protected static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
80 
81     private final AssistManager mAssistManager;
82     private final NotificationGroupManager mGroupManager;
83     private final StatusBarRemoteInputCallback mStatusBarRemoteInputCallback;
84     private final NotificationRemoteInputManager mRemoteInputManager;
85     private final NotificationLockscreenUserManager mLockscreenUserManager;
86     private final ShadeController mShadeController;
87     private final KeyguardMonitor mKeyguardMonitor;
88     private final ActivityStarter mActivityStarter;
89     private final NotificationEntryManager mEntryManager;
90     private final StatusBarStateController mStatusBarStateController;
91     private final NotificationInterruptionStateProvider mNotificationInterruptionStateProvider;
92     private final MetricsLogger mMetricsLogger;
93     private final Context mContext;
94     private final NotificationPanelView mNotificationPanel;
95     private final NotificationPresenter mPresenter;
96     private final LockPatternUtils mLockPatternUtils;
97     private final HeadsUpManagerPhone mHeadsUpManager;
98     private final KeyguardManager mKeyguardManager;
99     private final ActivityLaunchAnimator mActivityLaunchAnimator;
100     private final IStatusBarService mBarService;
101     private final CommandQueue mCommandQueue;
102     private final IDreamManager mDreamManager;
103     private final Handler mMainThreadHandler;
104     private final Handler mBackgroundHandler;
105     private final ActivityIntentHelper mActivityIntentHelper;
106     private final BubbleController mBubbleController;
107 
108     private boolean mIsCollapsingToShowActivityOverLockscreen;
109 
StatusBarNotificationActivityStarter(Context context, CommandQueue commandQueue, AssistManager assistManager, NotificationPanelView panel, NotificationPresenter presenter, NotificationEntryManager entryManager, HeadsUpManagerPhone headsUpManager, ActivityStarter activityStarter, ActivityLaunchAnimator activityLaunchAnimator, IStatusBarService statusBarService, StatusBarStateController statusBarStateController, KeyguardManager keyguardManager, IDreamManager dreamManager, NotificationRemoteInputManager remoteInputManager, StatusBarRemoteInputCallback remoteInputCallback, NotificationGroupManager groupManager, NotificationLockscreenUserManager lockscreenUserManager, ShadeController shadeController, KeyguardMonitor keyguardMonitor, NotificationInterruptionStateProvider notificationInterruptionStateProvider, MetricsLogger metricsLogger, LockPatternUtils lockPatternUtils, Handler mainThreadHandler, Handler backgroundHandler, ActivityIntentHelper activityIntentHelper, BubbleController bubbleController)110     public StatusBarNotificationActivityStarter(Context context,
111             CommandQueue commandQueue,
112             AssistManager assistManager,
113             NotificationPanelView panel,
114             NotificationPresenter presenter,
115             NotificationEntryManager entryManager,
116             HeadsUpManagerPhone headsUpManager,
117             ActivityStarter activityStarter,
118             ActivityLaunchAnimator activityLaunchAnimator,
119             IStatusBarService statusBarService,
120             StatusBarStateController statusBarStateController,
121             KeyguardManager keyguardManager,
122             IDreamManager dreamManager,
123             NotificationRemoteInputManager remoteInputManager,
124             StatusBarRemoteInputCallback remoteInputCallback,
125             NotificationGroupManager groupManager,
126             NotificationLockscreenUserManager lockscreenUserManager,
127             ShadeController shadeController,
128             KeyguardMonitor keyguardMonitor,
129             NotificationInterruptionStateProvider notificationInterruptionStateProvider,
130             MetricsLogger metricsLogger,
131             LockPatternUtils lockPatternUtils,
132             Handler mainThreadHandler,
133             Handler backgroundHandler,
134             ActivityIntentHelper activityIntentHelper,
135             BubbleController bubbleController) {
136         mContext = context;
137         mNotificationPanel = panel;
138         mPresenter = presenter;
139         mHeadsUpManager = headsUpManager;
140         mActivityLaunchAnimator = activityLaunchAnimator;
141         mBarService = statusBarService;
142         mCommandQueue = commandQueue;
143         mKeyguardManager = keyguardManager;
144         mDreamManager = dreamManager;
145         mRemoteInputManager = remoteInputManager;
146         mLockscreenUserManager = lockscreenUserManager;
147         mShadeController = shadeController;
148         mKeyguardMonitor = keyguardMonitor;
149         mActivityStarter = activityStarter;
150         mEntryManager = entryManager;
151         mStatusBarStateController = statusBarStateController;
152         mNotificationInterruptionStateProvider = notificationInterruptionStateProvider;
153         mMetricsLogger = metricsLogger;
154         mAssistManager = assistManager;
155         mGroupManager = groupManager;
156         mLockPatternUtils = lockPatternUtils;
157         mBackgroundHandler = backgroundHandler;
158         mEntryManager.addNotificationEntryListener(new NotificationEntryListener() {
159             @Override
160             public void onPendingEntryAdded(NotificationEntry entry) {
161                 handleFullScreenIntent(entry);
162             }
163         });
164         mStatusBarRemoteInputCallback = remoteInputCallback;
165         mMainThreadHandler = mainThreadHandler;
166         mActivityIntentHelper = activityIntentHelper;
167         mBubbleController = bubbleController;
168     }
169 
170     /**
171      * Called when a notification is clicked.
172      *
173      * @param sbn notification that was clicked
174      * @param row row for that notification
175      */
176     @Override
onNotificationClicked(StatusBarNotification sbn, ExpandableNotificationRow row)177     public void onNotificationClicked(StatusBarNotification sbn, ExpandableNotificationRow row) {
178         RemoteInputController controller = mRemoteInputManager.getController();
179         if (controller.isRemoteInputActive(row.getEntry())
180                 && !TextUtils.isEmpty(row.getActiveRemoteInputText())) {
181             // We have an active remote input typed and the user clicked on the notification.
182             // this was probably unintentional, so we're closing the edit text instead.
183             controller.closeRemoteInputs();
184             return;
185         }
186         Notification notification = sbn.getNotification();
187         final PendingIntent intent = notification.contentIntent != null
188                 ? notification.contentIntent
189                 : notification.fullScreenIntent;
190         final boolean isBubble = row.getEntry().isBubble();
191 
192         // This code path is now executed for notification without a contentIntent.
193         // The only valid case is Bubble notifications. Guard against other cases
194         // entering here.
195         if (intent == null && !isBubble) {
196             Log.e(TAG, "onNotificationClicked called for non-clickable notification!");
197             return;
198         }
199 
200         final String notificationKey = sbn.getKey();
201 
202         boolean isActivityIntent = intent != null && intent.isActivity() && !isBubble;
203         final boolean afterKeyguardGone = isActivityIntent
204                 && mActivityIntentHelper.wouldLaunchResolverActivity(intent.getIntent(),
205                 mLockscreenUserManager.getCurrentUserId());
206         final boolean wasOccluded = mShadeController.isOccluded();
207         boolean showOverLockscreen = mKeyguardMonitor.isShowing() && intent != null
208                 && mActivityIntentHelper.wouldShowOverLockscreen(intent.getIntent(),
209                 mLockscreenUserManager.getCurrentUserId());
210         ActivityStarter.OnDismissAction postKeyguardAction =
211                 () -> handleNotificationClickAfterKeyguardDismissed(
212                         sbn, row, controller, intent, notificationKey,
213                         isActivityIntent, wasOccluded, showOverLockscreen);
214         if (showOverLockscreen) {
215             mIsCollapsingToShowActivityOverLockscreen = true;
216             postKeyguardAction.onDismiss();
217         } else {
218             mActivityStarter.dismissKeyguardThenExecute(
219                     postKeyguardAction, null /* cancel */, afterKeyguardGone);
220         }
221     }
222 
handleNotificationClickAfterKeyguardDismissed( StatusBarNotification sbn, ExpandableNotificationRow row, RemoteInputController controller, PendingIntent intent, String notificationKey, boolean isActivityIntent, boolean wasOccluded, boolean showOverLockscreen)223     private boolean handleNotificationClickAfterKeyguardDismissed(
224             StatusBarNotification sbn,
225             ExpandableNotificationRow row,
226             RemoteInputController controller,
227             PendingIntent intent,
228             String notificationKey,
229             boolean isActivityIntent,
230             boolean wasOccluded,
231             boolean showOverLockscreen) {
232         // TODO: Some of this code may be able to move to NotificationEntryManager.
233         if (mHeadsUpManager != null && mHeadsUpManager.isAlerting(notificationKey)) {
234             // Release the HUN notification to the shade.
235 
236             if (mPresenter.isPresenterFullyCollapsed()) {
237                 HeadsUpUtil.setIsClickedHeadsUpNotification(row, true);
238             }
239             //
240             // In most cases, when FLAG_AUTO_CANCEL is set, the notification will
241             // become canceled shortly by NoMan, but we can't assume that.
242             mHeadsUpManager.removeNotification(sbn.getKey(),
243                     true /* releaseImmediately */);
244         }
245         StatusBarNotification parentToCancel = null;
246         if (shouldAutoCancel(sbn) && mGroupManager.isOnlyChildInGroup(sbn)) {
247             StatusBarNotification summarySbn =
248                     mGroupManager.getLogicalGroupSummary(sbn).notification;
249             if (shouldAutoCancel(summarySbn)) {
250                 parentToCancel = summarySbn;
251             }
252         }
253         final StatusBarNotification parentToCancelFinal = parentToCancel;
254         final Runnable runnable = () -> handleNotificationClickAfterPanelCollapsed(
255                 sbn, row, controller, intent, notificationKey,
256                 isActivityIntent, wasOccluded, parentToCancelFinal);
257 
258         if (showOverLockscreen) {
259             mShadeController.addPostCollapseAction(runnable);
260             mShadeController.collapsePanel(true /* animate */);
261         } else if (mKeyguardMonitor.isShowing()
262                 && mShadeController.isOccluded()) {
263             mShadeController.addAfterKeyguardGoneRunnable(runnable);
264             mShadeController.collapsePanel();
265         } else {
266             mBackgroundHandler.postAtFrontOfQueue(runnable);
267         }
268         return !mNotificationPanel.isFullyCollapsed();
269     }
270 
handleNotificationClickAfterPanelCollapsed( StatusBarNotification sbn, ExpandableNotificationRow row, RemoteInputController controller, PendingIntent intent, String notificationKey, boolean isActivityIntent, boolean wasOccluded, StatusBarNotification parentToCancelFinal)271     private void handleNotificationClickAfterPanelCollapsed(
272             StatusBarNotification sbn,
273             ExpandableNotificationRow row,
274             RemoteInputController controller,
275             PendingIntent intent,
276             String notificationKey,
277             boolean isActivityIntent,
278             boolean wasOccluded,
279             StatusBarNotification parentToCancelFinal) {
280         try {
281             // The intent we are sending is for the application, which
282             // won't have permission to immediately start an activity after
283             // the user switches to home.  We know it is safe to do at this
284             // point, so make sure new activity switches are now allowed.
285             ActivityManager.getService().resumeAppSwitches();
286         } catch (RemoteException e) {
287         }
288         // If we are launching a work activity and require to launch
289         // separate work challenge, we defer the activity action and cancel
290         // notification until work challenge is unlocked.
291         if (isActivityIntent) {
292             final int userId = intent.getCreatorUserHandle().getIdentifier();
293             if (mLockPatternUtils.isSeparateProfileChallengeEnabled(userId)
294                     && mKeyguardManager.isDeviceLocked(userId)) {
295                 // TODO(b/28935539): should allow certain activities to
296                 // bypass work challenge
297                 if (mStatusBarRemoteInputCallback.startWorkChallengeIfNecessary(userId,
298                         intent.getIntentSender(), notificationKey)) {
299                     // Show work challenge, do not run PendingIntent and
300                     // remove notification
301                     collapseOnMainThread();
302                     return;
303                 }
304             }
305         }
306         Intent fillInIntent = null;
307         NotificationEntry entry = row.getEntry();
308         final boolean isBubble = entry.isBubble();
309         CharSequence remoteInputText = null;
310         if (!TextUtils.isEmpty(entry.remoteInputText)) {
311             remoteInputText = entry.remoteInputText;
312         }
313         if (!TextUtils.isEmpty(remoteInputText) && !controller.isSpinning(entry.key)) {
314             fillInIntent = new Intent().putExtra(Notification.EXTRA_REMOTE_INPUT_DRAFT,
315                     remoteInputText.toString());
316         }
317         if (isBubble) {
318             expandBubbleStackOnMainThread(notificationKey);
319         } else {
320             startNotificationIntent(intent, fillInIntent, row, wasOccluded, isActivityIntent);
321         }
322         if (isActivityIntent || isBubble) {
323             mAssistManager.hideAssist();
324         }
325         if (shouldCollapse()) {
326             collapseOnMainThread();
327         }
328 
329         final int count =
330                 mEntryManager.getNotificationData().getActiveNotifications().size();
331         final int rank = mEntryManager.getNotificationData().getRank(notificationKey);
332         NotificationVisibility.NotificationLocation location =
333                 NotificationLogger.getNotificationLocation(
334                         mEntryManager.getNotificationData().get(notificationKey));
335         final NotificationVisibility nv = NotificationVisibility.obtain(notificationKey,
336                 rank, count, true, location);
337         try {
338             mBarService.onNotificationClick(notificationKey, nv);
339         } catch (RemoteException ex) {
340             // system process is dead if we're here.
341         }
342         if (!isBubble) {
343             if (parentToCancelFinal != null) {
344                 removeNotification(parentToCancelFinal);
345             }
346             if (shouldAutoCancel(sbn)
347                     || mRemoteInputManager.isNotificationKeptForRemoteInputHistory(
348                     notificationKey)) {
349                 // Automatically remove all notifications that we may have kept around longer
350                 removeNotification(sbn);
351             }
352         }
353         mIsCollapsingToShowActivityOverLockscreen = false;
354     }
355 
expandBubbleStackOnMainThread(String notificationKey)356     private void expandBubbleStackOnMainThread(String notificationKey) {
357         if (Looper.getMainLooper().isCurrentThread()) {
358             mBubbleController.expandStackAndSelectBubble(notificationKey);
359         } else {
360             mMainThreadHandler.post(
361                     () -> mBubbleController.expandStackAndSelectBubble(notificationKey));
362         }
363     }
364 
startNotificationIntent(PendingIntent intent, Intent fillInIntent, ExpandableNotificationRow row, boolean wasOccluded, boolean isActivityIntent)365     private void startNotificationIntent(PendingIntent intent, Intent fillInIntent,
366             ExpandableNotificationRow row, boolean wasOccluded, boolean isActivityIntent) {
367         RemoteAnimationAdapter adapter = mActivityLaunchAnimator.getLaunchAnimation(row,
368                 wasOccluded);
369         try {
370             if (adapter != null) {
371                 ActivityTaskManager.getService()
372                         .registerRemoteAnimationForNextActivityStart(
373                                 intent.getCreatorPackage(), adapter);
374             }
375             int launchResult = intent.sendAndReturnResult(mContext, 0, fillInIntent, null,
376                     null, null, getActivityOptions(adapter));
377             mActivityLaunchAnimator.setLaunchResult(launchResult, isActivityIntent);
378         } catch (RemoteException | PendingIntent.CanceledException e) {
379             // the stack trace isn't very helpful here.
380             // Just log the exception message.
381             Log.w(TAG, "Sending contentIntent failed: " + e);
382             // TODO: Dismiss Keyguard.
383         }
384     }
385 
386     @Override
startNotificationGutsIntent(final Intent intent, final int appUid, ExpandableNotificationRow row)387     public void startNotificationGutsIntent(final Intent intent, final int appUid,
388             ExpandableNotificationRow row) {
389         mActivityStarter.dismissKeyguardThenExecute(() -> {
390             AsyncTask.execute(() -> {
391                 int launchResult = TaskStackBuilder.create(mContext)
392                         .addNextIntentWithParentStack(intent)
393                         .startActivities(getActivityOptions(
394                                 mActivityLaunchAnimator.getLaunchAnimation(
395                                         row, mShadeController.isOccluded())),
396                                 new UserHandle(UserHandle.getUserId(appUid)));
397                 mActivityLaunchAnimator.setLaunchResult(launchResult, true /* isActivityIntent */);
398                 if (shouldCollapse()) {
399                     // Putting it back on the main thread, since we're touching views
400                     mMainThreadHandler.post(() -> mCommandQueue.animateCollapsePanels(
401                             CommandQueue.FLAG_EXCLUDE_RECENTS_PANEL, true /* force */));
402                 }
403             });
404             return true;
405         }, null, false /* afterKeyguardGone */);
406     }
407 
handleFullScreenIntent(NotificationEntry entry)408     private void handleFullScreenIntent(NotificationEntry entry) {
409         if (mNotificationInterruptionStateProvider.shouldLaunchFullScreenIntentWhenAdded(entry)) {
410             if (shouldSuppressFullScreenIntent(entry)) {
411                 if (DEBUG) {
412                     Log.d(TAG, "No Fullscreen intent: suppressed by DND: " + entry.key);
413                 }
414             } else if (entry.importance < NotificationManager.IMPORTANCE_HIGH) {
415                 if (DEBUG) {
416                     Log.d(TAG, "No Fullscreen intent: not important enough: " + entry.key);
417                 }
418             } else {
419                 // Stop screensaver if the notification has a fullscreen intent.
420                 // (like an incoming phone call)
421                 Dependency.get(UiOffloadThread.class).submit(() -> {
422                     try {
423                         mDreamManager.awaken();
424                     } catch (RemoteException e) {
425                         e.printStackTrace();
426                     }
427                 });
428 
429                 // not immersive & a fullscreen alert should be shown
430                 if (DEBUG) {
431                     Log.d(TAG, "Notification has fullScreenIntent; sending fullScreenIntent");
432                 }
433                 try {
434                     EventLog.writeEvent(EventLogTags.SYSUI_FULLSCREEN_NOTIFICATION,
435                             entry.key);
436                     entry.notification.getNotification().fullScreenIntent.send();
437                     entry.notifyFullScreenIntentLaunched();
438                     mMetricsLogger.count("note_fullscreen", 1);
439                 } catch (PendingIntent.CanceledException e) {
440                     // ignore
441                 }
442             }
443         }
444     }
445 
446     @Override
isCollapsingToShowActivityOverLockscreen()447     public boolean isCollapsingToShowActivityOverLockscreen() {
448         return mIsCollapsingToShowActivityOverLockscreen;
449     }
450 
shouldAutoCancel(StatusBarNotification sbn)451     private static boolean shouldAutoCancel(StatusBarNotification sbn) {
452         int flags = sbn.getNotification().flags;
453         if ((flags & Notification.FLAG_AUTO_CANCEL) != Notification.FLAG_AUTO_CANCEL) {
454             return false;
455         }
456         if ((flags & Notification.FLAG_FOREGROUND_SERVICE) != 0) {
457             return false;
458         }
459         return true;
460     }
461 
collapseOnMainThread()462     private void collapseOnMainThread() {
463         if (Looper.getMainLooper().isCurrentThread()) {
464             mShadeController.collapsePanel();
465         } else {
466             mMainThreadHandler.post(mShadeController::collapsePanel);
467         }
468     }
469 
shouldCollapse()470     private boolean shouldCollapse() {
471         return mStatusBarStateController.getState() != StatusBarState.SHADE
472                 || !mActivityLaunchAnimator.isAnimationPending();
473     }
474 
shouldSuppressFullScreenIntent(NotificationEntry entry)475     private boolean shouldSuppressFullScreenIntent(NotificationEntry entry) {
476         if (mPresenter.isDeviceInVrMode()) {
477             return true;
478         }
479 
480         return entry.shouldSuppressFullScreenIntent();
481     }
482 
removeNotification(StatusBarNotification notification)483     private void removeNotification(StatusBarNotification notification) {
484         // We have to post it to the UI thread for synchronization
485         mMainThreadHandler.post(() -> {
486             Runnable removeRunnable =
487                     () -> mEntryManager.performRemoveNotification(notification, REASON_CLICK);
488             if (mPresenter.isCollapsing()) {
489                 // To avoid lags we're only performing the remove
490                 // after the shade was collapsed
491                 mShadeController.addPostCollapseAction(removeRunnable);
492             } else {
493                 removeRunnable.run();
494             }
495         });
496     }
497 }
498