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.compose.animation.scene.ObservableTransitionState; 38 import com.android.systemui.ActivityIntentHelper; 39 import com.android.systemui.dagger.SysUISingleton; 40 import com.android.systemui.dagger.qualifiers.Main; 41 import com.android.systemui.deviceentry.domain.interactor.DeviceUnlockedInteractor; 42 import com.android.systemui.plugins.ActivityStarter; 43 import com.android.systemui.plugins.statusbar.StatusBarStateController; 44 import com.android.systemui.scene.domain.interactor.SceneInteractor; 45 import com.android.systemui.scene.shared.flag.SceneContainerFlag; 46 import com.android.systemui.shade.ShadeController; 47 import com.android.systemui.statusbar.ActionClickLogger; 48 import com.android.systemui.statusbar.CommandQueue; 49 import com.android.systemui.statusbar.CommandQueue.Callbacks; 50 import com.android.systemui.statusbar.NotificationLockscreenUserManager; 51 import com.android.systemui.statusbar.NotificationRemoteInputManager; 52 import com.android.systemui.statusbar.NotificationRemoteInputManager.Callback; 53 import com.android.systemui.statusbar.StatusBarState; 54 import com.android.systemui.statusbar.SysuiStatusBarStateController; 55 import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManager; 56 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; 57 import com.android.systemui.statusbar.notification.shared.NotificationBundleUi; 58 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout; 59 import com.android.systemui.statusbar.policy.KeyguardStateController; 60 import com.android.systemui.util.kotlin.JavaAdapter; 61 62 import dagger.Lazy; 63 64 import java.util.concurrent.Executor; 65 66 import javax.inject.Inject; 67 68 /** 69 */ 70 @SysUISingleton 71 public class StatusBarRemoteInputCallback implements Callback, Callbacks, 72 StatusBarStateController.StateListener { 73 74 private final KeyguardStateController mKeyguardStateController; 75 private final SysuiStatusBarStateController mStatusBarStateController; 76 private final NotificationLockscreenUserManager mLockscreenUserManager; 77 private final ActivityStarter mActivityStarter; 78 private final Context mContext; 79 private final StatusBarKeyguardViewManager mStatusBarKeyguardViewManager; 80 private final com.android.systemui.shade.ShadeController mShadeController; 81 private Executor mExecutor; 82 private final ActivityIntentHelper mActivityIntentHelper; 83 private final GroupExpansionManager mGroupExpansionManager; 84 private View mPendingWorkRemoteInputView; 85 private View mPendingRemoteInputView; 86 private KeyguardManager mKeyguardManager; 87 private final CommandQueue mCommandQueue; 88 private final ActionClickLogger mActionClickLogger; 89 private int mDisabled2; 90 protected BroadcastReceiver mChallengeReceiver = new ChallengeReceiver(); 91 private final Lazy<DeviceUnlockedInteractor> mDeviceUnlockedInteractorLazy; 92 private final Lazy<SceneInteractor> mSceneInteractorLazy; 93 94 /** 95 */ 96 @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, Lazy<DeviceUnlockedInteractor> deviceUnlockedInteractorLazy, Lazy<SceneInteractor> sceneInteractorLazy, JavaAdapter javaAdapter)97 public StatusBarRemoteInputCallback( 98 Context context, 99 GroupExpansionManager groupExpansionManager, 100 NotificationLockscreenUserManager notificationLockscreenUserManager, 101 KeyguardStateController keyguardStateController, 102 StatusBarStateController statusBarStateController, 103 StatusBarKeyguardViewManager statusBarKeyguardViewManager, 104 ActivityStarter activityStarter, 105 ShadeController shadeController, 106 CommandQueue commandQueue, 107 ActionClickLogger clickLogger, 108 @Main Executor executor, 109 Lazy<DeviceUnlockedInteractor> deviceUnlockedInteractorLazy, 110 Lazy<SceneInteractor> sceneInteractorLazy, 111 JavaAdapter javaAdapter) { 112 mContext = context; 113 mStatusBarKeyguardViewManager = statusBarKeyguardViewManager; 114 mShadeController = shadeController; 115 mExecutor = executor; 116 mContext.registerReceiverAsUser(mChallengeReceiver, UserHandle.ALL, 117 new IntentFilter(ACTION_DEVICE_LOCKED_CHANGED), null, null); 118 mLockscreenUserManager = notificationLockscreenUserManager; 119 mKeyguardStateController = keyguardStateController; 120 mStatusBarStateController = (SysuiStatusBarStateController) statusBarStateController; 121 mActivityStarter = activityStarter; 122 mStatusBarStateController.addCallback(this); 123 mKeyguardManager = context.getSystemService(KeyguardManager.class); 124 mCommandQueue = commandQueue; 125 mCommandQueue.addCallback(this); 126 mActionClickLogger = clickLogger; 127 mActivityIntentHelper = new ActivityIntentHelper(mContext); 128 mGroupExpansionManager = groupExpansionManager; 129 mDeviceUnlockedInteractorLazy = deviceUnlockedInteractorLazy; 130 mSceneInteractorLazy = sceneInteractorLazy; 131 132 if (SceneContainerFlag.isEnabled()) { 133 javaAdapter.alwaysCollectFlow( 134 mDeviceUnlockedInteractorLazy.get().getDeviceUnlockStatus(), 135 deviceUnlockStatus -> onStateChanged(mStatusBarStateController.getState())); 136 javaAdapter.alwaysCollectFlow( 137 mSceneInteractorLazy.get().getTransitionState(), 138 deviceUnlockStatus -> onStateChanged(mStatusBarStateController.getState())); 139 } 140 } 141 142 @Override onStateChanged(int state)143 public void onStateChanged(int state) { 144 if (mPendingRemoteInputView == null) { 145 return; 146 } 147 148 if (state == StatusBarState.SHADE && canRetryPendingRemoteInput()) { 149 mExecutor.execute(mPendingRemoteInputView::callOnClick); 150 mPendingRemoteInputView = null; 151 } 152 } 153 154 @Override onLockedRemoteInput(ExpandableNotificationRow row, View clicked)155 public void onLockedRemoteInput(ExpandableNotificationRow row, View clicked) { 156 if (!row.isPinned()) { 157 mStatusBarStateController.setLeaveOpenOnKeyguardHide(true); 158 } 159 mStatusBarKeyguardViewManager.showBouncer(true /* scrimmed */, 160 "StatusBarRemoteInputCallback#onLockedRemoteInput"); 161 mPendingRemoteInputView = clicked; 162 } 163 onWorkChallengeChanged()164 protected void onWorkChallengeChanged() { 165 mLockscreenUserManager.updatePublicMode(); 166 if (mPendingWorkRemoteInputView != null 167 && !mLockscreenUserManager.isAnyProfilePublicMode()) { 168 // Expand notification panel and the notification row, then click on remote input view 169 final Runnable clickPendingViewRunnable = () -> { 170 final View pendingWorkRemoteInputView = mPendingWorkRemoteInputView; 171 if (pendingWorkRemoteInputView == null) { 172 return; 173 } 174 175 // Climb up the hierarchy until we get to the container for this row. 176 ViewParent p = pendingWorkRemoteInputView.getParent(); 177 while (!(p instanceof ExpandableNotificationRow)) { 178 if (p == null) { 179 return; 180 } 181 p = p.getParent(); 182 } 183 184 final ExpandableNotificationRow row = (ExpandableNotificationRow) p; 185 ViewParent viewParent = row.getParent(); 186 if (viewParent instanceof NotificationStackScrollLayout) { 187 final NotificationStackScrollLayout scrollLayout = 188 (NotificationStackScrollLayout) viewParent; 189 row.makeActionsVisibile(); 190 row.post(() -> { 191 final Runnable finishScrollingCallback = () -> { 192 mPendingWorkRemoteInputView.callOnClick(); 193 mPendingWorkRemoteInputView = null; 194 scrollLayout.setFinishScrollingCallback(null); 195 }; 196 if (scrollLayout.scrollTo(row)) { 197 // It scrolls! So call it when it's finished. 198 scrollLayout.setFinishScrollingCallback(finishScrollingCallback); 199 } else { 200 // It does not scroll, so call it now! 201 finishScrollingCallback.run(); 202 } 203 }); 204 } 205 }; 206 mShadeController.postOnShadeExpanded(clickPendingViewRunnable); 207 mShadeController.instantExpandShade(); 208 } 209 } 210 211 @Override onMakeExpandedVisibleForRemoteInput(ExpandableNotificationRow row, View clickedView, boolean deferBouncer, Runnable runnable)212 public void onMakeExpandedVisibleForRemoteInput(ExpandableNotificationRow row, 213 View clickedView, boolean deferBouncer, Runnable runnable) { 214 if (!deferBouncer && mKeyguardStateController.isShowing()) { 215 onLockedRemoteInput(row, clickedView); 216 } else { 217 if (ExpandHeadsUpOnInlineReply.isEnabled()) { 218 if (row.isChildInGroup() && !row.areChildrenExpanded()) { 219 // The group isn't expanded, let's make sure it's visible! 220 if (NotificationBundleUi.isEnabled()) { 221 mGroupExpansionManager.toggleGroupExpansion(row.getEntryAdapter()); 222 } else { 223 mGroupExpansionManager.toggleGroupExpansion(row.getEntryLegacy()); 224 } 225 } else if (!row.isChildInGroup()) { 226 final boolean expandNotification; 227 if (row.isPinned()) { 228 expandNotification = !row.isPinnedAndExpanded(); 229 } else { 230 expandNotification = !row.isExpanded(); 231 } 232 233 if (expandNotification) { 234 // notification isn't expanded, let's make sure it's expanded! 235 row.toggleExpansionState(); 236 row.getPrivateLayout().setOnExpandedVisibleListener(runnable); 237 } 238 } 239 } else { 240 if (row.isChildInGroup() && !row.areChildrenExpanded()) { 241 // The group isn't expanded, let's make sure it's visible! 242 if (NotificationBundleUi.isEnabled()) { 243 mGroupExpansionManager.toggleGroupExpansion(row.getEntryAdapter()); 244 } else { 245 mGroupExpansionManager.toggleGroupExpansion(row.getEntryLegacy()); 246 } 247 } 248 249 if (android.app.Flags.compactHeadsUpNotificationReply() 250 && row.isCompactConversationHeadsUpOnScreen()) { 251 // Notification can be system expanded true and it is set user expanded in 252 // activateRemoteInput. notifyHeightChanged also doesn't work as visibleType 253 // doesn't change. To expand huning notification properly, 254 // we need set userExpanded false. 255 if (!row.isPinned() && row.isExpanded()) { 256 row.setUserExpanded(false); 257 } 258 // expand notification emits expanded information to HUN listener. 259 row.expandNotification(); 260 } else { 261 // TODO(b/346976443) Group and normal notification expansions are two different 262 // concepts. We should never call setUserExpanded for expanding groups. 263 264 // Note: Since Normal HUN has remote input view in it, we don't expect to hit 265 // onMakeExpandedVisibleForRemoteInput from activateRemoteInput for Normal HUN. 266 row.setUserExpanded(true); 267 } 268 row.getPrivateLayout().setOnExpandedVisibleListener(runnable); 269 } 270 } 271 } 272 273 @Override onLockedWorkRemoteInput(int userId, ExpandableNotificationRow row, View clicked)274 public void onLockedWorkRemoteInput(int userId, ExpandableNotificationRow row, 275 View clicked) { 276 // Collapse notification and show work challenge 277 mCommandQueue.animateCollapsePanels(); 278 startWorkChallengeIfNecessary(userId, null, null); 279 // Add pending remote input view after starting work challenge, as starting work challenge 280 // will clear all previous pending review view 281 mPendingWorkRemoteInputView = clicked; 282 } 283 startWorkChallengeIfNecessary(int userId, IntentSender intendSender, String notificationKey)284 boolean startWorkChallengeIfNecessary(int userId, IntentSender intendSender, 285 String notificationKey) { 286 // Clear pending remote view, as we do not want to trigger pending remote input view when 287 // it's called by other code 288 mPendingWorkRemoteInputView = null; 289 // Begin old BaseStatusBar.startWorkChallengeIfNecessary. 290 final Intent newIntent = mKeyguardManager.createConfirmDeviceCredentialIntent(null, 291 null, userId); 292 if (newIntent == null) { 293 return false; 294 } 295 final Intent callBackIntent = new Intent(NOTIFICATION_UNLOCKED_BY_WORK_CHALLENGE_ACTION); 296 callBackIntent.putExtra(Intent.EXTRA_INTENT, intendSender); 297 callBackIntent.putExtra(Intent.EXTRA_INDEX, notificationKey); 298 callBackIntent.setPackage(mContext.getPackageName()); 299 300 PendingIntent callBackPendingIntent = PendingIntent.getBroadcast( 301 mContext, 302 0, 303 callBackIntent, 304 PendingIntent.FLAG_CANCEL_CURRENT | 305 PendingIntent.FLAG_ONE_SHOT | 306 PendingIntent.FLAG_IMMUTABLE); 307 newIntent.putExtra( 308 Intent.EXTRA_INTENT, 309 callBackPendingIntent.getIntentSender()); 310 try { 311 ActivityManager.getService().startConfirmDeviceCredentialIntent(newIntent, 312 null /*options*/); 313 } catch (RemoteException ex) { 314 // ignore 315 } 316 return true; 317 // End old BaseStatusBar.startWorkChallengeIfNecessary. 318 } 319 320 @Override shouldHandleRemoteInput(View view, PendingIntent pendingIntent)321 public boolean shouldHandleRemoteInput(View view, PendingIntent pendingIntent) { 322 // Skip remote input as doing so will expand the notification shade. 323 return (mDisabled2 & StatusBarManager.DISABLE2_NOTIFICATION_SHADE) != 0; 324 } 325 326 @Override handleRemoteViewClick(View view, PendingIntent pendingIntent, boolean appRequestedAuth, @Nullable Integer actionIndex, NotificationRemoteInputManager.ClickHandler defaultHandler)327 public boolean handleRemoteViewClick(View view, PendingIntent pendingIntent, 328 boolean appRequestedAuth, @Nullable Integer actionIndex, 329 NotificationRemoteInputManager.ClickHandler defaultHandler) { 330 final boolean isActivity = pendingIntent.isActivity(); 331 if (isActivity || appRequestedAuth) { 332 mActionClickLogger.logWaitingToCloseKeyguard(pendingIntent, actionIndex); 333 final boolean afterKeyguardGone = mActivityIntentHelper 334 .wouldPendingLaunchResolverActivity(pendingIntent, 335 mLockscreenUserManager.getCurrentUserId()); 336 mActivityStarter.dismissKeyguardThenExecute(() -> { 337 mActionClickLogger.logKeyguardGone(pendingIntent, actionIndex); 338 339 try { 340 ActivityManager.getService().resumeAppSwitches(); 341 } catch (RemoteException e) { 342 } 343 344 boolean handled = defaultHandler.handleClick(); 345 346 // close the shade if it was open and maybe wait for activity start. 347 return handled && mShadeController.closeShadeIfOpen(); 348 }, null, afterKeyguardGone); 349 return true; 350 } else { 351 return defaultHandler.handleClick(); 352 } 353 } 354 355 @Override disable(int displayId, int state1, int state2, boolean animate)356 public void disable(int displayId, int state1, int state2, boolean animate) { 357 if (displayId == mContext.getDisplayId()) { 358 mDisabled2 = state2; 359 } 360 } 361 362 /** 363 * Returns {@code true} if it is safe to retry a pending remote input. The exact criteria for 364 * this vary depending whether the scene container is enabled. 365 */ canRetryPendingRemoteInput()366 private boolean canRetryPendingRemoteInput() { 367 if (SceneContainerFlag.isEnabled()) { 368 final boolean isUnlocked = mDeviceUnlockedInteractorLazy.get() 369 .getDeviceUnlockStatus().getValue().isUnlocked(); 370 final boolean isIdle = mSceneInteractorLazy.get() 371 .getTransitionState().getValue() instanceof ObservableTransitionState.Idle; 372 return isUnlocked && isIdle; 373 } else { 374 return mKeyguardStateController.isUnlocked() 375 && !mStatusBarStateController.isKeyguardRequested(); 376 } 377 } 378 379 protected class ChallengeReceiver extends BroadcastReceiver { 380 @Override onReceive(Context context, Intent intent)381 public void onReceive(Context context, Intent intent) { 382 final String action = intent.getAction(); 383 final int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, UserHandle.USER_NULL); 384 if (Intent.ACTION_DEVICE_LOCKED_CHANGED.equals(action)) { 385 if (userId != mLockscreenUserManager.getCurrentUserId() 386 && mLockscreenUserManager.isCurrentProfile(userId)) { 387 onWorkChallengeChanged(); 388 } 389 } 390 } 391 }; 392 } 393