diff options
15 files changed, 837 insertions, 4 deletions
diff --git a/api/current.txt b/api/current.txt index be0936ac5fb4..6f1055026635 100644 --- a/api/current.txt +++ b/api/current.txt @@ -6767,6 +6767,13 @@ package android.app.admin { 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 @@ package android.app.admin { 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 @@ package android.app.admin { 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 66b779d51669..ac989ac4e4b4 100755 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -845,6 +845,7 @@ package android.app.admin { 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 000000000000..c2a76c5014c0 --- /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 a35a89948f25..d58c4eb1eb3d 100644 --- a/core/java/android/app/admin/DevicePolicyManager.java +++ b/core/java/android/app/admin/DevicePolicyManager.java @@ -2384,6 +2384,13 @@ public class DevicePolicyManager { 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 @@ public class DevicePolicyManager { } /** + * 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 a2c0856717f5..e7667c0a1b4a 100644 --- a/core/java/android/app/admin/IDevicePolicyManager.aidl +++ b/core/java/android/app/admin/IDevicePolicyManager.aidl @@ -252,6 +252,9 @@ interface IDevicePolicyManager { 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 000000000000..81e7d4dee902 --- /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 000000000000..4bfd990cf717 --- /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 000000000000..2f8ef2dc8828 --- /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 9ae446e58e13..ae787260adca 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.Activity; 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.Slog; 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 @@ public class KeyguardSecurityContainer extends FrameLayout implements KeyguardSe private AlertDialog mAlertDialog; private InjectionInflationController mInjectionInflationController; private boolean mSwipeUpToRetry; + private AdminSecondaryLockScreenController mSecondaryLockScreenController; private final ViewConfiguration mViewConfiguration; private final SpringAnimation mSpringAnimation; @@ -137,6 +142,9 @@ public class KeyguardSecurityContainer extends FrameLayout implements KeyguardSe 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 @@ public class KeyguardSecurityContainer extends FrameLayout implements KeyguardSe mAlertDialog.dismiss(); mAlertDialog = null; } + mSecondaryLockScreenController.hide(); if (mCurrentSecuritySelection != SecurityMode.None) { getSecurityView(mCurrentSecuritySelection).onPause(); } @@ -532,6 +541,15 @@ public class KeyguardSecurityContainer extends FrameLayout implements KeyguardSe 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 class KeyguardSecurityContainer extends FrameLayout implements KeyguardSe 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 65fc215f7505..f03648ad096d 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.lang.ref.WeakReference; 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 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab 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 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab 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 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab 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 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab 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 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab 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 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab /** * 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 04502f054dad..8e87b7ad45b9 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java @@ -310,4 +310,9 @@ public class KeyguardUpdateMonitorCallback { */ 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 000000000000..1954b3936376 --- /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 12da00678ac2..b3c2ba3a32fb 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.spy; 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 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { 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 e939d84292b5..0c79a6f611a5 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 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { 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 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { 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 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { 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 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { } 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 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { // 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 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { } @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 aeba488b5f63..8f1d0f7648f5 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 @@ public class DevicePolicyManagerTest extends DpmTestBase { () -> 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(); |