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