Extracts NotificationActivityStarter from NotificationPresenter.
This change simplifies the NotificationPresenter interface by extracting
the methods dealing with starting an activity due to actions taken on a
notification to a separate interface, NotificationActivityStarter.
The extracted methods in StatusBarNotificationActivityStarter are also
cleaned up to make the control flow easier to read.
Pure refactor; no functional changes.
Test: atest SystemUITests
Change-Id: I2e8cf0626a005dc52b9969865a097cc14da9b669
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java
new file mode 100644
index 0000000..c93d151
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java
@@ -0,0 +1,368 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.phone;
+
+import static com.android.systemui.Dependency.MAIN_HANDLER;
+import static com.android.systemui.SysUiServiceProvider.getComponent;
+import static com.android.systemui.statusbar.phone.StatusBar.getActivityOptions;
+
+import android.app.ActivityManager;
+import android.app.ActivityTaskManager;
+import android.app.KeyguardManager;
+import android.app.Notification;
+import android.app.PendingIntent;
+import android.app.TaskStackBuilder;
+import android.content.Context;
+import android.content.Intent;
+import android.os.AsyncTask;
+import android.os.Looper;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.os.UserHandle;
+import android.service.notification.StatusBarNotification;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.RemoteAnimationAdapter;
+
+import com.android.internal.statusbar.IStatusBarService;
+import com.android.internal.statusbar.NotificationVisibility;
+import com.android.internal.widget.LockPatternUtils;
+import com.android.systemui.Dependency;
+import com.android.systemui.assist.AssistManager;
+import com.android.systemui.plugins.ActivityStarter;
+import com.android.systemui.statusbar.CommandQueue;
+import com.android.systemui.statusbar.NotificationLockscreenUserManager;
+import com.android.systemui.statusbar.NotificationPresenter;
+import com.android.systemui.statusbar.NotificationRemoteInputManager;
+import com.android.systemui.statusbar.RemoteInputController;
+import com.android.systemui.statusbar.StatusBarState;
+import com.android.systemui.statusbar.StatusBarStateController;
+import com.android.systemui.statusbar.notification.ActivityLaunchAnimator;
+import com.android.systemui.statusbar.notification.NotificationActivityStarter;
+import com.android.systemui.statusbar.notification.NotificationData;
+import com.android.systemui.statusbar.notification.NotificationEntryManager;
+import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
+import com.android.systemui.statusbar.policy.HeadsUpUtil;
+import com.android.systemui.statusbar.policy.KeyguardMonitor;
+import com.android.systemui.statusbar.policy.PreviewInflater;
+
+/**
+ * Status bar implementation of {@link NotificationActivityStarter}.
+ */
+public class StatusBarNotificationActivityStarter implements NotificationActivityStarter {
+
+ private static final String TAG = "NotificationClickHandler";
+
+ private final AssistManager mAssistManager = Dependency.get(AssistManager.class);
+ private final NotificationGroupManager mGroupManager =
+ Dependency.get(NotificationGroupManager.class);
+ private final StatusBarRemoteInputCallback mStatusBarRemoteInputCallback =
+ (StatusBarRemoteInputCallback) Dependency.get(
+ NotificationRemoteInputManager.Callback.class);
+ private final NotificationRemoteInputManager mRemoteInputManager =
+ Dependency.get(NotificationRemoteInputManager.class);
+ private final NotificationLockscreenUserManager mLockscreenUserManager =
+ Dependency.get(NotificationLockscreenUserManager.class);
+ private final ShadeController mShadeController = Dependency.get(ShadeController.class);
+ private final KeyguardMonitor mKeyguardMonitor = Dependency.get(KeyguardMonitor.class);
+ private final ActivityStarter mActivityStarter = Dependency.get(ActivityStarter.class);
+ private final NotificationEntryManager mEntryManager =
+ Dependency.get(NotificationEntryManager.class);
+ private final StatusBarStateController mStatusBarStateController =
+ Dependency.get(StatusBarStateController.class);
+
+ private final Context mContext;
+ private final NotificationPanelView mNotificationPanel;
+ private final NotificationPresenter mPresenter;
+ private final LockPatternUtils mLockPatternUtils;
+ private final HeadsUpManagerPhone mHeadsUpManager;
+ private final KeyguardManager mKeyguardManager;
+ private final ActivityLaunchAnimator mActivityLaunchAnimator;
+ private final IStatusBarService mBarService;
+ private final CommandQueue mCommandQueue;
+
+ private boolean mIsCollapsingToShowActivityOverLockscreen;
+
+ public StatusBarNotificationActivityStarter(Context context,
+ NotificationPanelView panel,
+ NotificationPresenter presenter,
+ HeadsUpManagerPhone headsUpManager,
+ ActivityLaunchAnimator activityLaunchAnimator) {
+ mContext = context;
+ mNotificationPanel = panel;
+ mPresenter = presenter;
+ mLockPatternUtils = new LockPatternUtils(context);
+ mHeadsUpManager = headsUpManager;
+ mKeyguardManager = context.getSystemService(KeyguardManager.class);
+ mActivityLaunchAnimator = activityLaunchAnimator;
+ mBarService = IStatusBarService.Stub.asInterface(
+ ServiceManager.getService(Context.STATUS_BAR_SERVICE));
+ mCommandQueue = getComponent(context, CommandQueue.class);
+ }
+
+ /**
+ * Called when a notification is clicked.
+ *
+ * @param sbn notification that was clicked
+ * @param row row for that notification
+ */
+ @Override
+ public void onNotificationClicked(StatusBarNotification sbn, ExpandableNotificationRow row) {
+ RemoteInputController controller = mRemoteInputManager.getController();
+ if (controller.isRemoteInputActive(row.getEntry())
+ && !TextUtils.isEmpty(row.getActiveRemoteInputText())) {
+ // We have an active remote input typed and the user clicked on the notification.
+ // this was probably unintentional, so we're closing the edit text instead.
+ controller.closeRemoteInputs();
+ return;
+ }
+ Notification notification = sbn.getNotification();
+ final PendingIntent intent = notification.contentIntent != null
+ ? notification.contentIntent
+ : notification.fullScreenIntent;
+ final String notificationKey = sbn.getKey();
+
+ boolean isActivityIntent = intent.isActivity();
+ final boolean afterKeyguardGone = isActivityIntent
+ && PreviewInflater.wouldLaunchResolverActivity(mContext, intent.getIntent(),
+ mLockscreenUserManager.getCurrentUserId());
+ final boolean wasOccluded = mShadeController.isOccluded();
+ boolean showOverLockscreen = mKeyguardMonitor.isShowing()
+ && PreviewInflater.wouldShowOverLockscreen(mContext,
+ intent.getIntent(),
+ mLockscreenUserManager.getCurrentUserId());
+ ActivityStarter.OnDismissAction postKeyguardAction =
+ () -> handleNotificationClickAfterKeyguardDismissed(
+ sbn, row, controller, intent, notificationKey,
+ isActivityIntent, wasOccluded, showOverLockscreen);
+ if (showOverLockscreen) {
+ mIsCollapsingToShowActivityOverLockscreen = true;
+ postKeyguardAction.onDismiss();
+ } else {
+ mActivityStarter.dismissKeyguardThenExecute(
+ postKeyguardAction, null /* cancel */, afterKeyguardGone);
+ }
+ }
+
+ private boolean handleNotificationClickAfterKeyguardDismissed(
+ StatusBarNotification sbn,
+ ExpandableNotificationRow row,
+ RemoteInputController controller,
+ PendingIntent intent,
+ String notificationKey,
+ boolean isActivityIntent,
+ boolean wasOccluded,
+ boolean showOverLockscreen) {
+ // TODO: Some of this code may be able to move to NotificationEntryManager.
+ if (mHeadsUpManager != null && mHeadsUpManager.isAlerting(notificationKey)) {
+ // Release the HUN notification to the shade.
+
+ if (mPresenter.isPresenterFullyCollapsed()) {
+ HeadsUpUtil.setIsClickedHeadsUpNotification(row, true);
+ }
+ //
+ // In most cases, when FLAG_AUTO_CANCEL is set, the notification will
+ // become canceled shortly by NoMan, but we can't assume that.
+ mHeadsUpManager.removeNotification(sbn.getKey(),
+ true /* releaseImmediately */);
+ }
+ StatusBarNotification parentToCancel = null;
+ if (shouldAutoCancel(sbn) && mGroupManager.isOnlyChildInGroup(sbn)) {
+ StatusBarNotification summarySbn =
+ mGroupManager.getLogicalGroupSummary(sbn).notification;
+ if (shouldAutoCancel(summarySbn)) {
+ parentToCancel = summarySbn;
+ }
+ }
+ final StatusBarNotification parentToCancelFinal = parentToCancel;
+ final Runnable runnable = () -> handleNotificationClickAfterPanelCollapsed(
+ sbn, row, controller, intent, notificationKey,
+ isActivityIntent, wasOccluded, parentToCancelFinal);
+
+ if (showOverLockscreen) {
+ mShadeController.addPostCollapseAction(runnable);
+ mShadeController.collapsePanel(true /* animate */);
+ } else if (mKeyguardMonitor.isShowing()
+ && mShadeController.isOccluded()) {
+ mShadeController.addAfterKeyguardGoneRunnable(runnable);
+ mShadeController.collapsePanel();
+ } else {
+ new Thread(runnable).start();
+ }
+
+ return !mNotificationPanel.isFullyCollapsed();
+ }
+
+ private void handleNotificationClickAfterPanelCollapsed(
+ StatusBarNotification sbn,
+ ExpandableNotificationRow row,
+ RemoteInputController controller,
+ PendingIntent intent,
+ String notificationKey,
+ boolean isActivityIntent,
+ boolean wasOccluded,
+ StatusBarNotification parentToCancelFinal) {
+ try {
+ // The intent we are sending is for the application, which
+ // won't have permission to immediately start an activity after
+ // the user switches to home. We know it is safe to do at this
+ // point, so make sure new activity switches are now allowed.
+ ActivityManager.getService().resumeAppSwitches();
+ } catch (RemoteException e) {
+ }
+ int launchResult;
+ // If we are launching a work activity and require to launch
+ // separate work challenge, we defer the activity action and cancel
+ // notification until work challenge is unlocked.
+ if (isActivityIntent) {
+ final int userId = intent.getCreatorUserHandle().getIdentifier();
+ if (mLockPatternUtils.isSeparateProfileChallengeEnabled(userId)
+ && mKeyguardManager.isDeviceLocked(userId)) {
+ // TODO(b/28935539): should allow certain activities to
+ // bypass work challenge
+ if (mStatusBarRemoteInputCallback.startWorkChallengeIfNecessary(userId,
+ intent.getIntentSender(), notificationKey)) {
+ // Show work challenge, do not run PendingIntent and
+ // remove notification
+ collapseOnMainThread();
+ return;
+ }
+ }
+ }
+ Intent fillInIntent = null;
+ NotificationData.Entry entry = row.getEntry();
+ CharSequence remoteInputText = null;
+ if (!TextUtils.isEmpty(entry.remoteInputText)) {
+ remoteInputText = entry.remoteInputText;
+ }
+ if (!TextUtils.isEmpty(remoteInputText) && !controller.isSpinning(entry.key)) {
+ fillInIntent = new Intent().putExtra(Notification.EXTRA_REMOTE_INPUT_DRAFT,
+ remoteInputText.toString());
+ }
+ RemoteAnimationAdapter adapter = mActivityLaunchAnimator.getLaunchAnimation(
+ row, wasOccluded);
+ try {
+ if (adapter != null) {
+ ActivityTaskManager.getService()
+ .registerRemoteAnimationForNextActivityStart(
+ intent.getCreatorPackage(), adapter);
+ }
+ launchResult = intent.sendAndReturnResult(mContext, 0, fillInIntent, null,
+ null, null, getActivityOptions(adapter));
+ mActivityLaunchAnimator.setLaunchResult(launchResult, isActivityIntent);
+ } catch (RemoteException | PendingIntent.CanceledException e) {
+ // the stack trace isn't very helpful here.
+ // Just log the exception message.
+ Log.w(TAG, "Sending contentIntent failed: " + e);
+
+ // TODO: Dismiss Keyguard.
+ }
+ if (isActivityIntent) {
+ mAssistManager.hideAssist();
+ }
+ if (shouldCollapse()) {
+ collapseOnMainThread();
+ }
+
+ final int count =
+ mEntryManager.getNotificationData().getActiveNotifications().size();
+ final int rank = mEntryManager.getNotificationData().getRank(notificationKey);
+ final NotificationVisibility nv = NotificationVisibility.obtain(notificationKey,
+ rank, count, true);
+ try {
+ mBarService.onNotificationClick(notificationKey, nv);
+ } catch (RemoteException ex) {
+ // system process is dead if we're here.
+ }
+ if (parentToCancelFinal != null) {
+ removeNotification(parentToCancelFinal);
+ }
+ if (shouldAutoCancel(sbn)
+ || mRemoteInputManager.isNotificationKeptForRemoteInputHistory(
+ notificationKey)) {
+ // Automatically remove all notifications that we may have kept around longer
+ removeNotification(sbn);
+ }
+ mIsCollapsingToShowActivityOverLockscreen = false;
+ }
+
+ @Override
+ public void startNotificationGutsIntent(final Intent intent, final int appUid,
+ ExpandableNotificationRow row) {
+ mActivityStarter.dismissKeyguardThenExecute(() -> {
+ AsyncTask.execute(() -> {
+ int launchResult = TaskStackBuilder.create(mContext)
+ .addNextIntentWithParentStack(intent)
+ .startActivities(getActivityOptions(
+ mActivityLaunchAnimator.getLaunchAnimation(
+ row, mShadeController.isOccluded())),
+ new UserHandle(UserHandle.getUserId(appUid)));
+ mActivityLaunchAnimator.setLaunchResult(launchResult, true /* isActivityIntent */);
+ if (shouldCollapse()) {
+ // Putting it back on the main thread, since we're touching views
+ Dependency.get(MAIN_HANDLER).post(() -> mCommandQueue.animateCollapsePanels(
+ CommandQueue.FLAG_EXCLUDE_RECENTS_PANEL, true /* force */));
+ }
+ });
+ return true;
+ }, null, false /* afterKeyguardGone */);
+ }
+
+ @Override
+ public boolean isCollapsingToShowActivityOverLockscreen() {
+ return mIsCollapsingToShowActivityOverLockscreen;
+ }
+
+ private static boolean shouldAutoCancel(StatusBarNotification sbn) {
+ int flags = sbn.getNotification().flags;
+ if ((flags & Notification.FLAG_AUTO_CANCEL) != Notification.FLAG_AUTO_CANCEL) {
+ return false;
+ }
+ if ((flags & Notification.FLAG_FOREGROUND_SERVICE) != 0) {
+ return false;
+ }
+ return true;
+ }
+
+ private void collapseOnMainThread() {
+ if (Looper.getMainLooper().isCurrentThread()) {
+ mShadeController.collapsePanel();
+ } else {
+ Dependency.get(MAIN_HANDLER).post(mShadeController::collapsePanel);
+ }
+ }
+
+ private boolean shouldCollapse() {
+ return mStatusBarStateController.getState() != StatusBarState.SHADE
+ || !mActivityLaunchAnimator.isAnimationPending();
+ }
+
+ private void removeNotification(StatusBarNotification notification) {
+ // We have to post it to the UI thread for synchronization
+ Dependency.get(MAIN_HANDLER).post(() -> {
+ Runnable removeRunnable =
+ () -> mEntryManager.performRemoveNotification(notification);
+ if (mPresenter.isCollapsing()) {
+ // To avoid lags we're only performing the remove
+ // after the shade was collapsed
+ mShadeController.addPostCollapseAction(removeRunnable);
+ } else {
+ removeRunnable.run();
+ }
+ });
+ }
+}