blob: 428de9e9adbb3372af23e636f099bff4c6189040 [file] [log] [blame]
/*
* 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 android.content.Intent.ACTION_DEVICE_LOCKED_CHANGED;
import static com.android.systemui.statusbar.NotificationLockscreenUserManager.NOTIFICATION_UNLOCKED_BY_WORK_CHALLENGE_ACTION;
import android.app.ActivityManager;
import android.app.KeyguardManager;
import android.app.PendingIntent;
import android.app.StatusBarManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.IntentSender;
import android.os.Handler;
import android.os.RemoteException;
import android.os.UserHandle;
import android.util.Log;
import android.view.View;
import android.view.ViewParent;
import com.android.systemui.ActivityIntentHelper;
import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.statusbar.CommandQueue.Callbacks;
import com.android.systemui.statusbar.NotificationLockscreenUserManager;
import com.android.systemui.statusbar.NotificationRemoteInputManager;
import com.android.systemui.statusbar.NotificationRemoteInputManager.Callback;
import com.android.systemui.statusbar.StatusBarState;
import com.android.systemui.statusbar.SysuiStatusBarStateController;
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout;
import com.android.systemui.statusbar.policy.KeyguardStateController;
import java.util.concurrent.atomic.AtomicReference;
import javax.inject.Inject;
import javax.inject.Singleton;
/**
*/
@Singleton
public class StatusBarRemoteInputCallback implements Callback, Callbacks,
StatusBarStateController.StateListener, KeyguardStateController.Callback {
private static final String TAG = StatusBarRemoteInputCallback.class.getSimpleName();
private final KeyguardStateController mKeyguardStateController;
private final SysuiStatusBarStateController mStatusBarStateController;
private final NotificationLockscreenUserManager mLockscreenUserManager;
private final ActivityStarter mActivityStarter;
private final Context mContext;
private final StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
private final ShadeController mShadeController;
private final ActivityIntentHelper mActivityIntentHelper;
private final NotificationGroupManager mGroupManager;
private View mPendingWorkRemoteInputView;
private View mPendingRemoteInputView;
private KeyguardManager mKeyguardManager;
private final CommandQueue mCommandQueue;
private int mDisabled2;
protected BroadcastReceiver mChallengeReceiver = new ChallengeReceiver();
private Handler mMainHandler = new Handler();
private final AtomicReference<Intent> mPendingConfirmCredentialIntent = new AtomicReference();
/**
*/
@Inject
public StatusBarRemoteInputCallback(Context context, NotificationGroupManager groupManager,
NotificationLockscreenUserManager notificationLockscreenUserManager,
KeyguardStateController keyguardStateController,
StatusBarStateController statusBarStateController,
StatusBarKeyguardViewManager statusBarKeyguardViewManager,
ActivityStarter activityStarter, ShadeController shadeController,
CommandQueue commandQueue) {
mContext = context;
mStatusBarKeyguardViewManager = statusBarKeyguardViewManager;
mShadeController = shadeController;
mContext.registerReceiverAsUser(mChallengeReceiver, UserHandle.ALL,
new IntentFilter(ACTION_DEVICE_LOCKED_CHANGED), null, null);
mLockscreenUserManager = notificationLockscreenUserManager;
mKeyguardStateController = keyguardStateController;
mStatusBarStateController = (SysuiStatusBarStateController) statusBarStateController;
mActivityStarter = activityStarter;
mStatusBarStateController.addCallback(this);
mKeyguardManager = context.getSystemService(KeyguardManager.class);
mCommandQueue = commandQueue;
mCommandQueue.addCallback(this);
mActivityIntentHelper = new ActivityIntentHelper(mContext);
mGroupManager = groupManager;
// Listen to onKeyguardShowingChanged in case a managed profile needs to be unlocked
// once the primary profile's keyguard is no longer shown.
mKeyguardStateController.addCallback(this);
}
@Override
public void onStateChanged(int state) {
boolean hasPendingRemoteInput = mPendingRemoteInputView != null;
if (state == StatusBarState.SHADE
&& (mStatusBarStateController.leaveOpenOnKeyguardHide() || hasPendingRemoteInput)) {
if (!mStatusBarStateController.isKeyguardRequested()) {
if (hasPendingRemoteInput) {
mMainHandler.post(mPendingRemoteInputView::callOnClick);
}
mPendingRemoteInputView = null;
}
}
}
@Override
public void onLockedRemoteInput(ExpandableNotificationRow row, View clicked) {
if (!row.isPinned()) {
mStatusBarStateController.setLeaveOpenOnKeyguardHide(true);
}
mStatusBarKeyguardViewManager.showBouncer(true /* scrimmed */);
mPendingRemoteInputView = clicked;
}
protected void onWorkChallengeChanged() {
mLockscreenUserManager.updatePublicMode();
if (mPendingWorkRemoteInputView != null
&& !mLockscreenUserManager.isAnyProfilePublicMode()) {
// Expand notification panel and the notification row, then click on remote input view
final Runnable clickPendingViewRunnable = () -> {
final View pendingWorkRemoteInputView = mPendingWorkRemoteInputView;
if (pendingWorkRemoteInputView == null) {
return;
}
// Climb up the hierarchy until we get to the container for this row.
ViewParent p = pendingWorkRemoteInputView.getParent();
while (!(p instanceof ExpandableNotificationRow)) {
if (p == null) {
return;
}
p = p.getParent();
}
final ExpandableNotificationRow row = (ExpandableNotificationRow) p;
ViewParent viewParent = row.getParent();
if (viewParent instanceof NotificationStackScrollLayout) {
final NotificationStackScrollLayout scrollLayout =
(NotificationStackScrollLayout) viewParent;
row.makeActionsVisibile();
row.post(() -> {
final Runnable finishScrollingCallback = () -> {
mPendingWorkRemoteInputView.callOnClick();
mPendingWorkRemoteInputView = null;
scrollLayout.setFinishScrollingCallback(null);
};
if (scrollLayout.scrollTo(row)) {
// It scrolls! So call it when it's finished.
scrollLayout.setFinishScrollingCallback(finishScrollingCallback);
} else {
// It does not scroll, so call it now!
finishScrollingCallback.run();
}
});
}
};
mShadeController.postOnShadeExpanded(clickPendingViewRunnable);
mShadeController.instantExpandNotificationsPanel();
}
}
@Override
public void onMakeExpandedVisibleForRemoteInput(ExpandableNotificationRow row,
View clickedView) {
if (mKeyguardStateController.isShowing()) {
onLockedRemoteInput(row, clickedView);
} else {
if (row.isChildInGroup() && !row.areChildrenExpanded()) {
// The group isn't expanded, let's make sure it's visible!
mGroupManager.toggleGroupExpansion(row.getEntry().getSbn());
}
row.setUserExpanded(true);
row.getPrivateLayout().setOnExpandedVisibleListener(clickedView::performClick);
}
}
@Override
public void onLockedWorkRemoteInput(int userId, ExpandableNotificationRow row,
View clicked) {
// Collapse notification and show work challenge
mCommandQueue.animateCollapsePanels();
startWorkChallengeIfNecessary(userId, null, null);
// Add pending remote input view after starting work challenge, as starting work challenge
// will clear all previous pending review view
mPendingWorkRemoteInputView = clicked;
}
boolean startWorkChallengeIfNecessary(int userId, IntentSender intendSender,
String notificationKey) {
// Clear pending remote view, as we do not want to trigger pending remote input view when
// it's called by other code
mPendingWorkRemoteInputView = null;
final Intent newIntent = createConfirmDeviceCredentialIntent(
userId, intendSender, notificationKey);
if (newIntent == null) {
Log.w(TAG, String.format("Cannot create intent to unlock user %d", userId));
return false;
}
mPendingConfirmCredentialIntent.set(newIntent);
// If the Keyguard is currently showing, starting the ConfirmDeviceCredentialActivity
// would cause it to pause, not letting the user actually unlock the managed profile.
// Instead, wait until we receive a callback indicating it is no longer showing and
// then start the pending intent.
if (mKeyguardStateController.isShowing()) {
// Do nothing, since the callback will get the pending intent and start it.
Log.w(TAG, String.format("Keyguard is showing, waiting until it's not"));
} else {
startPendingConfirmDeviceCredentialIntent();
}
return true;
}
private Intent createConfirmDeviceCredentialIntent(
int userId, IntentSender intendSender, String notificationKey) {
final Intent newIntent = mKeyguardManager.createConfirmDeviceCredentialIntent(null,
null, userId);
if (newIntent == null) {
return null;
}
final Intent callBackIntent = new Intent(NOTIFICATION_UNLOCKED_BY_WORK_CHALLENGE_ACTION);
callBackIntent.putExtra(Intent.EXTRA_INTENT, intendSender);
callBackIntent.putExtra(Intent.EXTRA_INDEX, notificationKey);
callBackIntent.setPackage(mContext.getPackageName());
PendingIntent callBackPendingIntent = PendingIntent.getBroadcast(
mContext,
0,
callBackIntent,
PendingIntent.FLAG_CANCEL_CURRENT |
PendingIntent.FLAG_ONE_SHOT |
PendingIntent.FLAG_IMMUTABLE);
newIntent.putExtra(
Intent.EXTRA_INTENT,
callBackPendingIntent.getIntentSender());
return newIntent;
}
private void startPendingConfirmDeviceCredentialIntent() {
final Intent pendingIntent = mPendingConfirmCredentialIntent.getAndSet(null);
if (pendingIntent == null) {
return;
}
try {
if (mKeyguardStateController.isShowing()) {
Log.w(TAG, "Keyguard is showing while starting confirm device credential intent.");
}
ActivityManager.getService().startConfirmDeviceCredentialIntent(pendingIntent,
null /*options*/);
} catch (RemoteException ex) {
// ignore
}
}
@Override
public void onKeyguardShowingChanged() {
if (mKeyguardStateController.isShowing()) {
// In order to avoid jarring UX where/ the managed profile challenge is shown and
// immediately dismissed, do not attempt to start the confirm device credential
// activity if the keyguard is still showing.
if (mPendingConfirmCredentialIntent.get() != null) {
Log.w(TAG, "There's a pending unlock intent but keyguard is still showing, abort.");
}
return;
}
startPendingConfirmDeviceCredentialIntent();
}
@Override
public boolean shouldHandleRemoteInput(View view, PendingIntent pendingIntent) {
// Skip remote input as doing so will expand the notification shade.
return (mDisabled2 & StatusBarManager.DISABLE2_NOTIFICATION_SHADE) != 0;
}
@Override
public boolean handleRemoteViewClick(View view, PendingIntent pendingIntent,
NotificationRemoteInputManager.ClickHandler defaultHandler) {
final boolean isActivity = pendingIntent.isActivity();
if (isActivity) {
final boolean afterKeyguardGone = mActivityIntentHelper.wouldLaunchResolverActivity(
pendingIntent.getIntent(), mLockscreenUserManager.getCurrentUserId());
mActivityStarter.dismissKeyguardThenExecute(() -> {
try {
ActivityManager.getService().resumeAppSwitches();
} catch (RemoteException e) {
}
boolean handled = defaultHandler.handleClick();
// close the shade if it was open and maybe wait for activity start.
return handled && mShadeController.closeShadeIfOpen();
}, null, afterKeyguardGone);
return true;
} else {
return defaultHandler.handleClick();
}
}
@Override
public void disable(int displayId, int state1, int state2, boolean animate) {
if (displayId == mContext.getDisplayId()) {
mDisabled2 = state2;
}
}
protected class ChallengeReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
final String action = intent.getAction();
final int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, UserHandle.USER_NULL);
if (Intent.ACTION_DEVICE_LOCKED_CHANGED.equals(action)) {
if (userId != mLockscreenUserManager.getCurrentUserId()
&& mLockscreenUserManager.isCurrentProfile(userId)) {
onWorkChallengeChanged();
}
}
}
};
}