Merge "Adding API for displaying a DO/PO controlled secondary lock screen post initial system lock screen."
diff --git a/api/current.txt b/api/current.txt
index b6e9a11..1be9926 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -6767,6 +6767,13 @@
method public final android.os.IBinder onBind(android.content.Intent);
}
+ public class DevicePolicyKeyguardService extends android.app.Service {
+ ctor public DevicePolicyKeyguardService();
+ method @Nullable public void dismiss();
+ method @Nullable public final android.os.IBinder onBind(@Nullable android.content.Intent);
+ method @Nullable public android.view.SurfaceControl onSurfaceReady(@Nullable android.os.IBinder);
+ }
+
public class DevicePolicyManager {
method public void addCrossProfileIntentFilter(@NonNull android.content.ComponentName, android.content.IntentFilter, int);
method public boolean addCrossProfileWidgetProvider(@NonNull android.content.ComponentName, String);
@@ -6979,6 +6986,7 @@
method public boolean setResetPasswordToken(android.content.ComponentName, byte[]);
method public void setRestrictionsProvider(@NonNull android.content.ComponentName, @Nullable android.content.ComponentName);
method public void setScreenCaptureDisabled(@NonNull android.content.ComponentName, boolean);
+ method public void setSecondaryLockscreenEnabled(@NonNull android.content.ComponentName, boolean);
method public void setSecureSetting(@NonNull android.content.ComponentName, String, String);
method public void setSecurityLoggingEnabled(@NonNull android.content.ComponentName, boolean);
method public void setShortSupportMessage(@NonNull android.content.ComponentName, @Nullable CharSequence);
@@ -7004,6 +7012,7 @@
field public static final String ACTION_ADD_DEVICE_ADMIN = "android.app.action.ADD_DEVICE_ADMIN";
field public static final String ACTION_ADMIN_POLICY_COMPLIANCE = "android.app.action.ADMIN_POLICY_COMPLIANCE";
field public static final String ACTION_APPLICATION_DELEGATION_SCOPES_CHANGED = "android.app.action.APPLICATION_DELEGATION_SCOPES_CHANGED";
+ field public static final String ACTION_BIND_SECONDARY_LOCKSCREEN_SERVICE = "android.app.action.BIND_SECONDARY_LOCKSCREEN_SERVICE";
field public static final String ACTION_DEVICE_ADMIN_SERVICE = "android.app.action.DEVICE_ADMIN_SERVICE";
field public static final String ACTION_DEVICE_OWNER_CHANGED = "android.app.action.DEVICE_OWNER_CHANGED";
field public static final String ACTION_GET_PROVISIONING_MODE = "android.app.action.GET_PROVISIONING_MODE";
diff --git a/api/system-current.txt b/api/system-current.txt
index d9ef009..52a26fd 100755
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -845,6 +845,7 @@
method @RequiresPermission(android.Manifest.permission.MANAGE_USERS) public boolean isDeviceProvisioned();
method @RequiresPermission(android.Manifest.permission.MANAGE_USERS) public boolean isDeviceProvisioningConfigApplied();
method @RequiresPermission(android.Manifest.permission.MANAGE_USERS) public boolean isManagedKiosk();
+ method public boolean isSecondaryLockscreenEnabled(int);
method @RequiresPermission(android.Manifest.permission.MANAGE_USERS) public boolean isUnattendedManagedKiosk();
method @RequiresPermission("android.permission.NOTIFY_PENDING_SYSTEM_UPDATE") public void notifyPendingSystemUpdate(long);
method @RequiresPermission("android.permission.NOTIFY_PENDING_SYSTEM_UPDATE") public void notifyPendingSystemUpdate(long, boolean);
diff --git a/core/java/android/app/admin/DevicePolicyKeyguardService.java b/core/java/android/app/admin/DevicePolicyKeyguardService.java
new file mode 100644
index 0000000..c2a76c5
--- /dev/null
+++ b/core/java/android/app/admin/DevicePolicyKeyguardService.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2020 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 android.app.admin;
+
+import android.annotation.Nullable;
+import android.app.Service;
+import android.content.Intent;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.Log;
+import android.view.SurfaceControl;
+
+/**
+ * Client interface for providing the SystemUI with secondary lockscreen information.
+ *
+ * <p>An implementation must be provided by the device admin app when
+ * {@link DevicePolicyManager#setSecondaryLockscreenEnabled} is set to true and the service must be
+ * declared in the manifest as handling the action
+ * {@link DevicePolicyManager#ACTION_BIND_SECONDARY_LOCKSCREEN_SERVICE}, otherwise the keyguard
+ * will fail to bind to the service and continue to unlock.
+ *
+ * @see DevicePolicyManager#setSecondaryLockscreenEnabled
+ */
+public class DevicePolicyKeyguardService extends Service {
+ private static final String TAG = "DevicePolicyKeyguardService";
+ private IKeyguardCallback mCallback;
+
+ private final IKeyguardClient mClient = new IKeyguardClient.Stub() {
+ @Override
+ public void onSurfaceReady(@Nullable IBinder hostInputToken, IKeyguardCallback callback) {
+ mCallback = callback;
+ SurfaceControl surfaceControl =
+ DevicePolicyKeyguardService.this.onSurfaceReady(hostInputToken);
+
+ if (mCallback != null) {
+ try {
+ mCallback.onSurfaceControlCreated(surfaceControl);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to return created SurfaceControl", e);
+ }
+ }
+ }
+ };
+
+ @Override
+ @Nullable
+ public final IBinder onBind(@Nullable Intent intent) {
+ return mClient.asBinder();
+ }
+
+ /**
+ * Called by keyguard once the host surface for the secondary lockscreen is ready to display
+ * remote content.
+ * @return the {@link SurfaceControl} for the Surface the secondary lockscreen content is
+ * attached to.
+ */
+ @Nullable
+ public SurfaceControl onSurfaceReady(@Nullable IBinder hostInputToken) {
+ return null;
+ }
+
+ /**
+ * Signals to keyguard that the secondary lock screen is ready to be dismissed.
+ */
+ @Nullable
+ public void dismiss() {
+ try {
+ mCallback.onDismiss();
+ } catch (RemoteException e) {
+ Log.e(TAG, "onDismiss failed", e);
+ }
+ }
+}
diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java
index a35a899..d58c4eb 100644
--- a/core/java/android/app/admin/DevicePolicyManager.java
+++ b/core/java/android/app/admin/DevicePolicyManager.java
@@ -2384,6 +2384,13 @@
public static final int MAX_PASSWORD_LENGTH = 16;
/**
+ * Service Action: Service implemented by a device owner or profile owner to provide a
+ * secondary lockscreen.
+ */
+ public static final String ACTION_BIND_SECONDARY_LOCKSCREEN_SERVICE =
+ "android.app.action.BIND_SECONDARY_LOCKSCREEN_SERVICE";
+
+ /**
* Return true if the given administrator component is currently active (enabled) in the system.
*
* @param admin The administrator component to check for.
@@ -8393,6 +8400,52 @@
}
/**
+ * Called by device owner or profile owner to set whether a secondary lockscreen needs to be
+ * shown.
+ *
+ * <p>The secondary lockscreen will by displayed after the primary keyguard security screen
+ * requirements are met. To provide the lockscreen content the DO/PO will need to provide a
+ * service handling the {@link #ACTION_BIND_SECONDARY_LOCKSCREEN_SERVICE} intent action,
+ * extending the {@link DevicePolicyKeyguardService} class.
+ *
+ * <p>Relevant interactions on the secondary lockscreen should be communicated back to the
+ * keyguard via {@link IKeyguardCallback}, such as when the screen is ready to be dismissed.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
+ * @param enabled Whether or not the lockscreen needs to be shown.
+ * @throws SecurityException if {@code admin} is not a device or profile owner.
+ * @see #isSecondaryLockscreenEnabled
+ **/
+ public void setSecondaryLockscreenEnabled(@NonNull ComponentName admin, boolean enabled) {
+ throwIfParentInstance("setSecondaryLockscreenEnabled");
+ if (mService != null) {
+ try {
+ mService.setSecondaryLockscreenEnabled(admin, enabled);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * Returns whether the secondary lock screen needs to be shown.
+ * @see #setSecondaryLockscreenEnabled
+ * @hide
+ */
+ @SystemApi
+ public boolean isSecondaryLockscreenEnabled(int userId) {
+ throwIfParentInstance("isSecondaryLockscreenEnabled");
+ if (mService != null) {
+ try {
+ return mService.isSecondaryLockscreenEnabled(userId);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return false;
+ }
+
+ /**
* Sets which packages may enter lock task mode.
* <p>
* Any packages that share uid with an allowed package will also be allowed to activate lock
diff --git a/core/java/android/app/admin/IDevicePolicyManager.aidl b/core/java/android/app/admin/IDevicePolicyManager.aidl
index a2c0856..e7667c0 100644
--- a/core/java/android/app/admin/IDevicePolicyManager.aidl
+++ b/core/java/android/app/admin/IDevicePolicyManager.aidl
@@ -252,6 +252,9 @@
String[] getAccountTypesWithManagementDisabled();
String[] getAccountTypesWithManagementDisabledAsUser(int userId);
+ void setSecondaryLockscreenEnabled(in ComponentName who, boolean enabled);
+ boolean isSecondaryLockscreenEnabled(int userId);
+
void setLockTaskPackages(in ComponentName who, in String[] packages);
String[] getLockTaskPackages(in ComponentName who);
boolean isLockTaskPermitted(in String pkg);
diff --git a/core/java/android/app/admin/IKeyguardCallback.aidl b/core/java/android/app/admin/IKeyguardCallback.aidl
new file mode 100644
index 0000000..81e7d4d
--- /dev/null
+++ b/core/java/android/app/admin/IKeyguardCallback.aidl
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2011 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 android.app.admin;
+
+import android.view.SurfaceControl;
+
+/**
+ * Internal IPC interface for informing the keyguard of events on the secondary lockscreen.
+ * @hide
+ */
+interface IKeyguardCallback {
+ oneway void onSurfaceControlCreated(in SurfaceControl remoteSurfaceControl);
+ oneway void onDismiss();
+}
diff --git a/core/java/android/app/admin/IKeyguardClient.aidl b/core/java/android/app/admin/IKeyguardClient.aidl
new file mode 100644
index 0000000..4bfd990
--- /dev/null
+++ b/core/java/android/app/admin/IKeyguardClient.aidl
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2011 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 android.app.admin;
+
+import android.app.admin.IKeyguardCallback;
+
+/**
+ * Internal IPC interface for a service to provide the SystemUI with secondary lockscreen
+ * information.
+ * @hide
+ */
+interface IKeyguardClient {
+ oneway void onSurfaceReady(in IBinder hostInputToken, in IKeyguardCallback keyguardCallback);
+}
diff --git a/packages/SystemUI/src/com/android/keyguard/AdminSecondaryLockScreenController.java b/packages/SystemUI/src/com/android/keyguard/AdminSecondaryLockScreenController.java
new file mode 100644
index 0000000..2f8ef2d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/keyguard/AdminSecondaryLockScreenController.java
@@ -0,0 +1,216 @@
+/*
+ * Copyright (C) 2020 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.keyguard;
+
+import android.annotation.Nullable;
+import android.app.admin.IKeyguardCallback;
+import android.app.admin.IKeyguardClient;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.util.Log;
+import android.view.SurfaceControl;
+import android.view.SurfaceHolder;
+import android.view.SurfaceView;
+import android.view.ViewGroup;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+/**
+ * Encapsulates all logic for secondary lockscreen state management.
+ */
+public class AdminSecondaryLockScreenController {
+ private static final String TAG = "AdminSecondaryLockScreenController";
+ private static final int REMOTE_CONTENT_READY_TIMEOUT_MILLIS = 500;
+ private final KeyguardUpdateMonitor mUpdateMonitor;
+ private final Context mContext;
+ private final ViewGroup mParent;
+ private AdminSecurityView mView;
+ private Handler mHandler;
+ private IKeyguardClient mClient;
+ private KeyguardSecurityCallback mKeyguardCallback;
+ private SurfaceControl.Transaction mTransaction;
+
+ private final ServiceConnection mConnection = new ServiceConnection() {
+ @Override
+ public void onServiceConnected(ComponentName className, IBinder service) {
+ mClient = IKeyguardClient.Stub.asInterface(service);
+ if (mView.isAttachedToWindow() && mClient != null) {
+ onSurfaceReady();
+
+ try {
+ service.linkToDeath(mKeyguardClientDeathRecipient, 0);
+ } catch (RemoteException e) {
+ // Failed to link to death, just dismiss and unbind the service for now.
+ Log.e(TAG, "Lost connection to secondary lockscreen service", e);
+ dismiss(KeyguardUpdateMonitor.getCurrentUser());
+ }
+ }
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName className) {
+ mClient = null;
+ }
+ };
+
+ private final IBinder.DeathRecipient mKeyguardClientDeathRecipient = () -> {
+ hide(); // hide also takes care of unlinking to death.
+ Log.d(TAG, "KeyguardClient service died");
+ };
+
+ private final IKeyguardCallback mCallback = new IKeyguardCallback.Stub() {
+ @Override
+ public void onDismiss() {
+ dismiss(UserHandle.getCallingUserId());
+ }
+
+ @Override
+ public void onSurfaceControlCreated(@Nullable SurfaceControl remoteSurfaceControl) {
+ if (mHandler != null) {
+ mHandler.removeCallbacksAndMessages(null);
+ }
+ if (remoteSurfaceControl != null) {
+ mTransaction.reparent(remoteSurfaceControl, mView.getSurfaceControl())
+ .apply();
+ } else {
+ dismiss(KeyguardUpdateMonitor.getCurrentUser());
+ }
+ }
+ };
+
+ private final KeyguardUpdateMonitorCallback mUpdateCallback =
+ new KeyguardUpdateMonitorCallback() {
+ @Override
+ public void onSecondaryLockscreenRequirementChanged(int userId) {
+ Intent newIntent = mUpdateMonitor.getSecondaryLockscreenRequirement(userId);
+ if (newIntent == null) {
+ dismiss(userId);
+ }
+ }
+ };
+
+ @VisibleForTesting
+ protected SurfaceHolder.Callback mSurfaceHolderCallback = new SurfaceHolder.Callback() {
+ @Override
+ public void surfaceCreated(SurfaceHolder holder) {
+ final int userId = KeyguardUpdateMonitor.getCurrentUser();
+ mUpdateMonitor.registerCallback(mUpdateCallback);
+
+ if (mClient != null) {
+ onSurfaceReady();
+ }
+ mHandler.postDelayed(
+ () -> {
+ // If the remote content is not readied within the timeout period,
+ // move on without the secondary lockscreen.
+ dismiss(userId);
+ },
+ REMOTE_CONTENT_READY_TIMEOUT_MILLIS);
+ }
+
+ @Override
+ public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {}
+
+ @Override
+ public void surfaceDestroyed(SurfaceHolder holder) {
+ mUpdateMonitor.removeCallback(mUpdateCallback);
+ }
+ };
+
+ public AdminSecondaryLockScreenController(Context context, ViewGroup parent,
+ KeyguardUpdateMonitor updateMonitor, KeyguardSecurityCallback callback,
+ Handler handler, SurfaceControl.Transaction transaction) {
+ mContext = context;
+ mHandler = handler;
+ mParent = parent;
+ mTransaction = transaction;
+ mUpdateMonitor = updateMonitor;
+ mKeyguardCallback = callback;
+ mView = new AdminSecurityView(mContext, mSurfaceHolderCallback);
+ }
+
+ /**
+ * Displays the Admin security Surface view.
+ */
+ public void show(Intent serviceIntent) {
+ mContext.bindService(serviceIntent, mConnection, Context.BIND_AUTO_CREATE);
+ mParent.addView(mView);
+ }
+
+ /**
+ * Hides the Admin security Surface view.
+ */
+ public void hide() {
+ if (mView.isAttachedToWindow()) {
+ mParent.removeView(mView);
+ }
+ if (mClient != null) {
+ mClient.asBinder().unlinkToDeath(mKeyguardClientDeathRecipient, 0);
+ mContext.unbindService(mConnection);
+ mClient = null;
+ }
+ }
+
+ private void onSurfaceReady() {
+ try {
+ mClient.onSurfaceReady(mView.getInputToken(), mCallback);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error in onSurfaceReady", e);
+ dismiss(KeyguardUpdateMonitor.getCurrentUser());
+ }
+ }
+
+ private void dismiss(int userId) {
+ mHandler.removeCallbacksAndMessages(null);
+ if (mView != null && mView.isAttachedToWindow()
+ && userId == KeyguardUpdateMonitor.getCurrentUser()) {
+ hide();
+ mKeyguardCallback.dismiss(true, userId);
+ }
+ }
+
+ /**
+ * Custom {@link SurfaceView} used to allow a device admin to present an additional security
+ * screen.
+ */
+ private class AdminSecurityView extends SurfaceView {
+ private SurfaceHolder.Callback mSurfaceHolderCallback;
+
+ AdminSecurityView(Context context, SurfaceHolder.Callback surfaceHolderCallback) {
+ super(context);
+ mSurfaceHolderCallback = surfaceHolderCallback;
+ setZOrderOnTop(true);
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ getHolder().addCallback(mSurfaceHolderCallback);
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ getHolder().removeCallback(mSurfaceHolderCallback);
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
index 9ae446e..ae78726 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
@@ -21,9 +21,12 @@
import android.app.AlertDialog;
import android.app.admin.DevicePolicyManager;
import android.content.Context;
+import android.content.Intent;
import android.content.res.ColorStateList;
import android.graphics.Rect;
import android.metrics.LogMaker;
+import android.os.Handler;
+import android.os.Looper;
import android.os.UserHandle;
import android.util.AttributeSet;
import android.util.Log;
@@ -31,6 +34,7 @@
import android.util.TypedValue;
import android.view.LayoutInflater;
import android.view.MotionEvent;
+import android.view.SurfaceControl;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;
@@ -90,6 +94,7 @@
private AlertDialog mAlertDialog;
private InjectionInflationController mInjectionInflationController;
private boolean mSwipeUpToRetry;
+ private AdminSecondaryLockScreenController mSecondaryLockScreenController;
private final ViewConfiguration mViewConfiguration;
private final SpringAnimation mSpringAnimation;
@@ -137,6 +142,9 @@
SystemUIFactory.getInstance().getRootComponent());
mViewConfiguration = ViewConfiguration.get(context);
mKeyguardStateController = Dependency.get(KeyguardStateController.class);
+ mSecondaryLockScreenController = new AdminSecondaryLockScreenController(context, this,
+ mUpdateMonitor, mCallback, new Handler(Looper.myLooper()),
+ new SurfaceControl.Transaction());
}
public void setSecurityCallback(SecurityCallback callback) {
@@ -157,6 +165,7 @@
mAlertDialog.dismiss();
mAlertDialog = null;
}
+ mSecondaryLockScreenController.hide();
if (mCurrentSecuritySelection != SecurityMode.None) {
getSecurityView(mCurrentSecuritySelection).onPause();
}
@@ -532,6 +541,15 @@
break;
}
}
+ // Check for device admin specified additional security measures.
+ if (finish) {
+ Intent secondaryLockscreenIntent =
+ mUpdateMonitor.getSecondaryLockscreenRequirement(targetUserId);
+ if (secondaryLockscreenIntent != null) {
+ mSecondaryLockScreenController.show(secondaryLockscreenIntent);
+ return false;
+ }
+ }
if (eventSubtype != -1) {
mMetricsLogger.write(new LogMaker(MetricsEvent.BOUNCER)
.setType(MetricsEvent.TYPE_DISMISS).setSubtype(eventSubtype));
@@ -751,6 +769,5 @@
public void showUsabilityHint() {
mSecurityViewFlipper.showUsabilityHint();
}
-
}
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
index 65fc215..f03648a 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
@@ -113,6 +113,7 @@
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
+import java.util.Map;
import java.util.Map.Entry;
import java.util.TimeZone;
import java.util.function.Consumer;
@@ -334,6 +335,7 @@
private SparseBooleanArray mUserFingerprintAuthenticated = new SparseBooleanArray();
private SparseBooleanArray mUserFaceAuthenticated = new SparseBooleanArray();
private SparseBooleanArray mUserFaceUnlockRunning = new SparseBooleanArray();
+ private Map<Integer, Intent> mSecondaryLockscreenRequirement = new HashMap<Integer, Intent>();
private static int sCurrentUser;
private Runnable mUpdateBiometricListeningState = this::updateBiometricListeningState;
@@ -928,6 +930,45 @@
return mUserTrustIsManaged.get(userId) && !isTrustDisabled(userId);
}
+ private void updateSecondaryLockscreenRequirement(int userId) {
+ Intent oldIntent = mSecondaryLockscreenRequirement.get(userId);
+ boolean enabled = mDevicePolicyManager.isSecondaryLockscreenEnabled(userId);
+ boolean changed = false;
+
+ if (enabled && (oldIntent == null)) {
+ ResolveInfo resolveInfo =
+ mContext.getPackageManager().resolveService(
+ new Intent(
+ DevicePolicyManager.ACTION_BIND_SECONDARY_LOCKSCREEN_SERVICE),
+ 0);
+ if (resolveInfo != null) {
+ Intent newIntent = new Intent();
+ newIntent.setComponent(resolveInfo.serviceInfo.getComponentName());
+ mSecondaryLockscreenRequirement.put(userId, newIntent);
+ changed = true;
+ }
+ } else if (!enabled && (oldIntent != null)) {
+ mSecondaryLockscreenRequirement.put(userId, null);
+ changed = true;
+ }
+ if (changed) {
+ for (int i = 0; i < mCallbacks.size(); i++) {
+ KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get();
+ if (cb != null) {
+ cb.onSecondaryLockscreenRequirementChanged(userId);
+ }
+ }
+ }
+ }
+
+ /**
+ * Returns an Intent by which to bind to a service that will provide additional security screen
+ * content that must be shown prior to dismissing the keyguard for this user.
+ */
+ public Intent getSecondaryLockscreenRequirement(int userId) {
+ return mSecondaryLockscreenRequirement.get(userId);
+ }
+
/**
* Cached version of {@link TrustManager#isTrustUsuallyManaged(int)}.
*/
@@ -1113,7 +1154,8 @@
getSendingUserId()));
} else if (DevicePolicyManager.ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED
.equals(action)) {
- mHandler.sendEmptyMessage(MSG_DPM_STATE_CHANGED);
+ mHandler.sendMessage(mHandler.obtainMessage(MSG_DPM_STATE_CHANGED,
+ getSendingUserId()));
} else if (ACTION_USER_UNLOCKED.equals(action)) {
mHandler.sendMessage(mHandler.obtainMessage(MSG_USER_UNLOCKED,
getSendingUserId(), 0));
@@ -1530,7 +1572,7 @@
handleDeviceProvisioned();
break;
case MSG_DPM_STATE_CHANGED:
- handleDevicePolicyManagerStateChanged();
+ handleDevicePolicyManagerStateChanged(msg.arg1);
break;
case MSG_USER_SWITCHING:
handleUserSwitching(msg.arg1, (IRemoteCallback) msg.obj);
@@ -1706,6 +1748,7 @@
mUserIsUnlocked.put(user, mUserManager.isUserUnlocked(user));
mDevicePolicyManager = context.getSystemService(DevicePolicyManager.class);
mLogoutEnabled = mDevicePolicyManager.isLogoutEnabled();
+ updateSecondaryLockscreenRequirement(user);
List<UserInfo> allUsers = mUserManager.getUsers();
for (UserInfo userInfo : allUsers) {
mUserTrustIsUsuallyManaged.put(userInfo.id,
@@ -2046,9 +2089,10 @@
/**
* Handle {@link #MSG_DPM_STATE_CHANGED}
*/
- private void handleDevicePolicyManagerStateChanged() {
+ private void handleDevicePolicyManagerStateChanged(int userId) {
checkIsHandlerThread();
updateFingerprintListeningState();
+ updateSecondaryLockscreenRequirement(userId);
for (int i = 0; i < mCallbacks.size(); i++) {
KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get();
if (cb != null) {
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java
index 04502f0..8e87b7a 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java
@@ -310,4 +310,9 @@
*/
public void onBiometricsCleared() { }
+ /**
+ * Called when the secondary lock screen requirement changes.
+ */
+ public void onSecondaryLockscreenRequirementChanged(int userId) { }
+
}
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/AdminSecondaryLockScreenControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/AdminSecondaryLockScreenControllerTest.java
new file mode 100644
index 0000000..1954b39
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/keyguard/AdminSecondaryLockScreenControllerTest.java
@@ -0,0 +1,207 @@
+/*
+ * Copyright (C) 2020 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.keyguard;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.AdditionalAnswers.answerVoid;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.admin.IKeyguardCallback;
+import android.app.admin.IKeyguardClient;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.os.Handler;
+import android.os.RemoteException;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+import android.testing.TestableLooper.RunWithLooper;
+import android.testing.ViewUtils;
+import android.view.SurfaceControl;
+import android.view.SurfaceView;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.systemui.SysuiTestCase;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.Spy;
+
+@RunWithLooper
+@RunWith(AndroidTestingRunner.class)
+@SmallTest
+public class AdminSecondaryLockScreenControllerTest extends SysuiTestCase {
+
+ private static final int TARGET_USER_ID = KeyguardUpdateMonitor.getCurrentUser();
+
+ private AdminSecondaryLockScreenController mTestController;
+ private ComponentName mComponentName;
+ private Intent mServiceIntent;
+ private TestableLooper mTestableLooper;
+ private ViewGroup mParent;
+
+ @Mock
+ private Handler mHandler;
+ @Mock
+ private IKeyguardClient.Stub mKeyguardClient;
+ @Mock
+ private KeyguardSecurityCallback mKeyguardCallback;
+ @Mock
+ private KeyguardUpdateMonitor mUpdateMonitor;
+ @Spy
+ private StubTransaction mTransaction;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+
+ mParent = spy(new FrameLayout(mContext));
+ ViewUtils.attachView(mParent);
+
+ mTestableLooper = TestableLooper.get(this);
+ mComponentName = new ComponentName(mContext, "FakeKeyguardClient.class");
+ mServiceIntent = new Intent().setComponent(mComponentName);
+
+ mContext.addMockService(mComponentName, mKeyguardClient);
+ // Have Stub.asInterface return the mocked interface.
+ when(mKeyguardClient.queryLocalInterface(anyString())).thenReturn(mKeyguardClient);
+ when(mKeyguardClient.asBinder()).thenReturn(mKeyguardClient);
+
+ mTestController = new AdminSecondaryLockScreenController(
+ mContext, mParent, mUpdateMonitor, mKeyguardCallback, mHandler, mTransaction);
+ }
+
+ @Test
+ public void testShow() throws Exception {
+ doAnswer(invocation -> {
+ IKeyguardCallback callback = (IKeyguardCallback) invocation.getArguments()[1];
+ callback.onSurfaceControlCreated(new SurfaceControl());
+ return null;
+ }).when(mKeyguardClient).onSurfaceReady(any(), any(IKeyguardCallback.class));
+
+ mTestController.show(mServiceIntent);
+
+ verifySurfaceReady();
+ verify(mTransaction).reparent(any(), any());
+ assertThat(mContext.isBound(mComponentName)).isTrue();
+ }
+
+ @Test
+ public void testShow_dismissedByCallback() throws Exception {
+ doAnswer(invocation -> {
+ IKeyguardCallback callback = (IKeyguardCallback) invocation.getArguments()[1];
+ callback.onDismiss();
+ return null;
+ }).when(mKeyguardClient).onSurfaceReady(any(), any(IKeyguardCallback.class));
+
+ mTestController.show(mServiceIntent);
+
+ verifyViewDismissed(verifySurfaceReady());
+ }
+
+ @Test
+ public void testHide() throws Exception {
+ // Show the view first, then hide.
+ doAnswer(invocation -> {
+ IKeyguardCallback callback = (IKeyguardCallback) invocation.getArguments()[1];
+ callback.onSurfaceControlCreated(new SurfaceControl());
+ return null;
+ }).when(mKeyguardClient).onSurfaceReady(any(), any(IKeyguardCallback.class));
+
+ mTestController.show(mServiceIntent);
+ SurfaceView v = verifySurfaceReady();
+
+ mTestController.hide();
+ verify(mParent).removeView(v);
+ assertThat(mContext.isBound(mComponentName)).isFalse();
+ }
+
+ @Test
+ public void testHide_notShown() throws Exception {
+ mTestController.hide();
+ // Nothing should happen if trying to hide when the view isn't attached yet.
+ verify(mParent, never()).removeView(any(SurfaceView.class));
+ }
+
+ @Test
+ public void testDismissed_onSurfaceReady_RemoteException() throws Exception {
+ doThrow(new RemoteException()).when(mKeyguardClient)
+ .onSurfaceReady(any(), any(IKeyguardCallback.class));
+
+ mTestController.show(mServiceIntent);
+
+ verifyViewDismissed(verifySurfaceReady());
+ }
+
+ @Test
+ public void testDismissed_onSurfaceReady_timeout() throws Exception {
+ // Mocked KeyguardClient never handles the onSurfaceReady, so the operation times out,
+ // resulting in the view being dismissed.
+ doAnswer(answerVoid(Runnable::run)).when(mHandler)
+ .postDelayed(any(Runnable.class), anyLong());
+
+ mTestController.show(mServiceIntent);
+
+ verifyViewDismissed(verifySurfaceReady());
+ }
+
+ private SurfaceView verifySurfaceReady() throws Exception {
+ mTestableLooper.processAllMessages();
+ ArgumentCaptor<SurfaceView> captor = ArgumentCaptor.forClass(SurfaceView.class);
+ verify(mParent).addView(captor.capture());
+
+ mTestableLooper.processAllMessages();
+ verify(mKeyguardClient).onSurfaceReady(any(), any(IKeyguardCallback.class));
+ return captor.getValue();
+ }
+
+ private void verifyViewDismissed(SurfaceView v) throws Exception {
+ verify(mParent).removeView(v);
+ verify(mKeyguardCallback).dismiss(true, TARGET_USER_ID);
+ assertThat(mContext.isBound(mComponentName)).isFalse();
+ }
+
+ /**
+ * Stubbed {@link SurfaceControl.Transaction} class that can be used when unit testing to
+ * avoid calls to native code.
+ */
+ private class StubTransaction extends SurfaceControl.Transaction {
+ @Override
+ public void apply() {
+ }
+
+ @Override
+ public SurfaceControl.Transaction reparent(SurfaceControl sc, SurfaceControl newParent) {
+ return this;
+ }
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
index 12da006..b3c2ba3 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
@@ -34,12 +34,16 @@
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import android.app.Activity;
import android.app.admin.DevicePolicyManager;
import android.app.trust.TrustManager;
+import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.pm.ServiceInfo;
import android.hardware.biometrics.BiometricManager;
import android.hardware.biometrics.BiometricSourceType;
import android.hardware.biometrics.IBiometricEnabledOnKeyguardCallback;
@@ -519,6 +523,52 @@
assertThat(mKeyguardUpdateMonitor.isTrustUsuallyManaged(user)).isFalse();
}
+ @Test
+ public void testSecondaryLockscreenRequirement() {
+ int user = KeyguardUpdateMonitor.getCurrentUser();
+ String packageName = "fake.test.package";
+ String cls = "FakeService";
+ ServiceInfo serviceInfo = new ServiceInfo();
+ serviceInfo.packageName = packageName;
+ serviceInfo.name = cls;
+ ResolveInfo resolveInfo = new ResolveInfo();
+ resolveInfo.serviceInfo = serviceInfo;
+ when(mPackageManager.resolveService(any(Intent.class), eq(0))).thenReturn(resolveInfo);
+ when(mDevicePolicyManager.isSecondaryLockscreenEnabled(eq(user))).thenReturn(true, false);
+
+ // Initially null.
+ assertThat(mKeyguardUpdateMonitor.getSecondaryLockscreenRequirement(user)).isNull();
+
+ // Set non-null after DPM change.
+ setBroadcastReceiverPendingResult(mKeyguardUpdateMonitor.mBroadcastAllReceiver);
+ Intent intent = new Intent(DevicePolicyManager.ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED);
+ mKeyguardUpdateMonitor.mBroadcastAllReceiver.onReceive(getContext(), intent);
+ mTestableLooper.processAllMessages();
+
+ Intent storedIntent = mKeyguardUpdateMonitor.getSecondaryLockscreenRequirement(user);
+ assertThat(storedIntent.getComponent().getClassName()).isEqualTo(cls);
+ assertThat(storedIntent.getComponent().getPackageName()).isEqualTo(packageName);
+
+ // Back to null after another DPM change.
+ mKeyguardUpdateMonitor.mBroadcastAllReceiver.onReceive(getContext(), intent);
+ mTestableLooper.processAllMessages();
+ assertThat(mKeyguardUpdateMonitor.getSecondaryLockscreenRequirement(user)).isNull();
+ }
+
+ private void setBroadcastReceiverPendingResult(BroadcastReceiver receiver) {
+ BroadcastReceiver.PendingResult pendingResult =
+ new BroadcastReceiver.PendingResult(Activity.RESULT_OK,
+ "resultData",
+ /* resultExtras= */ null,
+ BroadcastReceiver.PendingResult.TYPE_UNREGISTERED,
+ /* ordered= */ true,
+ /* sticky= */ false,
+ /* token= */ null,
+ UserHandle.myUserId(),
+ /* flags= */ 0);
+ receiver.setPendingResult(pendingResult);
+ }
+
private Intent putPhoneInfo(Intent intent, Bundle data, Boolean simInited) {
int subscription = simInited
? 1/* mock subid=1 */ : SubscriptionManager.DUMMY_SUBSCRIPTION_ID_BASE;
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index e939d84..0c79a6f 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -370,6 +370,8 @@
private static final String TAG_PROTECTED_PACKAGES = "protected-packages";
+ private static final String TAG_SECONDARY_LOCK_SCREEN = "secondary-lock-screen";
+
private static final int REQUEST_EXPIRE_PASSWORD = 5571;
private static final long MS_PER_DAY = TimeUnit.DAYS.toMillis(1);
@@ -771,6 +773,8 @@
boolean mCurrentInputMethodSet = false;
+ boolean mSecondaryLockscreenEnabled = false;
+
// TODO(b/35385311): Keep track of metadata in TrustedCertificateStore instead.
Set<String> mOwnerInstalledCaCerts = new ArraySet<>();
@@ -3322,6 +3326,12 @@
out.endTag(null, TAG_LOCK_TASK_FEATURES);
}
+ if (policy.mSecondaryLockscreenEnabled) {
+ out.startTag(null, TAG_SECONDARY_LOCK_SCREEN);
+ out.attribute(null, ATTR_VALUE, Boolean.toString(true));
+ out.endTag(null, TAG_SECONDARY_LOCK_SCREEN);
+ }
+
if (policy.mStatusBarDisabled) {
out.startTag(null, TAG_STATUS_BAR);
out.attribute(null, ATTR_DISABLED, Boolean.toString(policy.mStatusBarDisabled));
@@ -3571,6 +3581,9 @@
} else if (TAG_LOCK_TASK_FEATURES.equals(tag)) {
policy.mLockTaskFeatures = Integer.parseInt(
parser.getAttributeValue(null, ATTR_VALUE));
+ } else if (TAG_SECONDARY_LOCK_SCREEN.equals(tag)) {
+ policy.mSecondaryLockscreenEnabled = Boolean.parseBoolean(
+ parser.getAttributeValue(null, ATTR_VALUE));
} else if (TAG_STATUS_BAR.equals(tag)) {
policy.mStatusBarDisabled = Boolean.parseBoolean(
parser.getAttributeValue(null, ATTR_DISABLED));
@@ -8601,6 +8614,7 @@
// Clear delegations.
policy.mDelegationMap.clear();
policy.mStatusBarDisabled = false;
+ policy.mSecondaryLockscreenEnabled = false;
policy.mUserProvisioningState = DevicePolicyManager.STATE_USER_UNMANAGED;
policy.mAffiliationIds.clear();
policy.mLockTaskPackages.clear();
@@ -11154,6 +11168,33 @@
}
@Override
+ public void setSecondaryLockscreenEnabled(ComponentName who, boolean enabled) {
+ enforceCanSetSecondaryLockscreenEnabled(who);
+ synchronized (getLockObject()) {
+ final int userId = mInjector.userHandleGetCallingUserId();
+ DevicePolicyData policy = getUserData(userId);
+ policy.mSecondaryLockscreenEnabled = enabled;
+ saveSettingsLocked(userId);
+ }
+ }
+
+ @Override
+ public boolean isSecondaryLockscreenEnabled(int userId) {
+ synchronized (getLockObject()) {
+ return getUserData(userId).mSecondaryLockscreenEnabled;
+ }
+ }
+
+ private void enforceCanSetSecondaryLockscreenEnabled(ComponentName who) {
+ enforceProfileOrDeviceOwner(who);
+ final int userId = mInjector.userHandleGetCallingUserId();
+ if (isManagedProfile(userId)) {
+ throw new SecurityException(
+ "User " + userId + " is not allowed to call setSecondaryLockscreenEnabled");
+ }
+ }
+
+ @Override
public void setLockTaskPackages(ComponentName who, String[] packages)
throws SecurityException {
Objects.requireNonNull(who, "ComponentName is null");
diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java
index aeba488..8f1d0f7 100644
--- a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java
@@ -4189,6 +4189,52 @@
() -> dpm.setLockTaskFeatures(admin1, flags));
}
+ public void testSecondaryLockscreen_profileOwner() throws Exception {
+ mContext.binder.callingUid = DpmMockContext.CALLER_UID;
+
+ // Initial state is disabled.
+ assertFalse(dpm.isSecondaryLockscreenEnabled(DpmMockContext.CALLER_USER_HANDLE));
+
+ // Profile owner can set enabled state.
+ setAsProfileOwner(admin1);
+ dpm.setSecondaryLockscreenEnabled(admin1, true);
+ assertTrue(dpm.isSecondaryLockscreenEnabled(DpmMockContext.CALLER_USER_HANDLE));
+
+ // Managed profile managed by different package is unaffiliated - cannot set enabled.
+ final int managedProfileUserId = 15;
+ final int managedProfileAdminUid = UserHandle.getUid(managedProfileUserId, 20456);
+ final ComponentName adminDifferentPackage =
+ new ComponentName("another.package", "whatever.class");
+ addManagedProfile(adminDifferentPackage, managedProfileAdminUid, admin2);
+ mContext.binder.callingUid = managedProfileAdminUid;
+ assertExpectException(SecurityException.class, /* messageRegex= */ null,
+ () -> dpm.setSecondaryLockscreenEnabled(adminDifferentPackage, false));
+ }
+
+ public void testSecondaryLockscreen_deviceOwner() throws Exception {
+ mContext.binder.callingUid = DpmMockContext.CALLER_SYSTEM_USER_UID;
+
+ // Initial state is disabled.
+ assertFalse(dpm.isSecondaryLockscreenEnabled(UserHandle.USER_SYSTEM));
+
+ // Device owners can set enabled state.
+ setupDeviceOwner();
+ dpm.setSecondaryLockscreenEnabled(admin1, true);
+ assertTrue(dpm.isSecondaryLockscreenEnabled(UserHandle.USER_SYSTEM));
+ }
+
+ public void testSecondaryLockscreen_nonOwner() throws Exception {
+ mContext.binder.callingUid = DpmMockContext.CALLER_UID;
+
+ // Initial state is disabled.
+ assertFalse(dpm.isSecondaryLockscreenEnabled(DpmMockContext.CALLER_USER_HANDLE));
+
+ // Non-DO/PO cannot set enabled state.
+ assertExpectException(SecurityException.class, /* messageRegex= */ null,
+ () -> dpm.setSecondaryLockscreenEnabled(admin1, true));
+ assertFalse(dpm.isSecondaryLockscreenEnabled(DpmMockContext.CALLER_USER_HANDLE));
+ }
+
public void testIsDeviceManaged() throws Exception {
mContext.binder.callingUid = DpmMockContext.CALLER_SYSTEM_USER_UID;
setupDeviceOwner();