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