diff --git a/core/java/android/app/StatusBarManager.java b/core/java/android/app/StatusBarManager.java index 64d3a9f08548e..56c301f30d5fc 100644 --- a/core/java/android/app/StatusBarManager.java +++ b/core/java/android/app/StatusBarManager.java @@ -44,6 +44,7 @@ import com.android.internal.statusbar.NotificationVisibility; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.Objects; +import java.util.Set; import java.util.concurrent.Executor; import java.util.function.Consumer; @@ -213,6 +214,34 @@ public class StatusBarManager { /** @hide */ public static final int CAMERA_LAUNCH_SOURCE_LIFT_TRIGGER = 2; + /** + * Session flag for {@link #registerSessionListener} indicating the listener + * is interested in sessions on the keygaurd + * @hide + */ + public static final int SESSION_KEYGUARD = 1 << 0; + + /** + * Session flag for {@link #registerSessionListener} indicating the current session + * is interested in session on the biometric prompt. + * @hide + */ + public static final int SESSION_BIOMETRIC_PROMPT = 1 << 1; + + /** @hide */ + public static final Set ALL_SESSIONS = Set.of( + SESSION_KEYGUARD, + SESSION_BIOMETRIC_PROMPT + ); + + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(flag = true, prefix = { "SESSION_KEYGUARD" }, value = { + SESSION_KEYGUARD, + SESSION_BIOMETRIC_PROMPT, + }) + public @interface SessionFlags {} + /** * Response indicating that the tile was not added. */ diff --git a/core/java/com/android/internal/logging/InstanceId.aidl b/core/java/com/android/internal/logging/InstanceId.aidl new file mode 100644 index 0000000000000..19a6177c03947 --- /dev/null +++ b/core/java/com/android/internal/logging/InstanceId.aidl @@ -0,0 +1,19 @@ +/* + * 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.logging; + +parcelable InstanceId; diff --git a/core/java/com/android/internal/statusbar/ISessionListener.aidl b/core/java/com/android/internal/statusbar/ISessionListener.aidl new file mode 100644 index 0000000000000..101a2d2d159c3 --- /dev/null +++ b/core/java/com/android/internal/statusbar/ISessionListener.aidl @@ -0,0 +1,25 @@ +/** + * 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 permissons and + * limitations under the License. + */ + +package com.android.internal.statusbar; + +import com.android.internal.logging.InstanceId; + +/** {@hide} */ +oneway interface ISessionListener { + void onSessionStarted(int sessionType, in InstanceId instance); + void onSessionEnded(int sessionType, in InstanceId instance); +} \ No newline at end of file diff --git a/core/java/com/android/internal/statusbar/IStatusBarService.aidl b/core/java/com/android/internal/statusbar/IStatusBarService.aidl index 3c6b7ff60a030..accb98645599e 100644 --- a/core/java/com/android/internal/statusbar/IStatusBarService.aidl +++ b/core/java/com/android/internal/statusbar/IStatusBarService.aidl @@ -28,7 +28,9 @@ import android.os.Bundle; import android.os.UserHandle; import android.service.notification.StatusBarNotification; +import com.android.internal.logging.InstanceId; import com.android.internal.statusbar.IAddTileResultCallback; +import com.android.internal.statusbar.ISessionListener; import com.android.internal.statusbar.IStatusBar; import com.android.internal.statusbar.RegisterStatusBarResult; import com.android.internal.statusbar.StatusBarIcon; @@ -178,4 +180,17 @@ interface IStatusBarService * @hide */ int getNavBarModeOverride(); + + /** + * Register a listener for certain sessions. Each session may be guarded by its own permission. + */ + void registerSessionListener(int sessionFlags, in ISessionListener listener); + void unregisterSessionListener(int sessionFlags, in ISessionListener listener); + + /** + * Informs all registered listeners that a session has begun and has the following instanceId. + * Can only be set by callers with certain permission based on the session type being updated. + */ + void onSessionStarted(int sessionType, in InstanceId instanceId); + void onSessionEnded(int sessionType, in InstanceId instanceId); } diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml index 9d7cf1ad4f292..079f5d0a28051 100644 --- a/packages/SystemUI/res/values/config.xml +++ b/packages/SystemUI/res/values/config.xml @@ -309,6 +309,7 @@ com.android.systemui.globalactions.GlobalActionsComponent com.android.systemui.ScreenDecorations com.android.systemui.biometrics.AuthController + com.android.systemui.log.SessionTracker com.android.systemui.SliceBroadcastRelayHandler com.android.systemui.statusbar.notification.InstantAppNotifier com.android.systemui.theme.ThemeOverlayController diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java index f833c2ad433bc..b0f7e55112afa 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java @@ -170,6 +170,10 @@ public class AuthController extends CoreStartable implements CommandQueue.Callba mCurrentDialog = null; mOrientationListener.disable(); + for (Callback cb : mCallbacks) { + cb.onBiometricPromptDismissed(); + } + try { if (mReceiver != null) { mReceiver.onDialogDismissed(BiometricPrompt.DISMISSED_REASON_USER_CANCEL, @@ -200,6 +204,10 @@ public class AuthController extends CoreStartable implements CommandQueue.Callba mCurrentDialog = null; mOrientationListener.disable(); + for (Callback cb : mCallbacks) { + cb.onBiometricPromptDismissed(); + } + if (mReceiver != null) { mReceiver.onDialogDismissed( BiometricPrompt.DISMISSED_REASON_USER_CANCEL, @@ -462,6 +470,7 @@ public class AuthController extends CoreStartable implements CommandQueue.Callba Log.e(TAG, "sendResultAndCleanUp: Receiver is null"); return; } + try { mReceiver.onDialogDismissed(reason, credentialAttestation); } catch (RemoteException e) { @@ -816,6 +825,9 @@ public class AuthController extends CoreStartable implements CommandQueue.Callba } mReceiver = (IBiometricSysuiReceiver) args.arg2; + for (Callback cb : mCallbacks) { + cb.onBiometricPromptShown(); + } mCurrentDialog = newDialog; mCurrentDialog.show(mWindowManager, savedState); mOrientationListener.enable(); @@ -826,6 +838,11 @@ public class AuthController extends CoreStartable implements CommandQueue.Callba if (mCurrentDialog == null) { Log.w(TAG, "Dialog already dismissed"); } + + for (Callback cb : mCallbacks) { + cb.onBiometricPromptDismissed(); + } + mReceiver = null; mCurrentDialog = null; mOrientationListener.disable(); @@ -897,12 +914,22 @@ public class AuthController extends CoreStartable implements CommandQueue.Callba * Called when authenticators are registered. If authenticators are already * registered before this call, this callback will never be triggered. */ - void onAllAuthenticatorsRegistered(); + default void onAllAuthenticatorsRegistered() {} /** * Called when UDFPS enrollments have changed. This is called after boot and on changes to * enrollment. */ - void onEnrollmentsChanged(); + default void onEnrollmentsChanged() {} + + /** + * Called when the biometric prompt starts showing. + */ + default void onBiometricPromptShown() {} + + /** + * Called when the biometric prompt is no longer showing. + */ + default void onBiometricPromptDismissed() {} } } diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIBinder.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIBinder.java index bbe9dbd57f531..96e2302f937c2 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIBinder.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIBinder.java @@ -29,6 +29,7 @@ import com.android.systemui.dreams.DreamOverlayRegistrant; import com.android.systemui.globalactions.GlobalActionsComponent; import com.android.systemui.keyguard.KeyguardViewMediator; import com.android.systemui.keyguard.dagger.KeyguardModule; +import com.android.systemui.log.SessionTracker; import com.android.systemui.media.systemsounds.HomeSoundEffectController; import com.android.systemui.power.PowerUI; import com.android.systemui.privacy.television.TvOngoingPrivacyChip; @@ -66,6 +67,12 @@ public abstract class SystemUIBinder { @ClassKey(AuthController.class) public abstract CoreStartable bindAuthController(AuthController service); + /** Inject into SessionTracker. */ + @Binds + @IntoMap + @ClassKey(SessionTracker.class) + public abstract CoreStartable bindSessionTracker(SessionTracker service); + /** Inject into GarbageMonitor.Service. */ @Binds @IntoMap diff --git a/packages/SystemUI/src/com/android/systemui/log/SessionTracker.java b/packages/SystemUI/src/com/android/systemui/log/SessionTracker.java new file mode 100644 index 0000000000000..0656f5efbe802 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/log/SessionTracker.java @@ -0,0 +1,203 @@ +/* + * 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.systemui.log; + +import static android.app.StatusBarManager.ALL_SESSIONS; +import static android.app.StatusBarManager.SESSION_BIOMETRIC_PROMPT; +import static android.app.StatusBarManager.SESSION_KEYGUARD; + +import android.annotation.Nullable; +import android.content.Context; +import android.os.RemoteException; +import android.util.Log; + +import androidx.annotation.NonNull; + +import com.android.internal.logging.InstanceId; +import com.android.internal.logging.InstanceIdSequence; +import com.android.internal.statusbar.IStatusBarService; +import com.android.keyguard.KeyguardUpdateMonitor; +import com.android.keyguard.KeyguardUpdateMonitorCallback; +import com.android.systemui.CoreStartable; +import com.android.systemui.biometrics.AuthController; +import com.android.systemui.dagger.SysUISingleton; +import com.android.systemui.statusbar.policy.KeyguardStateController; + +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.util.HashMap; +import java.util.Map; + +import javax.inject.Inject; + +/** + * Track Session InstanceIds to be used for metrics logging to correlate logs in the same + * session. Can be used across processes via StatusBarManagerService#registerSessionListener + */ +@SysUISingleton +public class SessionTracker extends CoreStartable { + private static final String TAG = "SessionTracker"; + private static final boolean DEBUG = false; + + // At most 20 bits: ~1m possibilities, ~0.5% probability of collision in 100 values + private final InstanceIdSequence mInstanceIdGenerator = new InstanceIdSequence(1 << 20); + + private final IStatusBarService mStatusBarManagerService; + private final AuthController mAuthController; + private final KeyguardUpdateMonitor mKeyguardUpdateMonitor; + private final KeyguardStateController mKeyguardStateController; + private final Map mSessionToInstanceId = new HashMap<>(); + + private boolean mKeyguardSessionStarted; + + @Inject + public SessionTracker( + Context context, + IStatusBarService statusBarService, + AuthController authController, + KeyguardUpdateMonitor keyguardUpdateMonitor, + KeyguardStateController keyguardStateController + ) { + super(context); + mStatusBarManagerService = statusBarService; + mAuthController = authController; + mKeyguardUpdateMonitor = keyguardUpdateMonitor; + mKeyguardStateController = keyguardStateController; + } + + @Override + public void start() { + mAuthController.addCallback(mAuthControllerCallback); + mKeyguardUpdateMonitor.registerCallback(mKeyguardUpdateMonitorCallback); + mKeyguardStateController.addCallback(mKeyguardStateCallback); + + mKeyguardSessionStarted = mKeyguardStateController.isShowing(); + if (mKeyguardSessionStarted) { + startSession(SESSION_KEYGUARD); + } + } + + /** + * Get the session ID associated with the passed session type. + */ + public @Nullable InstanceId getSessionId(int type) { + return mSessionToInstanceId.getOrDefault(type, null); + } + + private void startSession(int type) { + if (mSessionToInstanceId.getOrDefault(type, null) != null) { + Log.e(TAG, "session [" + getString(type) + "] was already started"); + return; + } + + final InstanceId instanceId = mInstanceIdGenerator.newInstanceId(); + mSessionToInstanceId.put(type, instanceId); + try { + if (DEBUG) { + Log.d(TAG, "Session start for [" + getString(type) + "] id=" + instanceId); + } + mStatusBarManagerService.onSessionStarted(type, instanceId); + } catch (RemoteException e) { + Log.e(TAG, "Unable to send onSessionStarted for session=" + + "[" + getString(type) + "]", e); + } + } + + private void endSession(int type) { + if (mSessionToInstanceId.getOrDefault(type, null) == null) { + Log.e(TAG, "session [" + getString(type) + "] was not started"); + return; + } + + final InstanceId instanceId = mSessionToInstanceId.get(type); + mSessionToInstanceId.put(type, null); + try { + if (DEBUG) { + Log.d(TAG, "Session end for [" + getString(type) + "] id=" + instanceId); + } + mStatusBarManagerService.onSessionEnded(type, instanceId); + } catch (RemoteException e) { + Log.e(TAG, "Unable to send onSessionEnded for session=" + + "[" + getString(type) + "]", e); + } + } + + public KeyguardUpdateMonitorCallback mKeyguardUpdateMonitorCallback = + new KeyguardUpdateMonitorCallback() { + @Override + public void onStartedGoingToSleep(int why) { + // we need to register to the KeyguardUpdateMonitor lifecycle b/c it gets called + // before the WakefulnessLifecycle + if (mKeyguardSessionStarted) { + return; + } + + mKeyguardSessionStarted = true; + startSession(SESSION_KEYGUARD); + } + }; + + + public KeyguardStateController.Callback mKeyguardStateCallback = + new KeyguardStateController.Callback() { + public void onKeyguardShowingChanged() { + boolean wasSessionStarted = mKeyguardSessionStarted; + boolean keyguardShowing = mKeyguardStateController.isShowing(); + if (keyguardShowing && !wasSessionStarted) { + mKeyguardSessionStarted = true; + startSession(SESSION_KEYGUARD); + } else if (!keyguardShowing && wasSessionStarted) { + mKeyguardSessionStarted = false; + endSession(SESSION_KEYGUARD); + } + } + }; + + public AuthController.Callback mAuthControllerCallback = new AuthController.Callback() { + @Override + public void onBiometricPromptShown() { + startSession(SESSION_BIOMETRIC_PROMPT); + } + + @Override + public void onBiometricPromptDismissed() { + endSession(SESSION_BIOMETRIC_PROMPT); + } + }; + + @Override + public void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter pw, @NonNull String[] args) { + for (int session : ALL_SESSIONS) { + pw.println(" " + getString(session) + + " instanceId=" + mSessionToInstanceId.get(session)); + } + } + + /** + * @return the string representation of a SINGLE SessionFlag. Combined SessionFlags will be + * considered unknown. + */ + public static String getString(int sessionType) { + if (sessionType == SESSION_KEYGUARD) { + return "KEYGUARD"; + } else if (sessionType == SESSION_BIOMETRIC_PROMPT) { + return "BIOMETRIC_PROMPT"; + } + + return "unknownType=" + sessionType; + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java index 786f547825982..5d39eef999d74 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java @@ -628,6 +628,34 @@ public class AuthControllerTest extends SysuiTestCase { verify(mDisplayManager).unregisterDisplayListener(any()); } + @Test + public void testOnBiometricPromptShownCallback() { + // GIVEN a callback is registered + AuthController.Callback callback = mock(AuthController.Callback.class); + mAuthController.addCallback(callback); + + // WHEN dialog is shown + showDialog(new int[]{1} /* sensorIds */, false /* credentialAllowed */); + + // THEN callback should be received + verify(callback).onBiometricPromptShown(); + } + + @Test + public void testOnBiometricPromptDismissedCallback() { + // GIVEN a callback is registered + AuthController.Callback callback = mock(AuthController.Callback.class); + mAuthController.addCallback(callback); + + // WHEN dialog is shown and then dismissed + showDialog(new int[]{1} /* sensorIds */, false /* credentialAllowed */); + mAuthController.onDismissed(AuthDialogCallback.DISMISSED_USER_CANCELED, + null /* credentialAttestation */); + + // THEN callback should be received + verify(callback).onBiometricPromptDismissed(); + } + // Helpers private void showDialog(int[] sensorIds, boolean credentialAllowed) { diff --git a/packages/SystemUI/tests/src/com/android/systemui/log/SessionTrackerTest.java b/packages/SystemUI/tests/src/com/android/systemui/log/SessionTrackerTest.java new file mode 100644 index 0000000000000..b8e9cf48f3e20 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/log/SessionTrackerTest.java @@ -0,0 +1,229 @@ +/* + * 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.systemui.log; + +import static android.app.StatusBarManager.ALL_SESSIONS; +import static android.app.StatusBarManager.SESSION_BIOMETRIC_PROMPT; +import static android.app.StatusBarManager.SESSION_KEYGUARD; + +import static junit.framework.Assert.assertNotNull; +import static junit.framework.Assert.assertNull; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.os.RemoteException; +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; + +import androidx.test.filters.SmallTest; + +import com.android.internal.logging.InstanceId; +import com.android.internal.statusbar.IStatusBarService; +import com.android.keyguard.KeyguardUpdateMonitor; +import com.android.keyguard.KeyguardUpdateMonitorCallback; +import com.android.systemui.SysuiTestCase; +import com.android.systemui.biometrics.AuthController; +import com.android.systemui.statusbar.policy.KeyguardStateController; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@RunWith(AndroidTestingRunner.class) +@TestableLooper.RunWithLooper +@SmallTest +public class SessionTrackerTest extends SysuiTestCase { + @Mock + private IStatusBarService mStatusBarService; + @Mock + private AuthController mAuthController; + @Mock + private KeyguardUpdateMonitor mKeyguardUpdateMonitor; + @Mock + private KeyguardStateController mKeyguardStateController; + + @Captor + ArgumentCaptor mKeyguardUpdateMonitorCallbackCaptor; + KeyguardUpdateMonitorCallback mKeyguardUpdateMonitorCallback; + + @Captor + ArgumentCaptor mKeyguardStateCallbackCaptor; + KeyguardStateController.Callback mKeyguardStateCallback; + + @Captor + ArgumentCaptor mAuthControllerCallbackCaptor; + AuthController.Callback mAuthControllerCallback; + + private SessionTracker mSessionTracker; + + @Before + public void setup() throws RemoteException { + MockitoAnnotations.initMocks(this); + + mSessionTracker = new SessionTracker( + mContext, + mStatusBarService, + mAuthController, + mKeyguardUpdateMonitor, + mKeyguardStateController + ); + } + + @Test + public void testOnStartShowingKeyguard() throws RemoteException { + // GIVEN the keyguard is showing before start + when(mKeyguardStateController.isShowing()).thenReturn(true); + + // WHEN started + mSessionTracker.start(); + + // THEN keyguard session has a session id + assertNotNull(mSessionTracker.getSessionId(SESSION_KEYGUARD)); + + // THEN send event to status bar service + verify(mStatusBarService).onSessionStarted(eq(SESSION_KEYGUARD), any(InstanceId.class)); + } + + @Test + public void testNoSessions() throws RemoteException { + // GIVEN no sessions + when(mKeyguardStateController.isShowing()).thenReturn(false); + + // WHEN started + mSessionTracker.start(); + + // THEN all sessions are null + for (int sessionType : ALL_SESSIONS) { + assertNull(mSessionTracker.getSessionId(sessionType)); + } + } + + @Test + public void testBiometricPromptShowing() throws RemoteException { + // GIVEN session tracker started w/o any sessions + mSessionTracker.start(); + captureAuthControllerCallback(); + + // WHEN auth controller shows the biometric prompt + mAuthControllerCallback.onBiometricPromptShown(); + + // THEN the biometric prompt session has a session id + assertNotNull(mSessionTracker.getSessionId(SESSION_BIOMETRIC_PROMPT)); + + // THEN session started event gets sent to status bar service + verify(mStatusBarService).onSessionStarted( + eq(SESSION_BIOMETRIC_PROMPT), any(InstanceId.class)); + } + + @Test + public void testBiometricPromptDismissed() throws RemoteException { + // GIVEN session tracker started w/o any sessions + mSessionTracker.start(); + captureAuthControllerCallback(); + + // WHEN auth controller shows the biometric prompt and then hides it + mAuthControllerCallback.onBiometricPromptShown(); + mAuthControllerCallback.onBiometricPromptDismissed(); + + // THEN the biometric prompt session no longer has a session id + assertNull(mSessionTracker.getSessionId(SESSION_BIOMETRIC_PROMPT)); + + // THEN session end event gets sent to status bar service + verify(mStatusBarService).onSessionEnded( + eq(SESSION_BIOMETRIC_PROMPT), any(InstanceId.class)); + } + + @Test + public void testKeyguardSessionOnDeviceStartsSleeping() throws RemoteException { + // GIVEN session tracker started w/o any sessions + mSessionTracker.start(); + captureKeyguardUpdateMonitorCallback(); + + // WHEN device starts going to sleep + mKeyguardUpdateMonitorCallback.onStartedGoingToSleep(0); + + // THEN the keyguard session has a session id + assertNotNull(mSessionTracker.getSessionId(SESSION_KEYGUARD)); + + // THEN session start event gets sent to status bar service + verify(mStatusBarService).onSessionStarted( + eq(SESSION_KEYGUARD), any(InstanceId.class)); + } + + @Test + public void testKeyguardSessionOnKeyguardShowingChange() throws RemoteException { + // GIVEN session tracker started w/o any sessions + mSessionTracker.start(); + captureKeyguardStateControllerCallback(); + + // WHEN keyguard becomes visible (ie: from lockdown) + when(mKeyguardStateController.isShowing()).thenReturn(true); + mKeyguardStateCallback.onKeyguardShowingChanged(); + + // THEN the keyguard session has a session id + assertNotNull(mSessionTracker.getSessionId(SESSION_KEYGUARD)); + + // THEN session start event gets sent to status bar service + verify(mStatusBarService).onSessionStarted( + eq(SESSION_KEYGUARD), any(InstanceId.class)); + } + + @Test + public void testKeyguardSessionOnKeyguardNotShowing() throws RemoteException { + // GIVEN session tracker started w/o any sessions + mSessionTracker.start(); + captureKeyguardStateControllerCallback(); + + // WHEN keyguard was showing and now it's not + when(mKeyguardStateController.isShowing()).thenReturn(true); + mKeyguardStateCallback.onKeyguardShowingChanged(); + when(mKeyguardStateController.isShowing()).thenReturn(false); + mKeyguardStateCallback.onKeyguardShowingChanged(); + + // THEN the keyguard session no longer has a session id + assertNull(mSessionTracker.getSessionId(SESSION_KEYGUARD)); + + // THEN session end event gets sent to status bar service + verify(mStatusBarService).onSessionEnded( + eq(SESSION_KEYGUARD), any(InstanceId.class)); + } + + void captureKeyguardUpdateMonitorCallback() { + verify(mKeyguardUpdateMonitor).registerCallback( + mKeyguardUpdateMonitorCallbackCaptor.capture()); + mKeyguardUpdateMonitorCallback = mKeyguardUpdateMonitorCallbackCaptor.getValue(); + } + + void captureKeyguardStateControllerCallback() { + verify(mKeyguardStateController).addCallback( + mKeyguardStateCallbackCaptor.capture()); + mKeyguardStateCallback = mKeyguardStateCallbackCaptor.getValue(); + } + + void captureAuthControllerCallback() { + verify(mAuthController).addCallback( + mAuthControllerCallbackCaptor.capture()); + mAuthControllerCallback = mAuthControllerCallbackCaptor.getValue(); + } +} diff --git a/services/core/java/com/android/server/statusbar/SessionMonitor.java b/services/core/java/com/android/server/statusbar/SessionMonitor.java new file mode 100644 index 0000000000000..f4356bd674303 --- /dev/null +++ b/services/core/java/com/android/server/statusbar/SessionMonitor.java @@ -0,0 +1,166 @@ +/** + * 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.server.statusbar; + +import static android.app.StatusBarManager.ALL_SESSIONS; +import static android.app.StatusBarManager.SESSION_BIOMETRIC_PROMPT; +import static android.app.StatusBarManager.SESSION_KEYGUARD; +import static android.app.StatusBarManager.SessionFlags; + +import android.Manifest; +import android.annotation.NonNull; +import android.content.Context; +import android.os.RemoteException; +import android.util.Log; + +import com.android.internal.logging.InstanceId; +import com.android.internal.statusbar.ISessionListener; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * Monitors session starts and ends. Session instanceIds can be used to correlate logs. + */ +public class SessionMonitor { + private static final String TAG = "SessionMonitor"; + + private final Context mContext; + private final Map> mSessionToListeners = + new HashMap<>(); + + /** */ + public SessionMonitor(Context context) { + mContext = context; + // initialize all sessions in the map + for (int session : ALL_SESSIONS) { + mSessionToListeners.put(session, new HashSet<>()); + } + } + + /** + * Registers a listener for all sessionTypes included in sessionFlags. + */ + public void registerSessionListener(@SessionFlags int sessionFlags, + ISessionListener listener) { + requireListenerPermissions(sessionFlags); + synchronized (mSessionToListeners) { + for (int sessionType : ALL_SESSIONS) { + if ((sessionFlags & sessionType) != 0) { + mSessionToListeners.get(sessionType).add(listener); + } + } + } + } + + /** + * Unregisters a listener for all sessionTypes included in sessionFlags. + */ + public void unregisterSessionListener(@SessionFlags int sessionFlags, + ISessionListener listener) { + synchronized (mSessionToListeners) { + for (int sessionType : ALL_SESSIONS) { + if ((sessionFlags & sessionType) != 0) { + mSessionToListeners.get(sessionType).remove(listener); + } + } + } + } + + /** + * Starts a session with the given sessionType, creating a new instanceId. + * Sends this message to all listeners registered for the given sessionType. + * + * Callers require special permission to start and end a session depending on the session. + */ + public void onSessionStarted(@SessionFlags int sessionType, @NonNull InstanceId instanceId) { + requireSetterPermissions(sessionType); + + if (!isValidSessionType(sessionType)) { + Log.e(TAG, "invalid onSessionStarted sessionType=" + sessionType); + return; + } + + synchronized (mSessionToListeners) { + for (ISessionListener listener : mSessionToListeners.get(sessionType)) { + try { + listener.onSessionStarted(sessionType, instanceId); + } catch (RemoteException e) { + Log.e(TAG, "unable to send session start to listener=" + listener, e); + } + } + } + } + + /** + * Ends a session with the given sessionType and instanceId. Sends this message + * to all listeners registered for the given sessionType. + * + * Callers require special permission to start and end a session depending on the session. + */ + public void onSessionEnded(@SessionFlags int sessionType, @NonNull InstanceId instanceId) { + requireSetterPermissions(sessionType); + + if (!isValidSessionType(sessionType)) { + Log.e(TAG, "invalid onSessionEnded sessionType=" + sessionType); + return; + } + + synchronized (mSessionToListeners) { + for (ISessionListener listener : mSessionToListeners.get(sessionType)) { + try { + listener.onSessionEnded(sessionType, instanceId); + } catch (RemoteException e) { + Log.e(TAG, "unable to send session end to listener=" + listener, e); + } + } + } + } + + private boolean isValidSessionType(@SessionFlags int sessionType) { + return ALL_SESSIONS.contains(sessionType); + } + + private void requireListenerPermissions(@SessionFlags int sessionFlags) { + if ((sessionFlags & SESSION_KEYGUARD) != 0) { + mContext.enforceCallingOrSelfPermission( + Manifest.permission.MANAGE_BIOMETRIC, + "StatusBarManagerService.SessionMonitor"); + } + + if ((sessionFlags & SESSION_BIOMETRIC_PROMPT) != 0) { + mContext.enforceCallingOrSelfPermission( + Manifest.permission.MANAGE_BIOMETRIC, + "StatusBarManagerService.SessionMonitor"); + } + } + + private void requireSetterPermissions(@SessionFlags int sessionFlags) { + if ((sessionFlags & SESSION_KEYGUARD) != 0) { + mContext.enforceCallingOrSelfPermission( + Manifest.permission.CONTROL_KEYGUARD, + "StatusBarManagerService.SessionMonitor"); + } + + if ((sessionFlags & SESSION_BIOMETRIC_PROMPT) != 0) { + mContext.enforceCallingOrSelfPermission(android.Manifest.permission.STATUS_BAR_SERVICE, + "StatusBarManagerService.SessionMonitor"); + } + } +} diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java index 0edd06acd7e9a..e71ff784dc23e 100644 --- a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java +++ b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java @@ -21,6 +21,7 @@ import static android.app.StatusBarManager.DISABLE2_NOTIFICATION_SHADE; import static android.app.StatusBarManager.NAV_BAR_MODE_OVERRIDE_KIDS; import static android.app.StatusBarManager.NAV_BAR_MODE_OVERRIDE_NONE; import static android.app.StatusBarManager.NavBarModeOverride; +import static android.app.StatusBarManager.SessionFlags; import static android.view.Display.DEFAULT_DISPLAY; import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_3BUTTON_OVERLAY; @@ -84,8 +85,10 @@ import com.android.internal.R; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.inputmethod.SoftInputShowHideReason; +import com.android.internal.logging.InstanceId; import com.android.internal.os.TransferPipe; import com.android.internal.statusbar.IAddTileResultCallback; +import com.android.internal.statusbar.ISessionListener; import com.android.internal.statusbar.IStatusBar; import com.android.internal.statusbar.IStatusBarService; import com.android.internal.statusbar.NotificationVisibility; @@ -145,6 +148,7 @@ public class StatusBarManagerService extends IStatusBarService.Stub implements D private final ActivityManagerInternal mActivityManagerInternal; private final ActivityTaskManagerInternal mActivityTaskManager; private final PackageManagerInternal mPackageManagerInternal; + private final SessionMonitor mSessionMonitor; private int mCurrentUserId; private boolean mTracingEnabled; @@ -260,6 +264,7 @@ public class StatusBarManagerService extends IStatusBarService.Stub implements D mActivityManagerInternal = LocalServices.getService(ActivityManagerInternal.class); mTileRequestTracker = new TileRequestTracker(mContext); + mSessionMonitor = new SessionMonitor(mContext); } private IOverlayManager getOverlayManager() { @@ -1870,6 +1875,28 @@ public class StatusBarManagerService extends IStatusBarService.Stub implements D } } + @Override + public void onSessionStarted(@SessionFlags int sessionType, InstanceId instance) { + mSessionMonitor.onSessionStarted(sessionType, instance); + } + + @Override + public void onSessionEnded(@SessionFlags int sessionType, InstanceId instance) { + mSessionMonitor.onSessionEnded(sessionType, instance); + } + + @Override + public void registerSessionListener(@SessionFlags int sessionFlags, + ISessionListener listener) { + mSessionMonitor.registerSessionListener(sessionFlags, listener); + } + + @Override + public void unregisterSessionListener(@SessionFlags int sessionFlags, + ISessionListener listener) { + mSessionMonitor.unregisterSessionListener(sessionFlags, listener); + } + public String[] getStatusBarIcons() { return mContext.getResources().getStringArray(R.array.config_statusBarIcons); }