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.SysUiServiceProvider.getComponent;
20 import static com.android.systemui.statusbar.NotificationLockscreenUserManager.NOTIFICATION_UNLOCKED_BY_WORK_CHALLENGE_ACTION;
21 
22 import android.app.ActivityManager;
23 import android.app.KeyguardManager;
24 import android.app.PendingIntent;
25 import android.app.StatusBarManager;
26 import android.content.BroadcastReceiver;
27 import android.content.Context;
28 import android.content.Intent;
29 import android.content.IntentFilter;
30 import android.content.IntentSender;
31 import android.os.Handler;
32 import android.os.RemoteException;
33 import android.os.UserHandle;
34 import android.view.View;
35 import android.view.ViewParent;
36 
37 import com.android.systemui.ActivityIntentHelper;
38 import com.android.systemui.Dependency;
39 import com.android.systemui.plugins.ActivityStarter;
40 import com.android.systemui.plugins.statusbar.StatusBarStateController;
41 import com.android.systemui.statusbar.CommandQueue;
42 import com.android.systemui.statusbar.CommandQueue.Callbacks;
43 import com.android.systemui.statusbar.NotificationLockscreenUserManager;
44 import com.android.systemui.statusbar.NotificationRemoteInputManager;
45 import com.android.systemui.statusbar.NotificationRemoteInputManager.Callback;
46 import com.android.systemui.statusbar.StatusBarState;
47 import com.android.systemui.statusbar.SysuiStatusBarStateController;
48 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
49 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout;
50 import com.android.systemui.statusbar.policy.KeyguardMonitor;
51 import com.android.systemui.statusbar.policy.RemoteInputView;
52 
53 import javax.inject.Inject;
54 import javax.inject.Singleton;
55 
56 /**
57  */
58 @Singleton
59 public class StatusBarRemoteInputCallback implements Callback, Callbacks,
60         StatusBarStateController.StateListener {
61 
62     private final KeyguardMonitor mKeyguardMonitor = Dependency.get(KeyguardMonitor.class);
63     private final SysuiStatusBarStateController mStatusBarStateController =
64             (SysuiStatusBarStateController) Dependency.get(StatusBarStateController.class);
65     private final NotificationLockscreenUserManager mLockscreenUserManager =
66             Dependency.get(NotificationLockscreenUserManager.class);
67     private final ActivityStarter mActivityStarter = Dependency.get(ActivityStarter.class);
68     private final Context mContext;
69     private final ActivityIntentHelper mActivityIntentHelper;
70     private final NotificationGroupManager mGroupManager;
71     private View mPendingWorkRemoteInputView;
72     private View mPendingRemoteInputView;
73     private final ShadeController mShadeController = Dependency.get(ShadeController.class);
74     private KeyguardManager mKeyguardManager;
75     private final CommandQueue mCommandQueue;
76     private int mDisabled2;
77     protected BroadcastReceiver mChallengeReceiver = new ChallengeReceiver();
78     private Handler mMainHandler = new Handler();
79 
80     /**
81      */
82     @Inject
StatusBarRemoteInputCallback(Context context, NotificationGroupManager groupManager)83     public StatusBarRemoteInputCallback(Context context, NotificationGroupManager groupManager) {
84         mContext = context;
85         mContext.registerReceiverAsUser(mChallengeReceiver, UserHandle.ALL,
86                 new IntentFilter(ACTION_DEVICE_LOCKED_CHANGED), null, null);
87         mStatusBarStateController.addCallback(this);
88         mKeyguardManager = context.getSystemService(KeyguardManager.class);
89         mCommandQueue = getComponent(context, CommandQueue.class);
90         mCommandQueue.addCallback(this);
91         mActivityIntentHelper = new ActivityIntentHelper(mContext);
92         mGroupManager = groupManager;
93     }
94 
95     @Override
onStateChanged(int state)96     public void onStateChanged(int state) {
97         boolean hasPendingRemoteInput = mPendingRemoteInputView != null;
98         if (state == StatusBarState.SHADE
99                 && (mStatusBarStateController.leaveOpenOnKeyguardHide() || hasPendingRemoteInput)) {
100             if (!mStatusBarStateController.isKeyguardRequested()) {
101                 if (hasPendingRemoteInput) {
102                     mMainHandler.post(mPendingRemoteInputView::callOnClick);
103                 }
104                 mPendingRemoteInputView = null;
105             }
106         }
107     }
108 
109     @Override
onLockedRemoteInput(ExpandableNotificationRow row, View clicked)110     public void onLockedRemoteInput(ExpandableNotificationRow row, View clicked) {
111         if (!row.isPinned()) {
112             mStatusBarStateController.setLeaveOpenOnKeyguardHide(true);
113         }
114         mShadeController.showBouncer(true /* scrimmed */);
115         mPendingRemoteInputView = clicked;
116     }
117 
onWorkChallengeChanged()118     protected void onWorkChallengeChanged() {
119         mLockscreenUserManager.updatePublicMode();
120         if (mPendingWorkRemoteInputView != null
121                 && !mLockscreenUserManager.isAnyProfilePublicMode()) {
122             // Expand notification panel and the notification row, then click on remote input view
123             final Runnable clickPendingViewRunnable = () -> {
124                 final View pendingWorkRemoteInputView = mPendingWorkRemoteInputView;
125                 if (pendingWorkRemoteInputView == null) {
126                     return;
127                 }
128 
129                 // Climb up the hierarchy until we get to the container for this row.
130                 ViewParent p = pendingWorkRemoteInputView.getParent();
131                 while (!(p instanceof ExpandableNotificationRow)) {
132                     if (p == null) {
133                         return;
134                     }
135                     p = p.getParent();
136                 }
137 
138                 final ExpandableNotificationRow row = (ExpandableNotificationRow) p;
139                 ViewParent viewParent = row.getParent();
140                 if (viewParent instanceof NotificationStackScrollLayout) {
141                     final NotificationStackScrollLayout scrollLayout =
142                             (NotificationStackScrollLayout) viewParent;
143                     row.makeActionsVisibile();
144                     row.post(() -> {
145                         final Runnable finishScrollingCallback = () -> {
146                             mPendingWorkRemoteInputView.callOnClick();
147                             mPendingWorkRemoteInputView = null;
148                             scrollLayout.setFinishScrollingCallback(null);
149                         };
150                         if (scrollLayout.scrollTo(row)) {
151                             // It scrolls! So call it when it's finished.
152                             scrollLayout.setFinishScrollingCallback(finishScrollingCallback);
153                         } else {
154                             // It does not scroll, so call it now!
155                             finishScrollingCallback.run();
156                         }
157                     });
158                 }
159             };
160             mShadeController.postOnShadeExpanded(clickPendingViewRunnable);
161             mShadeController.instantExpandNotificationsPanel();
162         }
163     }
164 
165     @Override
onMakeExpandedVisibleForRemoteInput(ExpandableNotificationRow row, View clickedView)166     public void onMakeExpandedVisibleForRemoteInput(ExpandableNotificationRow row,
167             View clickedView) {
168         if (mKeyguardMonitor.isShowing()) {
169             onLockedRemoteInput(row, clickedView);
170         } else {
171             if (row.isChildInGroup() && !row.areChildrenExpanded()) {
172                 // The group isn't expanded, let's make sure it's visible!
173                 mGroupManager.toggleGroupExpansion(row.getStatusBarNotification());
174             }
175             row.setUserExpanded(true);
176             row.getPrivateLayout().setOnExpandedVisibleListener(clickedView::performClick);
177         }
178     }
179 
180     @Override
onLockedWorkRemoteInput(int userId, ExpandableNotificationRow row, View clicked)181     public void onLockedWorkRemoteInput(int userId, ExpandableNotificationRow row,
182             View clicked) {
183         // Collapse notification and show work challenge
184         mCommandQueue.animateCollapsePanels();
185         startWorkChallengeIfNecessary(userId, null, null);
186         // Add pending remote input view after starting work challenge, as starting work challenge
187         // will clear all previous pending review view
188         mPendingWorkRemoteInputView = clicked;
189     }
190 
startWorkChallengeIfNecessary(int userId, IntentSender intendSender, String notificationKey)191     boolean startWorkChallengeIfNecessary(int userId, IntentSender intendSender,
192             String notificationKey) {
193         // Clear pending remote view, as we do not want to trigger pending remote input view when
194         // it's called by other code
195         mPendingWorkRemoteInputView = null;
196         // Begin old BaseStatusBar.startWorkChallengeIfNecessary.
197         final Intent newIntent = mKeyguardManager.createConfirmDeviceCredentialIntent(null,
198                 null, userId);
199         if (newIntent == null) {
200             return false;
201         }
202         final Intent callBackIntent = new Intent(NOTIFICATION_UNLOCKED_BY_WORK_CHALLENGE_ACTION);
203         callBackIntent.putExtra(Intent.EXTRA_INTENT, intendSender);
204         callBackIntent.putExtra(Intent.EXTRA_INDEX, notificationKey);
205         callBackIntent.setPackage(mContext.getPackageName());
206 
207         PendingIntent callBackPendingIntent = PendingIntent.getBroadcast(
208                 mContext,
209                 0,
210                 callBackIntent,
211                 PendingIntent.FLAG_CANCEL_CURRENT |
212                         PendingIntent.FLAG_ONE_SHOT |
213                         PendingIntent.FLAG_IMMUTABLE);
214         newIntent.putExtra(
215                 Intent.EXTRA_INTENT,
216                 callBackPendingIntent.getIntentSender());
217         try {
218             ActivityManager.getService().startConfirmDeviceCredentialIntent(newIntent,
219                     null /*options*/);
220         } catch (RemoteException ex) {
221             // ignore
222         }
223         return true;
224         // End old BaseStatusBar.startWorkChallengeIfNecessary.
225     }
226 
227     @Override
shouldHandleRemoteInput(View view, PendingIntent pendingIntent)228     public boolean shouldHandleRemoteInput(View view, PendingIntent pendingIntent) {
229         // Skip remote input as doing so will expand the notification shade.
230         return (mDisabled2 & StatusBarManager.DISABLE2_NOTIFICATION_SHADE) != 0;
231     }
232 
233     @Override
handleRemoteViewClick(View view, PendingIntent pendingIntent, NotificationRemoteInputManager.ClickHandler defaultHandler)234     public boolean handleRemoteViewClick(View view, PendingIntent pendingIntent,
235             NotificationRemoteInputManager.ClickHandler defaultHandler) {
236         final boolean isActivity = pendingIntent.isActivity();
237         if (isActivity) {
238             final boolean afterKeyguardGone = mActivityIntentHelper.wouldLaunchResolverActivity(
239                     pendingIntent.getIntent(), mLockscreenUserManager.getCurrentUserId());
240             mActivityStarter.dismissKeyguardThenExecute(() -> {
241                 try {
242                     ActivityManager.getService().resumeAppSwitches();
243                 } catch (RemoteException e) {
244                 }
245 
246                 boolean handled = defaultHandler.handleClick();
247 
248                 // close the shade if it was open and maybe wait for activity start.
249                 return handled && mShadeController.closeShadeIfOpen();
250             }, null, afterKeyguardGone);
251             return true;
252         } else {
253             return defaultHandler.handleClick();
254         }
255     }
256 
257     @Override
disable(int displayId, int state1, int state2, boolean animate)258     public void disable(int displayId, int state1, int state2, boolean animate) {
259         if (displayId == mContext.getDisplayId()) {
260             mDisabled2 = state2;
261         }
262     }
263 
264     protected class ChallengeReceiver extends BroadcastReceiver {
265         @Override
onReceive(Context context, Intent intent)266         public void onReceive(Context context, Intent intent) {
267             final String action = intent.getAction();
268             final int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, UserHandle.USER_NULL);
269             if (Intent.ACTION_DEVICE_LOCKED_CHANGED.equals(action)) {
270                 if (userId != mLockscreenUserManager.getCurrentUserId()
271                         && mLockscreenUserManager.isCurrentProfile(userId)) {
272                     onWorkChallengeChanged();
273                 }
274             }
275         }
276     };
277 }
278