1 /* 2 * Copyright (C) 2018 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file 5 * except in compliance with the License. You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software distributed under the 10 * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 11 * KIND, either express or implied. See the License for the specific language governing 12 * permissions and limitations under the License. 13 */ 14 15 package com.android.systemui.statusbar.phone; 16 17 import static android.content.Intent.ACTION_DEVICE_LOCKED_CHANGED; 18 19 import static com.android.systemui.statusbar.NotificationLockscreenUserManager.NOTIFICATION_UNLOCKED_BY_WORK_CHALLENGE_ACTION; 20 21 import android.app.ActivityManager; 22 import android.app.KeyguardManager; 23 import android.app.PendingIntent; 24 import android.app.StatusBarManager; 25 import android.content.BroadcastReceiver; 26 import android.content.Context; 27 import android.content.Intent; 28 import android.content.IntentFilter; 29 import android.content.IntentSender; 30 import android.os.RemoteException; 31 import android.os.UserHandle; 32 import android.view.View; 33 import android.view.ViewParent; 34 35 import androidx.annotation.Nullable; 36 37 import com.android.systemui.ActivityIntentHelper; 38 import com.android.systemui.dagger.SysUISingleton; 39 import com.android.systemui.dagger.qualifiers.Main; 40 import com.android.systemui.plugins.ActivityStarter; 41 import com.android.systemui.plugins.statusbar.StatusBarStateController; 42 import com.android.systemui.shade.ShadeController; 43 import com.android.systemui.statusbar.ActionClickLogger; 44 import com.android.systemui.statusbar.CommandQueue; 45 import com.android.systemui.statusbar.CommandQueue.Callbacks; 46 import com.android.systemui.statusbar.NotificationLockscreenUserManager; 47 import com.android.systemui.statusbar.NotificationRemoteInputManager; 48 import com.android.systemui.statusbar.NotificationRemoteInputManager.Callback; 49 import com.android.systemui.statusbar.StatusBarState; 50 import com.android.systemui.statusbar.SysuiStatusBarStateController; 51 import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManager; 52 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; 53 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout; 54 import com.android.systemui.statusbar.policy.KeyguardStateController; 55 56 import java.util.concurrent.Executor; 57 58 import javax.inject.Inject; 59 60 /** 61 */ 62 @SysUISingleton 63 public class StatusBarRemoteInputCallback implements Callback, Callbacks, 64 StatusBarStateController.StateListener { 65 66 private final KeyguardStateController mKeyguardStateController; 67 private final SysuiStatusBarStateController mStatusBarStateController; 68 private final NotificationLockscreenUserManager mLockscreenUserManager; 69 private final ActivityStarter mActivityStarter; 70 private final Context mContext; 71 private final StatusBarKeyguardViewManager mStatusBarKeyguardViewManager; 72 private final com.android.systemui.shade.ShadeController mShadeController; 73 private Executor mExecutor; 74 private final ActivityIntentHelper mActivityIntentHelper; 75 private final GroupExpansionManager mGroupExpansionManager; 76 private View mPendingWorkRemoteInputView; 77 private View mPendingRemoteInputView; 78 private KeyguardManager mKeyguardManager; 79 private final CommandQueue mCommandQueue; 80 private final ActionClickLogger mActionClickLogger; 81 private int mDisabled2; 82 protected BroadcastReceiver mChallengeReceiver = new ChallengeReceiver(); 83 84 /** 85 */ 86 @Inject StatusBarRemoteInputCallback( Context context, GroupExpansionManager groupExpansionManager, NotificationLockscreenUserManager notificationLockscreenUserManager, KeyguardStateController keyguardStateController, StatusBarStateController statusBarStateController, StatusBarKeyguardViewManager statusBarKeyguardViewManager, ActivityStarter activityStarter, ShadeController shadeController, CommandQueue commandQueue, ActionClickLogger clickLogger, @Main Executor executor)87 public StatusBarRemoteInputCallback( 88 Context context, 89 GroupExpansionManager groupExpansionManager, 90 NotificationLockscreenUserManager notificationLockscreenUserManager, 91 KeyguardStateController keyguardStateController, 92 StatusBarStateController statusBarStateController, 93 StatusBarKeyguardViewManager statusBarKeyguardViewManager, 94 ActivityStarter activityStarter, 95 ShadeController shadeController, 96 CommandQueue commandQueue, 97 ActionClickLogger clickLogger, 98 @Main Executor executor) { 99 mContext = context; 100 mStatusBarKeyguardViewManager = statusBarKeyguardViewManager; 101 mShadeController = shadeController; 102 mExecutor = executor; 103 mContext.registerReceiverAsUser(mChallengeReceiver, UserHandle.ALL, 104 new IntentFilter(ACTION_DEVICE_LOCKED_CHANGED), null, null); 105 mLockscreenUserManager = notificationLockscreenUserManager; 106 mKeyguardStateController = keyguardStateController; 107 mStatusBarStateController = (SysuiStatusBarStateController) statusBarStateController; 108 mActivityStarter = activityStarter; 109 mStatusBarStateController.addCallback(this); 110 mKeyguardManager = context.getSystemService(KeyguardManager.class); 111 mCommandQueue = commandQueue; 112 mCommandQueue.addCallback(this); 113 mActionClickLogger = clickLogger; 114 mActivityIntentHelper = new ActivityIntentHelper(mContext); 115 mGroupExpansionManager = groupExpansionManager; 116 } 117 118 @Override onStateChanged(int state)119 public void onStateChanged(int state) { 120 boolean hasPendingRemoteInput = mPendingRemoteInputView != null; 121 if (state == StatusBarState.SHADE 122 && (mStatusBarStateController.leaveOpenOnKeyguardHide() || hasPendingRemoteInput)) { 123 if (!mStatusBarStateController.isKeyguardRequested() 124 && mKeyguardStateController.isUnlocked()) { 125 if (hasPendingRemoteInput) { 126 mExecutor.execute(mPendingRemoteInputView::callOnClick); 127 } 128 mPendingRemoteInputView = null; 129 } 130 } 131 } 132 133 @Override onLockedRemoteInput(ExpandableNotificationRow row, View clicked)134 public void onLockedRemoteInput(ExpandableNotificationRow row, View clicked) { 135 if (!row.isPinned()) { 136 mStatusBarStateController.setLeaveOpenOnKeyguardHide(true); 137 } 138 mStatusBarKeyguardViewManager.showBouncer(true /* scrimmed */); 139 mPendingRemoteInputView = clicked; 140 } 141 onWorkChallengeChanged()142 protected void onWorkChallengeChanged() { 143 mLockscreenUserManager.updatePublicMode(); 144 if (mPendingWorkRemoteInputView != null 145 && !mLockscreenUserManager.isAnyProfilePublicMode()) { 146 // Expand notification panel and the notification row, then click on remote input view 147 final Runnable clickPendingViewRunnable = () -> { 148 final View pendingWorkRemoteInputView = mPendingWorkRemoteInputView; 149 if (pendingWorkRemoteInputView == null) { 150 return; 151 } 152 153 // Climb up the hierarchy until we get to the container for this row. 154 ViewParent p = pendingWorkRemoteInputView.getParent(); 155 while (!(p instanceof ExpandableNotificationRow)) { 156 if (p == null) { 157 return; 158 } 159 p = p.getParent(); 160 } 161 162 final ExpandableNotificationRow row = (ExpandableNotificationRow) p; 163 ViewParent viewParent = row.getParent(); 164 if (viewParent instanceof NotificationStackScrollLayout) { 165 final NotificationStackScrollLayout scrollLayout = 166 (NotificationStackScrollLayout) viewParent; 167 row.makeActionsVisibile(); 168 row.post(() -> { 169 final Runnable finishScrollingCallback = () -> { 170 mPendingWorkRemoteInputView.callOnClick(); 171 mPendingWorkRemoteInputView = null; 172 scrollLayout.setFinishScrollingCallback(null); 173 }; 174 if (scrollLayout.scrollTo(row)) { 175 // It scrolls! So call it when it's finished. 176 scrollLayout.setFinishScrollingCallback(finishScrollingCallback); 177 } else { 178 // It does not scroll, so call it now! 179 finishScrollingCallback.run(); 180 } 181 }); 182 } 183 }; 184 mShadeController.postOnShadeExpanded(clickPendingViewRunnable); 185 mShadeController.instantExpandShade(); 186 } 187 } 188 189 @Override onMakeExpandedVisibleForRemoteInput(ExpandableNotificationRow row, View clickedView, boolean deferBouncer, Runnable runnable)190 public void onMakeExpandedVisibleForRemoteInput(ExpandableNotificationRow row, 191 View clickedView, boolean deferBouncer, Runnable runnable) { 192 if (!deferBouncer && mKeyguardStateController.isShowing()) { 193 onLockedRemoteInput(row, clickedView); 194 } else { 195 if (row.isChildInGroup() && !row.areChildrenExpanded()) { 196 // The group isn't expanded, let's make sure it's visible! 197 mGroupExpansionManager.toggleGroupExpansion(row.getEntry()); 198 } 199 row.setUserExpanded(true); 200 row.getPrivateLayout().setOnExpandedVisibleListener(runnable); 201 } 202 } 203 204 @Override onLockedWorkRemoteInput(int userId, ExpandableNotificationRow row, View clicked)205 public void onLockedWorkRemoteInput(int userId, ExpandableNotificationRow row, 206 View clicked) { 207 // Collapse notification and show work challenge 208 mCommandQueue.animateCollapsePanels(); 209 startWorkChallengeIfNecessary(userId, null, null); 210 // Add pending remote input view after starting work challenge, as starting work challenge 211 // will clear all previous pending review view 212 mPendingWorkRemoteInputView = clicked; 213 } 214 startWorkChallengeIfNecessary(int userId, IntentSender intendSender, String notificationKey)215 boolean startWorkChallengeIfNecessary(int userId, IntentSender intendSender, 216 String notificationKey) { 217 // Clear pending remote view, as we do not want to trigger pending remote input view when 218 // it's called by other code 219 mPendingWorkRemoteInputView = null; 220 // Begin old BaseStatusBar.startWorkChallengeIfNecessary. 221 final Intent newIntent = mKeyguardManager.createConfirmDeviceCredentialIntent(null, 222 null, userId); 223 if (newIntent == null) { 224 return false; 225 } 226 final Intent callBackIntent = new Intent(NOTIFICATION_UNLOCKED_BY_WORK_CHALLENGE_ACTION); 227 callBackIntent.putExtra(Intent.EXTRA_INTENT, intendSender); 228 callBackIntent.putExtra(Intent.EXTRA_INDEX, notificationKey); 229 callBackIntent.setPackage(mContext.getPackageName()); 230 231 PendingIntent callBackPendingIntent = PendingIntent.getBroadcast( 232 mContext, 233 0, 234 callBackIntent, 235 PendingIntent.FLAG_CANCEL_CURRENT | 236 PendingIntent.FLAG_ONE_SHOT | 237 PendingIntent.FLAG_IMMUTABLE); 238 newIntent.putExtra( 239 Intent.EXTRA_INTENT, 240 callBackPendingIntent.getIntentSender()); 241 try { 242 ActivityManager.getService().startConfirmDeviceCredentialIntent(newIntent, 243 null /*options*/); 244 } catch (RemoteException ex) { 245 // ignore 246 } 247 return true; 248 // End old BaseStatusBar.startWorkChallengeIfNecessary. 249 } 250 251 @Override shouldHandleRemoteInput(View view, PendingIntent pendingIntent)252 public boolean shouldHandleRemoteInput(View view, PendingIntent pendingIntent) { 253 // Skip remote input as doing so will expand the notification shade. 254 return (mDisabled2 & StatusBarManager.DISABLE2_NOTIFICATION_SHADE) != 0; 255 } 256 257 @Override handleRemoteViewClick(View view, PendingIntent pendingIntent, boolean appRequestedAuth, @Nullable Integer actionIndex, NotificationRemoteInputManager.ClickHandler defaultHandler)258 public boolean handleRemoteViewClick(View view, PendingIntent pendingIntent, 259 boolean appRequestedAuth, @Nullable Integer actionIndex, 260 NotificationRemoteInputManager.ClickHandler defaultHandler) { 261 final boolean isActivity = pendingIntent.isActivity(); 262 if (isActivity || appRequestedAuth) { 263 mActionClickLogger.logWaitingToCloseKeyguard(pendingIntent, actionIndex); 264 final boolean afterKeyguardGone = mActivityIntentHelper 265 .wouldPendingLaunchResolverActivity(pendingIntent, 266 mLockscreenUserManager.getCurrentUserId()); 267 mActivityStarter.dismissKeyguardThenExecute(() -> { 268 mActionClickLogger.logKeyguardGone(pendingIntent, actionIndex); 269 270 try { 271 ActivityManager.getService().resumeAppSwitches(); 272 } catch (RemoteException e) { 273 } 274 275 boolean handled = defaultHandler.handleClick(); 276 277 // close the shade if it was open and maybe wait for activity start. 278 return handled && mShadeController.closeShadeIfOpen(); 279 }, null, afterKeyguardGone); 280 return true; 281 } else { 282 return defaultHandler.handleClick(); 283 } 284 } 285 286 @Override disable(int displayId, int state1, int state2, boolean animate)287 public void disable(int displayId, int state1, int state2, boolean animate) { 288 if (displayId == mContext.getDisplayId()) { 289 mDisabled2 = state2; 290 } 291 } 292 293 protected class ChallengeReceiver extends BroadcastReceiver { 294 @Override onReceive(Context context, Intent intent)295 public void onReceive(Context context, Intent intent) { 296 final String action = intent.getAction(); 297 final int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, UserHandle.USER_NULL); 298 if (Intent.ACTION_DEVICE_LOCKED_CHANGED.equals(action)) { 299 if (userId != mLockscreenUserManager.getCurrentUserId() 300 && mLockscreenUserManager.isCurrentProfile(userId)) { 301 onWorkChallengeChanged(); 302 } 303 } 304 } 305 }; 306 } 307