Add role guarded APIs for subscribing to keyguard locked state in KeyguardManager.
Bug: 216630470
CTS-Coverage-Bug: 220371902
Test: manual
Test: atest CtsAppTestCases:KeyguardLockedStateApiTest
Ignore-AOSP-First: (for packages/Shell/AndroidManifest.xml) Permission
not yet publicly announced.
Change-Id: I1f44ec6799b0f74791613389ce28aadf84c9fe5c
diff --git a/core/api/current.txt b/core/api/current.txt
index 56415ec..753e046 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -189,6 +189,7 @@
field public static final String START_VIEW_APP_FEATURES = "android.permission.START_VIEW_APP_FEATURES";
field public static final String START_VIEW_PERMISSION_USAGE = "android.permission.START_VIEW_PERMISSION_USAGE";
field public static final String STATUS_BAR = "android.permission.STATUS_BAR";
+ field public static final String SUBSCRIBE_TO_KEYGUARD_LOCKED_STATE = "android.permission.SUBSCRIBE_TO_KEYGUARD_LOCKED_STATE";
field public static final String SYSTEM_ALERT_WINDOW = "android.permission.SYSTEM_ALERT_WINDOW";
field public static final String TRANSMIT_IR = "android.permission.TRANSMIT_IR";
field public static final String UNINSTALL_SHORTCUT = "com.android.launcher.permission.UNINSTALL_SHORTCUT";
@@ -5677,6 +5678,7 @@
}
public class KeyguardManager {
+ method @RequiresPermission(android.Manifest.permission.SUBSCRIBE_TO_KEYGUARD_LOCKED_STATE) public void addKeyguardLockedStateListener(@NonNull java.util.concurrent.Executor, @NonNull android.app.KeyguardManager.KeyguardLockedStateListener);
method @Deprecated public android.content.Intent createConfirmDeviceCredentialIntent(CharSequence, CharSequence);
method @Deprecated @RequiresPermission(android.Manifest.permission.DISABLE_KEYGUARD) public void exitKeyguardSecurely(android.app.KeyguardManager.OnKeyguardExitResult);
method @Deprecated public boolean inKeyguardRestrictedInputMode();
@@ -5685,6 +5687,7 @@
method public boolean isKeyguardLocked();
method public boolean isKeyguardSecure();
method @Deprecated public android.app.KeyguardManager.KeyguardLock newKeyguardLock(String);
+ method @RequiresPermission(android.Manifest.permission.SUBSCRIBE_TO_KEYGUARD_LOCKED_STATE) public void removeKeyguardLockedStateListener(@NonNull android.app.KeyguardManager.KeyguardLockedStateListener);
method public void requestDismissKeyguard(@NonNull android.app.Activity, @Nullable android.app.KeyguardManager.KeyguardDismissCallback);
}
@@ -5700,6 +5703,10 @@
method @Deprecated @RequiresPermission(android.Manifest.permission.DISABLE_KEYGUARD) public void reenableKeyguard();
}
+ @java.lang.FunctionalInterface public static interface KeyguardManager.KeyguardLockedStateListener {
+ method public void onKeyguardLockedStateChanged(boolean);
+ }
+
@Deprecated public static interface KeyguardManager.OnKeyguardExitResult {
method @Deprecated public void onKeyguardExitResult(boolean);
}
diff --git a/core/java/android/app/KeyguardManager.java b/core/java/android/app/KeyguardManager.java
index 9910000..87ba197 100644
--- a/core/java/android/app/KeyguardManager.java
+++ b/core/java/android/app/KeyguardManager.java
@@ -52,6 +52,7 @@
import android.view.WindowManagerGlobal;
import com.android.internal.policy.IKeyguardDismissCallback;
+import com.android.internal.policy.IKeyguardLockedStateListener;
import com.android.internal.util.Preconditions;
import com.android.internal.widget.IWeakEscrowTokenActivatedListener;
import com.android.internal.widget.IWeakEscrowTokenRemovedListener;
@@ -183,6 +184,10 @@
})
@interface LockTypes {}
+ // TODO(b/220379118): register only one binder listener and keep a map of listener to executor.
+ private final ArrayMap<KeyguardLockedStateListener, IKeyguardLockedStateListener>
+ mKeyguardLockedStateListeners = new ArrayMap<>();
+
/**
* Get an intent to prompt the user to confirm credentials (pin, pattern, password or biometrics
* if enrolled) for the current user of the device. The caller is expected to launch this
@@ -534,7 +539,7 @@
/**
* Return whether the keyguard is currently locked.
*
- * @return true if keyguard is locked.
+ * @return {@code true} if the keyguard is locked.
*/
public boolean isKeyguardLocked() {
try {
@@ -550,7 +555,7 @@
*
* <p>See also {@link #isDeviceSecure()} which ignores SIM locked states.
*
- * @return true if a PIN, pattern or password is set or a SIM card is locked.
+ * @return {@code true} if a PIN, pattern or password is set or a SIM card is locked.
*/
public boolean isKeyguardSecure() {
try {
@@ -565,7 +570,7 @@
* keyguard password emergency screen). When in such mode, certain keys,
* such as the Home key and the right soft keys, don't work.
*
- * @return true if in keyguard restricted input mode.
+ * @return {@code true} if in keyguard restricted input mode.
* @deprecated Use {@link #isKeyguardLocked()} instead.
*/
public boolean inKeyguardRestrictedInputMode() {
@@ -576,7 +581,7 @@
* Returns whether the device is currently locked and requires a PIN, pattern or
* password to unlock.
*
- * @return true if unlocking the device currently requires a PIN, pattern or
+ * @return {@code true} if unlocking the device currently requires a PIN, pattern or
* password.
*/
public boolean isDeviceLocked() {
@@ -603,7 +608,7 @@
*
* <p>See also {@link #isKeyguardSecure} which treats SIM locked states as secure.
*
- * @return true if a PIN, pattern or password was set.
+ * @return {@code true} if a PIN, pattern or password was set.
*/
public boolean isDeviceSecure() {
return isDeviceSecure(mContext.getUserId());
@@ -762,7 +767,7 @@
* as the output of String#getBytes
* @param complexity - complexity level imposed by the requester
* as defined in {@code DevicePolicyManager.PasswordComplexity}
- * @return true if the password is valid, false otherwise
+ * @return {@code true} if the password is valid, false otherwise
* @hide
*/
@RequiresPermission(Manifest.permission.SET_INITIAL_LOCK)
@@ -821,7 +826,7 @@
* as the output of String#getBytes
* @param complexity - complexity level imposed by the requester
* as defined in {@code DevicePolicyManager.PasswordComplexity}
- * @return true if the lock is successfully set, false otherwise
+ * @return {@code true} if the lock is successfully set, false otherwise
* @hide
*/
@RequiresPermission(Manifest.permission.SET_INITIAL_LOCK)
@@ -903,8 +908,8 @@
/**
* Remove a weak escrow token.
*
- * @return true if the given handle refers to a valid weak token previously returned from
- * {@link #addWeakEscrowToken}, whether it's active or not. return false otherwise.
+ * @return {@code true} if the given handle refers to a valid weak token previously returned
+ * from {@link #addWeakEscrowToken}, whether it's active or not. return false otherwise.
* @hide
*/
@RequiresFeature(PackageManager.FEATURE_AUTOMOTIVE)
@@ -944,7 +949,7 @@
/**
* Register the given WeakEscrowTokenRemovedListener.
*
- * @return true if the listener is registered successfully, return false otherwise.
+ * @return {@code true} if the listener is registered successfully, return false otherwise.
* @hide
*/
@RequiresFeature(PackageManager.FEATURE_AUTOMOTIVE)
@@ -982,7 +987,7 @@
/**
* Unregister the given WeakEscrowTokenRemovedListener.
*
- * @return true if the listener is unregistered successfully, return false otherwise.
+ * @return {@code true} if the listener is unregistered successfully, return false otherwise.
* @hide
*/
@RequiresFeature(PackageManager.FEATURE_AUTOMOTIVE)
@@ -1076,4 +1081,61 @@
throw new IllegalArgumentException("Unknown lock type " + lockType);
}
}
+
+ /**
+ * Listener for keyguard locked state changes.
+ */
+ @FunctionalInterface
+ public interface KeyguardLockedStateListener {
+ /**
+ * Callback function that executes when the keyguard locked state changes.
+ */
+ void onKeyguardLockedStateChanged(boolean isKeyguardLocked);
+ }
+
+ /**
+ * Registers a listener to execute when the keyguard visibility changes.
+ *
+ * @param listener The listener to add to receive keyguard visibility changes.
+ */
+ @RequiresPermission(Manifest.permission.SUBSCRIBE_TO_KEYGUARD_LOCKED_STATE)
+ public void addKeyguardLockedStateListener(@NonNull @CallbackExecutor Executor executor,
+ @NonNull KeyguardLockedStateListener listener) {
+ synchronized (mKeyguardLockedStateListeners) {
+ try {
+ final IKeyguardLockedStateListener innerListener =
+ new IKeyguardLockedStateListener.Stub() {
+ @Override
+ public void onKeyguardLockedStateChanged(boolean isKeyguardLocked) {
+ executor.execute(
+ () -> listener.onKeyguardLockedStateChanged(isKeyguardLocked));
+ }
+ };
+ mWM.addKeyguardLockedStateListener(innerListener);
+ mKeyguardLockedStateListeners.put(listener, innerListener);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * Unregisters a listener that executes when the keyguard visibility changes.
+ */
+ @RequiresPermission(Manifest.permission.SUBSCRIBE_TO_KEYGUARD_LOCKED_STATE)
+ public void removeKeyguardLockedStateListener(@NonNull KeyguardLockedStateListener listener) {
+ synchronized (mKeyguardLockedStateListeners) {
+ IKeyguardLockedStateListener innerListener = mKeyguardLockedStateListeners.get(
+ listener);
+ if (innerListener == null) {
+ return;
+ }
+ try {
+ mWM.removeKeyguardLockedStateListener(innerListener);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ mKeyguardLockedStateListeners.remove(listener);
+ }
+ }
}
diff --git a/core/java/android/view/IWindowManager.aidl b/core/java/android/view/IWindowManager.aidl
index ce21086..53b842a 100644
--- a/core/java/android/view/IWindowManager.aidl
+++ b/core/java/android/view/IWindowManager.aidl
@@ -18,6 +18,7 @@
import com.android.internal.os.IResultReceiver;
import com.android.internal.policy.IKeyguardDismissCallback;
+import com.android.internal.policy.IKeyguardLockedStateListener;
import com.android.internal.policy.IShortcutService;
import android.app.IAssistDataReceiver;
@@ -199,6 +200,9 @@
boolean isKeyguardSecure(int userId);
void dismissKeyguard(IKeyguardDismissCallback callback, CharSequence message);
+ void addKeyguardLockedStateListener(in IKeyguardLockedStateListener listener);
+ void removeKeyguardLockedStateListener(in IKeyguardLockedStateListener listener);
+
// Requires INTERACT_ACROSS_USERS_FULL permission
void setSwitchingUser(boolean switching);
diff --git a/core/java/com/android/internal/policy/IKeyguardLockedStateListener.aidl b/core/java/com/android/internal/policy/IKeyguardLockedStateListener.aidl
new file mode 100644
index 0000000..ee50219
--- /dev/null
+++ b/core/java/com/android/internal/policy/IKeyguardLockedStateListener.aidl
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2022 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.internal.policy;
+
+oneway interface IKeyguardLockedStateListener {
+ void onKeyguardLockedStateChanged(boolean isKeyguardLocked);
+}
\ No newline at end of file
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index d4c03e4..659b1b0 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -4119,6 +4119,13 @@
<permission android:name="android.permission.MANAGE_HOTWORD_DETECTION"
android:protectionLevel="internal|preinstalled" />
+ <!-- Allows an application to subscribe to keyguard locked (i.e., showing) state.
+ <p>Protection level: internal|role
+ <p>Intended for use by ROLE_ASSISTANT only.
+ -->
+ <permission android:name="android.permission.SUBSCRIBE_TO_KEYGUARD_LOCKED_STATE"
+ android:protectionLevel="internal|role"/>
+
<!-- Must be required by a {@link android.service.autofill.AutofillService},
to ensure that only the system can bind to it.
<p>Protection level: signature
diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml
index f0b180e..121f9e5 100644
--- a/packages/Shell/AndroidManifest.xml
+++ b/packages/Shell/AndroidManifest.xml
@@ -588,6 +588,9 @@
<uses-permission android:name="android.permission.MANAGE_HOTWORD_DETECTION" />
<uses-permission android:name="android.permission.BIND_HOTWORD_DETECTION_SERVICE" />
+ <!-- Permission required for CTS test - KeyguardLockedStateApiTest -->
+ <uses-permission android:name="android.permission.SUBSCRIBE_TO_KEYGUARD_LOCKED_STATE" />
+
<uses-permission android:name="android.permission.MANAGE_APP_HIBERNATION"/>
<!-- Permission required for CTS test - ResourceObserverNativeTest -->
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index 5b1021e..709f885 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -200,6 +200,7 @@
import android.os.PowerManagerInternal;
import android.os.PowerSaveState;
import android.os.RemoteCallback;
+import android.os.RemoteCallbackList;
import android.os.RemoteException;
import android.os.ResultReceiver;
import android.os.ServiceManager;
@@ -288,6 +289,7 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.os.IResultReceiver;
import com.android.internal.policy.IKeyguardDismissCallback;
+import com.android.internal.policy.IKeyguardLockedStateListener;
import com.android.internal.policy.IShortcutService;
import com.android.internal.policy.KeyInterceptionInfo;
import com.android.internal.protolog.ProtoLogImpl;
@@ -460,6 +462,10 @@
final private KeyguardDisableHandler mKeyguardDisableHandler;
+ private final RemoteCallbackList<IKeyguardLockedStateListener> mKeyguardLockedStateListeners =
+ new RemoteCallbackList<>();
+ private boolean mDispatchedKeyguardLockedState = false;
+
// VR Vr2d Display Id.
int mVr2dDisplayId = INVALID_DISPLAY;
boolean mVrModeEnabled = false;
@@ -3029,6 +3035,7 @@
@Override
public void onKeyguardShowingAndNotOccludedChanged() {
mH.sendEmptyMessage(H.RECOMPUTE_FOCUS);
+ dispatchKeyguardLockedStateState();
}
@Override
@@ -3218,6 +3225,50 @@
}
}
+ @RequiresPermission(Manifest.permission.SUBSCRIBE_TO_KEYGUARD_LOCKED_STATE)
+ @Override
+ public void addKeyguardLockedStateListener(IKeyguardLockedStateListener listener) {
+ enforceSubscribeToKeyguardLockedStatePermission();
+ boolean registered = mKeyguardLockedStateListeners.register(listener);
+ if (!registered) {
+ Slog.w(TAG, "Failed to register listener: " + listener);
+ }
+ }
+
+ @RequiresPermission(Manifest.permission.SUBSCRIBE_TO_KEYGUARD_LOCKED_STATE)
+ @Override
+ public void removeKeyguardLockedStateListener(IKeyguardLockedStateListener listener) {
+ enforceSubscribeToKeyguardLockedStatePermission();
+ mKeyguardLockedStateListeners.unregister(listener);
+ }
+
+ private void enforceSubscribeToKeyguardLockedStatePermission() {
+ mContext.enforceCallingOrSelfPermission(
+ Manifest.permission.SUBSCRIBE_TO_KEYGUARD_LOCKED_STATE,
+ Manifest.permission.SUBSCRIBE_TO_KEYGUARD_LOCKED_STATE
+ + " permission required to read keyguard visibility");
+ }
+
+ private void dispatchKeyguardLockedStateState() {
+ mH.post(() -> {
+ final boolean isKeyguardLocked = mPolicy.isKeyguardShowing();
+ if (mDispatchedKeyguardLockedState == isKeyguardLocked) {
+ return;
+ }
+ final int n = mKeyguardLockedStateListeners.beginBroadcast();
+ for (int i = 0; i < n; i++) {
+ try {
+ mKeyguardLockedStateListeners.getBroadcastItem(i).onKeyguardLockedStateChanged(
+ isKeyguardLocked);
+ } catch (RemoteException e) {
+ // Handled by the RemoteCallbackList.
+ }
+ }
+ mKeyguardLockedStateListeners.finishBroadcast();
+ mDispatchedKeyguardLockedState = isKeyguardLocked;
+ });
+ }
+
@Override
public void setSwitchingUser(boolean switching) {
if (!checkCallingPermission(Manifest.permission.INTERACT_ACROSS_USERS_FULL,