diff options
| author | 2023-12-22 17:14:43 +0000 | |
|---|---|---|
| committer | 2024-02-13 12:05:12 +0000 | |
| commit | e2c762196c8088eb9db11689e59326feefdd7d06 (patch) | |
| tree | 1b48108d95222ae4a55a155a425c6dfc856009b1 | |
| parent | ee804cb324a26b13734b3f2d88f19f441f26a547 (diff) | |
Add ExternalDisplay metrics logging
Change-Id: I3cbcc6f5cd7e55844bffd31e5f9af6224f5bd555
Test: statsd_testdrive 805
Bug: 310976810
Test: atest ExternalDisplayStatsServiceTest ExternalDisplayPolicyTest DisplayNotificationManagerTest DisplayManagerServiceTest
10 files changed, 1211 insertions, 11 deletions
diff --git a/core/java/android/hardware/display/DisplayManagerInternal.java b/core/java/android/hardware/display/DisplayManagerInternal.java index f18a0b758bc5..6b2814ed2146 100644 --- a/core/java/android/hardware/display/DisplayManagerInternal.java +++ b/core/java/android/hardware/display/DisplayManagerInternal.java @@ -431,6 +431,13 @@ public abstract class DisplayManagerInternal { public abstract IntArray getDisplayGroupIds(); /** + * Called upon presentation started/ended on the display. + * @param displayId the id of the display where presentation started. + * @param isShown whether presentation is shown. + */ + public abstract void onPresentation(int displayId, boolean isShown); + + /** * Describes the requested power state of the display. * * This object is intended to describe the general characteristics of the diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java index 7ebc311b5ee8..ad89444edcfd 100644 --- a/services/core/java/com/android/server/display/DisplayManagerService.java +++ b/services/core/java/com/android/server/display/DisplayManagerService.java @@ -534,6 +534,7 @@ public final class DisplayManagerService extends SystemService { private final DisplayManagerFlags mFlags; private final DisplayNotificationManager mDisplayNotificationManager; + private final ExternalDisplayStatsService mExternalDisplayStatsService; /** * Applications use {@link android.view.Display#getRefreshRate} and @@ -568,7 +569,6 @@ public final class DisplayManagerService extends SystemService { mInjector = injector; mContext = context; mFlags = injector.getFlags(); - mDisplayNotificationManager = new DisplayNotificationManager(mFlags, mContext); mHandler = new DisplayManagerHandler(DisplayThread.get().getLooper()); mUiHandler = UiThread.getHandler(); mDisplayDeviceRepo = new DisplayDeviceRepository(mSyncRoot, mPersistentDataStore); @@ -597,6 +597,10 @@ public final class DisplayManagerService extends SystemService { mConfigParameterProvider = new DeviceConfigParameterProvider(DeviceConfigInterface.REAL); mExtraDisplayLoggingPackageName = DisplayProperties.debug_vri_package().orElse(null); mExtraDisplayEventLogging = !TextUtils.isEmpty(mExtraDisplayLoggingPackageName); + + mExternalDisplayStatsService = new ExternalDisplayStatsService(mContext, mHandler); + mDisplayNotificationManager = new DisplayNotificationManager(mFlags, mContext, + mExternalDisplayStatsService); mExternalDisplayPolicy = new ExternalDisplayPolicy(new ExternalDisplayPolicyInjector()); } @@ -1911,6 +1915,7 @@ public final class DisplayManagerService extends SystemService { return; } releaseDisplayAndEmitEvent(display, DisplayManagerGlobal.EVENT_DISPLAY_DISCONNECTED); + mExternalDisplayPolicy.handleLogicalDisplayDisconnectedLocked(display); } @RequiresPermission(Manifest.permission.READ_DEVICE_CONFIG) @@ -1977,7 +1982,7 @@ public final class DisplayManagerService extends SystemService { setupLogicalDisplay(display); - if (ExternalDisplayPolicy.isExternalDisplay(display)) { + if (ExternalDisplayPolicy.isExternalDisplayLocked(display)) { mExternalDisplayPolicy.handleExternalDisplayConnectedLocked(display); } else { sendDisplayEventLocked(display, DisplayManagerGlobal.EVENT_DISPLAY_CONNECTED); @@ -2002,6 +2007,8 @@ public final class DisplayManagerService extends SystemService { sendDisplayEventIfEnabledLocked(display, DisplayManagerGlobal.EVENT_DISPLAY_ADDED); updateLogicalDisplayState(display); + + mExternalDisplayPolicy.handleLogicalDisplayAddedLocked(display); } private void handleLogicalDisplayChangedLocked(@NonNull LogicalDisplay display) { @@ -3280,7 +3287,7 @@ public final class DisplayManagerService extends SystemService { final var logicalDisplay = mLogicalDisplayMapper.getDisplayLocked(displayId); if (logicalDisplay == null) { Slog.w(TAG, "enableConnectedDisplay: Can not find displayId=" + displayId); - } else if (ExternalDisplayPolicy.isExternalDisplay(logicalDisplay)) { + } else if (ExternalDisplayPolicy.isExternalDisplayLocked(logicalDisplay)) { mExternalDisplayPolicy.setExternalDisplayEnabledLocked(logicalDisplay, enabled); } else { mLogicalDisplayMapper.setDisplayEnabledLocked(logicalDisplay, enabled); @@ -4966,6 +4973,11 @@ public final class DisplayManagerService extends SystemService { return session; } } + + @Override + public void onPresentation(int displayId, boolean isShown) { + mExternalDisplayPolicy.onPresentation(displayId, isShown); + } } class DesiredDisplayModeSpecsObserver @@ -5123,5 +5135,14 @@ public final class DisplayManagerService extends SystemService { public Handler getHandler() { return mHandler; } + + /** + * Gets service used for metrics collection. + */ + @Override + @NonNull + public ExternalDisplayStatsService getExternalDisplayStatsService() { + return mExternalDisplayStatsService; + } } } diff --git a/services/core/java/com/android/server/display/ExternalDisplayPolicy.java b/services/core/java/com/android/server/display/ExternalDisplayPolicy.java index dbe1e14f683f..ab7c503bcb83 100644 --- a/services/core/java/com/android/server/display/ExternalDisplayPolicy.java +++ b/services/core/java/com/android/server/display/ExternalDisplayPolicy.java @@ -57,7 +57,7 @@ class ExternalDisplayPolicy { @VisibleForTesting static final String ENABLE_ON_CONNECT = "persist.sys.display.enable_on_connect.external"; - static boolean isExternalDisplay(@NonNull final LogicalDisplay logicalDisplay) { + static boolean isExternalDisplayLocked(@NonNull final LogicalDisplay logicalDisplay) { return logicalDisplay.getDisplayInfoLocked().type == TYPE_EXTERNAL; } @@ -85,6 +85,9 @@ class ExternalDisplayPolicy { @NonNull Handler getHandler(); + + @NonNull + ExternalDisplayStatsService getExternalDisplayStatsService(); } @NonNull @@ -99,6 +102,8 @@ class ExternalDisplayPolicy { private final DisplayNotificationManager mDisplayNotificationManager; @NonNull private final Handler mHandler; + @NonNull + private final ExternalDisplayStatsService mExternalDisplayStatsService; @ThrottlingStatus private volatile int mStatus = THROTTLING_NONE; @@ -109,6 +114,7 @@ class ExternalDisplayPolicy { mFlags = mInjector.getFlags(); mDisplayNotificationManager = mInjector.getDisplayNotificationManager(); mHandler = mInjector.getHandler(); + mExternalDisplayStatsService = mInjector.getExternalDisplayStatsService(); } /** @@ -141,7 +147,7 @@ class ExternalDisplayPolicy { */ void setExternalDisplayEnabledLocked(@NonNull final LogicalDisplay logicalDisplay, final boolean enabled) { - if (!isExternalDisplay(logicalDisplay)) { + if (!isExternalDisplayLocked(logicalDisplay)) { Slog.e(TAG, "setExternalDisplayEnabledLocked called for non external display"); return; } @@ -170,7 +176,7 @@ class ExternalDisplayPolicy { * user to decide how to use this display. */ void handleExternalDisplayConnectedLocked(@NonNull final LogicalDisplay logicalDisplay) { - if (!isExternalDisplay(logicalDisplay)) { + if (!isExternalDisplayLocked(logicalDisplay)) { Slog.e(TAG, "handleExternalDisplayConnectedLocked called for non-external display"); return; } @@ -183,6 +189,8 @@ class ExternalDisplayPolicy { return; } + mExternalDisplayStatsService.onDisplayConnected(logicalDisplay); + if ((Build.IS_ENG || Build.IS_USERDEBUG) && SystemProperties.getBoolean(ENABLE_ON_CONNECT, false)) { Slog.w(TAG, "External display is enabled by default, bypassing user consent."); @@ -209,9 +217,59 @@ class ExternalDisplayPolicy { } } + /** + * Upon external display become unavailable. + */ + void handleLogicalDisplayDisconnectedLocked(@NonNull final LogicalDisplay logicalDisplay) { + // Type of the display here is always UNKNOWN, so we can't verify it is an external display + + if (!mFlags.isConnectedDisplayManagementEnabled()) { + return; + } + + mExternalDisplayStatsService.onDisplayDisconnected(logicalDisplay.getDisplayIdLocked()); + } + + /** + * Upon external display gets added. + */ + void handleLogicalDisplayAddedLocked(@NonNull final LogicalDisplay logicalDisplay) { + if (!isExternalDisplayLocked(logicalDisplay)) { + return; + } + + if (!mFlags.isConnectedDisplayManagementEnabled()) { + return; + } + + mExternalDisplayStatsService.onDisplayAdded(logicalDisplay.getDisplayIdLocked()); + } + + /** + * Upon presentation started. + */ + void onPresentation(int displayId, boolean isShown) { + synchronized (mSyncRoot) { + var logicalDisplay = mLogicalDisplayMapper.getDisplayLocked(displayId); + if (logicalDisplay == null || !isExternalDisplayLocked(logicalDisplay)) { + return; + } + } + + if (!mFlags.isConnectedDisplayManagementEnabled()) { + return; + } + + if (isShown) { + mExternalDisplayStatsService.onPresentationWindowAdded(displayId); + } else { + mExternalDisplayStatsService.onPresentationWindowRemoved(displayId); + } + } + @GuardedBy("mSyncRoot") private void disableExternalDisplayLocked(@NonNull final LogicalDisplay logicalDisplay) { - if (!isExternalDisplay(logicalDisplay)) { + if (!isExternalDisplayLocked(logicalDisplay)) { return; } @@ -245,6 +303,8 @@ class ExternalDisplayPolicy { mLogicalDisplayMapper.setDisplayEnabledLocked(logicalDisplay, /*enabled=*/ false); + mExternalDisplayStatsService.onDisplayDisabled(logicalDisplay.getDisplayIdLocked()); + if (DEBUG) { Slog.d(TAG, "disableExternalDisplayLocked complete" + " displayId=" + logicalDisplay.getDisplayIdLocked()); diff --git a/services/core/java/com/android/server/display/ExternalDisplayStatsService.java b/services/core/java/com/android/server/display/ExternalDisplayStatsService.java new file mode 100644 index 000000000000..f6f23d9f01d1 --- /dev/null +++ b/services/core/java/com/android/server/display/ExternalDisplayStatsService.java @@ -0,0 +1,662 @@ +/* + * Copyright (C) 2023 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.display; + +import static android.media.AudioDeviceInfo.TYPE_HDMI; +import static android.media.AudioDeviceInfo.TYPE_HDMI_ARC; +import static android.media.AudioDeviceInfo.TYPE_USB_DEVICE; +import static android.provider.Settings.Global.DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.media.AudioManager; +import android.media.AudioManager.AudioPlaybackCallback; +import android.media.AudioPlaybackConfiguration; +import android.os.Handler; +import android.os.PowerManager; +import android.provider.Settings; +import android.util.Slog; +import android.util.SparseIntArray; +import android.view.Display; +import android.view.DisplayInfo; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.FrameworkStatsLog; +import com.android.server.display.utils.DebugUtils; + +import java.util.List; + + +/** + * Manages metrics logging for external display. + */ +public final class ExternalDisplayStatsService { + private static final String TAG = "ExternalDisplayStatsService"; + // To enable these logs, run: + // 'adb shell setprop persist.log.tag.ExternalDisplayStatsService DEBUG && adb reboot' + private static final boolean DEBUG = DebugUtils.isDebuggable(TAG); + + private static final int INVALID_DISPLAYS_COUNT = -1; + private static final int DISCONNECTED_STATE = + FrameworkStatsLog.EXTERNAL_DISPLAY_STATE_CHANGED__STATE__DISCONNECTED; + private static final int CONNECTED_STATE = + FrameworkStatsLog.EXTERNAL_DISPLAY_STATE_CHANGED__STATE__CONNECTED; + private static final int MIRRORING_STATE = + FrameworkStatsLog.EXTERNAL_DISPLAY_STATE_CHANGED__STATE__MIRRORING; + private static final int EXTENDED_STATE = + FrameworkStatsLog.EXTERNAL_DISPLAY_STATE_CHANGED__STATE__EXTENDED; + private static final int PRESENTATION_WHILE_MIRRORING = + FrameworkStatsLog.EXTERNAL_DISPLAY_STATE_CHANGED__STATE__PRESENTATION_WHILE_MIRRORING; + private static final int PRESENTATION_WHILE_EXTENDED = + FrameworkStatsLog.EXTERNAL_DISPLAY_STATE_CHANGED__STATE__PRESENTATION_WHILE_EXTENDED; + private static final int PRESENTATION_ENDED = + FrameworkStatsLog.EXTERNAL_DISPLAY_STATE_CHANGED__STATE__PRESENTATION_ENDED; + private static final int KEYGUARD = + FrameworkStatsLog.EXTERNAL_DISPLAY_STATE_CHANGED__STATE__KEYGUARD; + private static final int DISABLED_STATE = + FrameworkStatsLog.EXTERNAL_DISPLAY_STATE_CHANGED__STATE__DISABLED; + private static final int AUDIO_SINK_CHANGED = + FrameworkStatsLog.EXTERNAL_DISPLAY_STATE_CHANGED__STATE__AUDIO_SINK_CHANGED; + private static final int ERROR_HOTPLUG_CONNECTION = + FrameworkStatsLog.EXTERNAL_DISPLAY_STATE_CHANGED__STATE__ERROR_HOTPLUG_CONNECTION; + private static final int ERROR_DISPLAYPORT_LINK_FAILED = + FrameworkStatsLog.EXTERNAL_DISPLAY_STATE_CHANGED__STATE__ERROR_DISPLAYPORT_LINK_FAILED; + private static final int ERROR_CABLE_NOT_CAPABLE_DISPLAYPORT = + FrameworkStatsLog + .EXTERNAL_DISPLAY_STATE_CHANGED__STATE__ERROR_CABLE_NOT_CAPABLE_DISPLAYPORT; + + private final Injector mInjector; + + @GuardedBy("mExternalDisplayStates") + private final SparseIntArray mExternalDisplayStates = new SparseIntArray(); + + /** + * Count of interactive external displays or INVALID_DISPLAYS_COUNT, modified only from Handler + */ + private int mInteractiveExternalDisplays; + + /** + * Guards init deinit, modified only from Handler + */ + private boolean mIsInitialized; + + /** + * Whether audio plays on external display, modified only from Handler + */ + private boolean mIsExternalDisplayUsedForAudio; + + private final AudioPlaybackCallback mAudioPlaybackCallback = new AudioPlaybackCallback() { + private final Runnable mLogStateAfterAudioSinkEnabled = + () -> logStateAfterAudioSinkChanged(true); + private final Runnable mLogStateAfterAudioSinkDisabled = + () -> logStateAfterAudioSinkChanged(false); + + @Override + public void onPlaybackConfigChanged(List<AudioPlaybackConfiguration> configs) { + super.onPlaybackConfigChanged(configs); + scheduleAudioSinkChange(isExternalDisplayUsedForAudio(configs)); + } + + private boolean isExternalDisplayUsedForAudio(List<AudioPlaybackConfiguration> configs) { + for (var config : configs) { + var info = config.getAudioDeviceInfo(); + if (config.isActive() && info != null + && info.isSink() + && (info.getType() == TYPE_HDMI + || info.getType() == TYPE_HDMI_ARC + || info.getType() == TYPE_USB_DEVICE)) { + if (DEBUG) { + Slog.d(TAG, "isExternalDisplayUsedForAudio:" + + " use " + info.getProductName() + + " isActive=" + config.isActive() + + " isSink=" + info.isSink() + + " type=" + info.getType()); + } + return true; + } + if (DEBUG) { + // info is null if the device is not available at the time of query. + if (info != null) { + Slog.d(TAG, "isExternalDisplayUsedForAudio:" + + " drop " + info.getProductName() + + " isActive=" + config.isActive() + + " isSink=" + info.isSink() + + " type=" + info.getType()); + } + } + } + return false; + } + + private void scheduleAudioSinkChange(final boolean isAudioOnExternalDisplay) { + if (DEBUG) { + Slog.d(TAG, "scheduleAudioSinkChange:" + + " mIsExternalDisplayUsedForAudio=" + + mIsExternalDisplayUsedForAudio + + " isAudioOnExternalDisplay=" + + isAudioOnExternalDisplay); + } + mInjector.getHandler().removeCallbacks(mLogStateAfterAudioSinkEnabled); + mInjector.getHandler().removeCallbacks(mLogStateAfterAudioSinkDisabled); + final var callback = isAudioOnExternalDisplay ? mLogStateAfterAudioSinkEnabled + : mLogStateAfterAudioSinkDisabled; + if (isAudioOnExternalDisplay) { + mInjector.getHandler().postDelayed(callback, /*delayMillis=*/ 10000L); + } else { + mInjector.getHandler().post(callback); + } + } + }; + + private final BroadcastReceiver mInteractivityReceiver = new BroadcastReceiver() { + /** + * Verifies that there is a change to the mInteractiveExternalDisplays and logs the change. + * Executed within a handler - no need to keep lock on mInteractiveExternalDisplays update. + */ + @Override + public void onReceive(Context context, Intent intent) { + int interactiveDisplaysCount = 0; + synchronized (mExternalDisplayStates) { + if (mExternalDisplayStates.size() == 0) { + return; + } + for (var i = 0; i < mExternalDisplayStates.size(); i++) { + if (mInjector.isInteractive(mExternalDisplayStates.keyAt(i))) { + interactiveDisplaysCount++; + } + } + } + + // For the first time, mInteractiveExternalDisplays is INVALID_DISPLAYS_COUNT(-1) + // which is always not equal to interactiveDisplaysCount. + if (mInteractiveExternalDisplays == interactiveDisplaysCount) { + return; + } else if (0 == interactiveDisplaysCount) { + logExternalDisplayIdleStarted(); + } else if (INVALID_DISPLAYS_COUNT != mInteractiveExternalDisplays) { + // Log Only if mInteractiveExternalDisplays was previously initialised. + // Otherwise no need to log that idle has ended, as we assume it never started. + // This is because, currently for enabling external display, the display must be + // non-idle for the user to press the Mirror/Dismiss dialog button. + logExternalDisplayIdleEnded(); + } + mInteractiveExternalDisplays = interactiveDisplaysCount; + } + }; + + ExternalDisplayStatsService(Context context, Handler handler) { + this(new Injector(context, handler)); + } + + @VisibleForTesting + ExternalDisplayStatsService(Injector injector) { + mInjector = injector; + } + + /** + * Write log on hotplug connection error + */ + public void onHotplugConnectionError() { + logExternalDisplayError(ERROR_HOTPLUG_CONNECTION); + } + + /** + * Write log on DisplayPort link training failure + */ + public void onDisplayPortLinkTrainingFailure() { + logExternalDisplayError(ERROR_DISPLAYPORT_LINK_FAILED); + } + + /** + * Write log on USB cable not capable DisplayPort + */ + public void onCableNotCapableDisplayPort() { + logExternalDisplayError(ERROR_CABLE_NOT_CAPABLE_DISPLAYPORT); + } + + void onDisplayConnected(final LogicalDisplay display) { + DisplayInfo displayInfo = display.getDisplayInfoLocked(); + if (displayInfo == null || displayInfo.type != Display.TYPE_EXTERNAL) { + return; + } + logStateConnected(display.getDisplayIdLocked()); + } + + void onDisplayAdded(int displayId) { + if (mInjector.isExtendedDisplayEnabled()) { + logStateExtended(displayId); + } else { + logStateMirroring(displayId); + } + } + + void onDisplayDisabled(int displayId) { + logStateDisabled(displayId); + } + + void onDisplayDisconnected(int displayId) { + logStateDisconnected(displayId); + } + + /** + * Callback triggered upon presentation window gets added. + */ + void onPresentationWindowAdded(int displayId) { + logExternalDisplayPresentationStarted(displayId); + } + + /** + * Callback triggered upon presentation window gets removed. + */ + void onPresentationWindowRemoved(int displayId) { + logExternalDisplayPresentationEnded(displayId); + } + + @VisibleForTesting + boolean isInteractiveExternalDisplays() { + return mInteractiveExternalDisplays != 0; + } + + @VisibleForTesting + boolean isExternalDisplayUsedForAudio() { + return mIsExternalDisplayUsedForAudio; + } + + private void logExternalDisplayError(int errorType) { + final int countOfExternalDisplays; + synchronized (mExternalDisplayStates) { + countOfExternalDisplays = mExternalDisplayStates.size(); + } + + mInjector.writeLog(FrameworkStatsLog.EXTERNAL_DISPLAY_STATE_CHANGED, + errorType, countOfExternalDisplays, + mIsExternalDisplayUsedForAudio); + if (DEBUG) { + Slog.d(TAG, "logExternalDisplayError" + + " countOfExternalDisplays=" + countOfExternalDisplays + + " errorType=" + errorType + + " mIsExternalDisplayUsedForAudio=" + + mIsExternalDisplayUsedForAudio); + } + } + + private void scheduleInit() { + mInjector.getHandler().post(() -> { + if (mIsInitialized) { + Slog.e(TAG, "scheduleInit is called but already initialized"); + return; + } + mIsInitialized = true; + var filter = new IntentFilter(); + filter.addAction(Intent.ACTION_SCREEN_OFF); + filter.addAction(Intent.ACTION_SCREEN_ON); + mInteractiveExternalDisplays = INVALID_DISPLAYS_COUNT; + mIsExternalDisplayUsedForAudio = false; + mInjector.registerInteractivityReceiver(mInteractivityReceiver, filter); + mInjector.registerAudioPlaybackCallback(mAudioPlaybackCallback); + }); + } + + private void scheduleDeinit() { + mInjector.getHandler().post(() -> { + if (!mIsInitialized) { + Slog.e(TAG, "scheduleDeinit is called but never initialized"); + return; + } + mIsInitialized = false; + mInjector.unregisterInteractivityReceiver(mInteractivityReceiver); + mInjector.unregisterAudioPlaybackCallback(mAudioPlaybackCallback); + }); + } + + private void logStateConnected(final int displayId) { + final int countOfExternalDisplays, state; + synchronized (mExternalDisplayStates) { + state = mExternalDisplayStates.get(displayId, DISCONNECTED_STATE); + if (state != DISCONNECTED_STATE) { + return; + } + mExternalDisplayStates.put(displayId, CONNECTED_STATE); + countOfExternalDisplays = mExternalDisplayStates.size(); + } + + if (countOfExternalDisplays == 1) { + scheduleInit(); + } + + mInjector.writeLog(FrameworkStatsLog.EXTERNAL_DISPLAY_STATE_CHANGED, + CONNECTED_STATE, countOfExternalDisplays, mIsExternalDisplayUsedForAudio); + if (DEBUG) { + Slog.d(TAG, "logStateConnected" + + " displayId=" + displayId + + " countOfExternalDisplays=" + countOfExternalDisplays + + " currentState=" + state + + " state=" + CONNECTED_STATE + + " mIsExternalDisplayUsedForAudio=" + + mIsExternalDisplayUsedForAudio); + } + } + + private void logStateDisconnected(final int displayId) { + final int countOfExternalDisplays, state; + synchronized (mExternalDisplayStates) { + state = mExternalDisplayStates.get(displayId, DISCONNECTED_STATE); + if (state == DISCONNECTED_STATE) { + if (DEBUG) { + Slog.d(TAG, "logStateDisconnected" + + " displayId=" + displayId + + " already disconnected"); + } + return; + } + countOfExternalDisplays = mExternalDisplayStates.size(); + mExternalDisplayStates.delete(displayId); + } + + mInjector.writeLog(FrameworkStatsLog.EXTERNAL_DISPLAY_STATE_CHANGED, + DISCONNECTED_STATE, countOfExternalDisplays, + mIsExternalDisplayUsedForAudio); + + if (DEBUG) { + Slog.d(TAG, "logStateDisconnected" + + " displayId=" + displayId + + " countOfExternalDisplays=" + countOfExternalDisplays + + " currentState=" + state + + " state=" + DISCONNECTED_STATE + + " mIsExternalDisplayUsedForAudio=" + + mIsExternalDisplayUsedForAudio); + } + + if (countOfExternalDisplays == 1) { + scheduleDeinit(); + } + } + + private void logStateMirroring(final int displayId) { + synchronized (mExternalDisplayStates) { + final int state = mExternalDisplayStates.get(displayId, DISCONNECTED_STATE); + if (state == DISCONNECTED_STATE || state == MIRRORING_STATE) { + return; + } + for (var i = 0; i < mExternalDisplayStates.size(); i++) { + if (mExternalDisplayStates.keyAt(i) != displayId) { + continue; + } + mExternalDisplayStates.put(displayId, MIRRORING_STATE); + mInjector.writeLog(FrameworkStatsLog.EXTERNAL_DISPLAY_STATE_CHANGED, + MIRRORING_STATE, i + 1, mIsExternalDisplayUsedForAudio); + if (DEBUG) { + Slog.d(TAG, "logStateMirroring" + + " displayId=" + displayId + + " countOfExternalDisplays=" + (i + 1) + + " currentState=" + state + + " state=" + MIRRORING_STATE + + " mIsExternalDisplayUsedForAudio=" + + mIsExternalDisplayUsedForAudio); + } + } + } + } + + private void logStateExtended(final int displayId) { + synchronized (mExternalDisplayStates) { + final int state = mExternalDisplayStates.get(displayId, DISCONNECTED_STATE); + if (state == DISCONNECTED_STATE || state == EXTENDED_STATE) { + return; + } + for (var i = 0; i < mExternalDisplayStates.size(); i++) { + if (mExternalDisplayStates.keyAt(i) != displayId) { + continue; + } + mExternalDisplayStates.put(displayId, EXTENDED_STATE); + mInjector.writeLog(FrameworkStatsLog.EXTERNAL_DISPLAY_STATE_CHANGED, + EXTENDED_STATE, i + 1, mIsExternalDisplayUsedForAudio); + if (DEBUG) { + Slog.d(TAG, "logStateExtended" + + " displayId=" + displayId + + " countOfExternalDisplays=" + (i + 1) + + " currentState=" + state + + " state=" + EXTENDED_STATE + + " mIsExternalDisplayUsedForAudio=" + + mIsExternalDisplayUsedForAudio); + } + } + } + } + + private void logStateDisabled(final int displayId) { + synchronized (mExternalDisplayStates) { + final int state = mExternalDisplayStates.get(displayId, DISCONNECTED_STATE); + if (state == DISCONNECTED_STATE || state == DISABLED_STATE) { + return; + } + for (var i = 0; i < mExternalDisplayStates.size(); i++) { + if (mExternalDisplayStates.keyAt(i) != displayId) { + continue; + } + mExternalDisplayStates.put(displayId, DISABLED_STATE); + mInjector.writeLog(FrameworkStatsLog.EXTERNAL_DISPLAY_STATE_CHANGED, + DISABLED_STATE, i + 1, mIsExternalDisplayUsedForAudio); + if (DEBUG) { + Slog.d(TAG, "logStateDisabled" + + " displayId=" + displayId + + " countOfExternalDisplays=" + (i + 1) + + " currentState=" + state + + " state=" + DISABLED_STATE + + " mIsExternalDisplayUsedForAudio=" + + mIsExternalDisplayUsedForAudio); + } + } + } + } + + private void logExternalDisplayPresentationStarted(int displayId) { + final int countOfExternalDisplays, state; + synchronized (mExternalDisplayStates) { + state = mExternalDisplayStates.get(displayId, DISCONNECTED_STATE); + if (state == DISCONNECTED_STATE) { + return; + } + countOfExternalDisplays = mExternalDisplayStates.size(); + } + + final var newState = mInjector.isExtendedDisplayEnabled() ? PRESENTATION_WHILE_EXTENDED + : PRESENTATION_WHILE_MIRRORING; + mInjector.writeLog(FrameworkStatsLog.EXTERNAL_DISPLAY_STATE_CHANGED, + newState, countOfExternalDisplays, + mIsExternalDisplayUsedForAudio); + if (DEBUG) { + Slog.d(TAG, "logExternalDisplayPresentationStarted" + + " state=" + state + + " newState=" + newState + + " mIsExternalDisplayUsedForAudio=" + + mIsExternalDisplayUsedForAudio); + } + } + + private void logExternalDisplayPresentationEnded(int displayId) { + final int countOfExternalDisplays, state; + synchronized (mExternalDisplayStates) { + state = mExternalDisplayStates.get(displayId, DISCONNECTED_STATE); + if (state == DISCONNECTED_STATE) { + return; + } + countOfExternalDisplays = mExternalDisplayStates.size(); + } + + mInjector.writeLog(FrameworkStatsLog.EXTERNAL_DISPLAY_STATE_CHANGED, + PRESENTATION_ENDED, countOfExternalDisplays, + mIsExternalDisplayUsedForAudio); + if (DEBUG) { + Slog.d(TAG, "logExternalDisplayPresentationEnded" + + " state=" + state + + " countOfExternalDisplays=" + countOfExternalDisplays + + " mIsExternalDisplayUsedForAudio=" + + mIsExternalDisplayUsedForAudio); + } + } + + private void logExternalDisplayIdleStarted() { + synchronized (mExternalDisplayStates) { + for (var i = 0; i < mExternalDisplayStates.size(); i++) { + mInjector.writeLog(FrameworkStatsLog.EXTERNAL_DISPLAY_STATE_CHANGED, + KEYGUARD, i + 1, mIsExternalDisplayUsedForAudio); + if (DEBUG) { + final int displayId = mExternalDisplayStates.keyAt(i); + final int state = mExternalDisplayStates.get(displayId, DISCONNECTED_STATE); + Slog.d(TAG, "logExternalDisplayIdleStarted" + + " displayId=" + displayId + + " currentState=" + state + + " countOfExternalDisplays=" + (i + 1) + + " state=" + KEYGUARD + + " mIsExternalDisplayUsedForAudio=" + + mIsExternalDisplayUsedForAudio); + } + } + } + } + + private void logExternalDisplayIdleEnded() { + synchronized (mExternalDisplayStates) { + for (var i = 0; i < mExternalDisplayStates.size(); i++) { + final int displayId = mExternalDisplayStates.keyAt(i); + final int state = mExternalDisplayStates.get(displayId, DISCONNECTED_STATE); + if (state == DISCONNECTED_STATE) { + return; + } + mInjector.writeLog(FrameworkStatsLog.EXTERNAL_DISPLAY_STATE_CHANGED, + state, i + 1, mIsExternalDisplayUsedForAudio); + if (DEBUG) { + Slog.d(TAG, "logExternalDisplayIdleEnded" + + " displayId=" + displayId + + " state=" + state + + " countOfExternalDisplays=" + (i + 1) + + " mIsExternalDisplayUsedForAudio=" + + mIsExternalDisplayUsedForAudio); + } + } + } + } + + /** + * Executed within Handler + */ + private void logStateAfterAudioSinkChanged(boolean enabled) { + if (mIsExternalDisplayUsedForAudio == enabled) { + return; + } + mIsExternalDisplayUsedForAudio = enabled; + int countOfExternalDisplays; + synchronized (mExternalDisplayStates) { + countOfExternalDisplays = mExternalDisplayStates.size(); + } + mInjector.writeLog(FrameworkStatsLog.EXTERNAL_DISPLAY_STATE_CHANGED, + AUDIO_SINK_CHANGED, countOfExternalDisplays, + mIsExternalDisplayUsedForAudio); + if (DEBUG) { + Slog.d(TAG, "logStateAfterAudioSinkChanged" + + " countOfExternalDisplays)=" + + countOfExternalDisplays + + " mIsExternalDisplayUsedForAudio=" + + mIsExternalDisplayUsedForAudio); + } + } + + /** + * Implements necessary functionality for {@link ExternalDisplayStatsService} + */ + static class Injector { + @NonNull + private final Context mContext; + @NonNull + private final Handler mHandler; + @Nullable + private AudioManager mAudioManager; + @Nullable + private PowerManager mPowerManager; + + Injector(@NonNull Context context, @NonNull Handler handler) { + mContext = context; + mHandler = handler; + } + + boolean isExtendedDisplayEnabled() { + try { + return 0 != Settings.Global.getInt( + mContext.getContentResolver(), + DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS, 0); + } catch (Throwable e) { + // Some services might not be initialised yet to be able to call getInt + return false; + } + } + + void registerInteractivityReceiver(BroadcastReceiver interactivityReceiver, + IntentFilter filter) { + mContext.registerReceiver(interactivityReceiver, filter, /*broadcastPermission=*/ null, + mHandler, Context.RECEIVER_NOT_EXPORTED); + } + + void unregisterInteractivityReceiver(BroadcastReceiver interactivityReceiver) { + mContext.unregisterReceiver(interactivityReceiver); + } + + void registerAudioPlaybackCallback( + AudioPlaybackCallback audioPlaybackCallback) { + if (mAudioManager == null) { + mAudioManager = mContext.getSystemService(AudioManager.class); + } + if (mAudioManager != null) { + mAudioManager.registerAudioPlaybackCallback(audioPlaybackCallback, mHandler); + } + } + + void unregisterAudioPlaybackCallback( + AudioPlaybackCallback audioPlaybackCallback) { + if (mAudioManager == null) { + mAudioManager = mContext.getSystemService(AudioManager.class); + } + if (mAudioManager != null) { + mAudioManager.unregisterAudioPlaybackCallback(audioPlaybackCallback); + } + } + + boolean isInteractive(int displayId) { + if (mPowerManager == null) { + mPowerManager = mContext.getSystemService(PowerManager.class); + } + // By default it is interactive, unless power manager is initialised and says it is not. + return mPowerManager == null || mPowerManager.isInteractive(displayId); + } + + @NonNull + Handler getHandler() { + return mHandler; + } + + void writeLog(int externalDisplayStateChanged, int event, int numberOfDisplays, + boolean isExternalDisplayUsedForAudio) { + FrameworkStatsLog.write(externalDisplayStateChanged, event, numberOfDisplays, + isExternalDisplayUsedForAudio); + } + } +} diff --git a/services/core/java/com/android/server/display/notifications/DisplayNotificationManager.java b/services/core/java/com/android/server/display/notifications/DisplayNotificationManager.java index 405c14941442..280a7e1e0521 100644 --- a/services/core/java/com/android/server/display/notifications/DisplayNotificationManager.java +++ b/services/core/java/com/android/server/display/notifications/DisplayNotificationManager.java @@ -30,6 +30,7 @@ import android.util.Slog; import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; +import com.android.server.display.ExternalDisplayStatsService; import com.android.server.display.feature.DisplayManagerFlags; /** @@ -45,6 +46,10 @@ public class DisplayNotificationManager implements ConnectedDisplayUsbErrorsDete /** Get {@link ConnectedDisplayUsbErrorsDetector} or null if not available. */ @Nullable ConnectedDisplayUsbErrorsDetector getUsbErrorsDetector(); + + /** Get {@link com.android.server.display.ExternalDisplayStatsService} or null */ + @Nullable + ExternalDisplayStatsService getExternalDisplayStatsService(); } private static final String TAG = "DisplayNotificationManager"; @@ -59,7 +64,10 @@ public class DisplayNotificationManager implements ConnectedDisplayUsbErrorsDete private NotificationManager mNotificationManager; private ConnectedDisplayUsbErrorsDetector mConnectedDisplayUsbErrorsDetector; - public DisplayNotificationManager(final DisplayManagerFlags flags, final Context context) { + private final ExternalDisplayStatsService mExternalDisplayStatsService; + + public DisplayNotificationManager(final DisplayManagerFlags flags, final Context context, + final ExternalDisplayStatsService statsService) { this(flags, context, new Injector() { @Nullable @Override @@ -72,6 +80,12 @@ public class DisplayNotificationManager implements ConnectedDisplayUsbErrorsDete public ConnectedDisplayUsbErrorsDetector getUsbErrorsDetector() { return new ConnectedDisplayUsbErrorsDetector(flags, context); } + + @Nullable + @Override + public ExternalDisplayStatsService getExternalDisplayStatsService() { + return statsService; + } }); } @@ -81,6 +95,7 @@ public class DisplayNotificationManager implements ConnectedDisplayUsbErrorsDete mConnectedDisplayErrorHandlingEnabled = flags.isConnectedDisplayErrorHandlingEnabled(); mContext = context; mInjector = injector; + mExternalDisplayStatsService = injector.getExternalDisplayStatsService(); } /** @@ -111,6 +126,8 @@ public class DisplayNotificationManager implements ConnectedDisplayUsbErrorsDete return; } + mExternalDisplayStatsService.onDisplayPortLinkTrainingFailure(); + sendErrorNotification(createErrorNotification( R.string.connected_display_unavailable_notification_title, R.string.connected_display_unavailable_notification_content, @@ -129,6 +146,8 @@ public class DisplayNotificationManager implements ConnectedDisplayUsbErrorsDete return; } + mExternalDisplayStatsService.onCableNotCapableDisplayPort(); + sendErrorNotification(createErrorNotification( R.string.connected_display_unavailable_notification_title, R.string.connected_display_unavailable_notification_content, @@ -145,6 +164,8 @@ public class DisplayNotificationManager implements ConnectedDisplayUsbErrorsDete return; } + mExternalDisplayStatsService.onHotplugConnectionError(); + sendErrorNotification(createErrorNotification( R.string.connected_display_unavailable_notification_title, R.string.connected_display_unavailable_notification_content, diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index de8d9f96453d..631ebcd2b6e9 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -1511,6 +1511,11 @@ public class WindowManagerService extends IWindowManager.Stub } } + if (type == TYPE_PRESENTATION || type == TYPE_PRIVATE_PRESENTATION) { + mDisplayManagerInternal.onPresentation(displayContent.getDisplay().getDisplayId(), + /*isShown=*/ true); + } + if (type == TYPE_PRIVATE_PRESENTATION && !displayContent.isPrivate()) { ProtoLog.w(WM_ERROR, "Attempted to add private presentation window to a non-private display. " diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java index 68dade0fae3b..f56e50e2e9fd 100644 --- a/services/core/java/com/android/server/wm/WindowState.java +++ b/services/core/java/com/android/server/wm/WindowState.java @@ -2332,6 +2332,11 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP dc.mTapExcludedWindows.remove(this); } + if (type == TYPE_PRESENTATION || type == TYPE_PRIVATE_PRESENTATION) { + mWmService.mDisplayManagerInternal.onPresentation(dc.getDisplay().getDisplayId(), + /*isShown=*/ false); + } + // Remove this window from mTapExcludeProvidingWindows. If it was not registered, this will // not do anything. dc.mTapExcludeProvidingWindows.remove(this); diff --git a/services/tests/displayservicetests/src/com/android/server/display/ExternalDisplayPolicyTest.java b/services/tests/displayservicetests/src/com/android/server/display/ExternalDisplayPolicyTest.java index fea431c5623a..1529a087c284 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/ExternalDisplayPolicyTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/ExternalDisplayPolicyTest.java @@ -25,6 +25,7 @@ import static org.junit.Assume.assumeFalse; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; @@ -97,6 +98,8 @@ public class ExternalDisplayPolicyTest { private LogicalDisplay mMockedLogicalDisplay; @Mock private DisplayNotificationManager mMockedDisplayNotificationManager; + @Mock + private ExternalDisplayStatsService mMockedExternalDisplayStatsService; @Captor private ArgumentCaptor<IThermalEventListener> mThermalEventListenerCaptor; @Captor @@ -126,6 +129,8 @@ public class ExternalDisplayPolicyTest { when(mMockedInjector.getDisplayNotificationManager()).thenReturn( mMockedDisplayNotificationManager); when(mMockedInjector.getHandler()).thenReturn(mHandler); + when(mMockedInjector.getExternalDisplayStatsService()) + .thenReturn(mMockedExternalDisplayStatsService); mExternalDisplayPolicy = new ExternalDisplayPolicy(mMockedInjector); // Initialize mocked logical display @@ -178,12 +183,47 @@ public class ExternalDisplayPolicyTest { assertDisplaySetEnabled(/*enabled=*/ false); // Expected only 1 invocation, upon critical temperature. verify(mMockedDisplayNotificationManager).onHighTemperatureExternalDisplayNotAllowed(); + verify(mMockedExternalDisplayStatsService).onDisplayDisabled(eq(EXTERNAL_DISPLAY_ID)); } @Test - public void testSetEnabledExternalDisplay(@TestParameter final boolean enable) { - mExternalDisplayPolicy.setExternalDisplayEnabledLocked(mMockedLogicalDisplay, enable); - assertDisplaySetEnabled(enable); + public void testSetEnabledExternalDisplay() { + mExternalDisplayPolicy.setExternalDisplayEnabledLocked(mMockedLogicalDisplay, + /*enabled=*/ true); + assertDisplaySetEnabled(/*enabled=*/ true); + } + + @Test + public void testHandleDisplayAdded() { + mExternalDisplayPolicy.handleLogicalDisplayAddedLocked(mMockedLogicalDisplay); + verify(mMockedExternalDisplayStatsService).onDisplayAdded(eq(EXTERNAL_DISPLAY_ID)); + } + + @Test + public void testHandleDisplayDisconnected() { + mExternalDisplayPolicy.handleLogicalDisplayDisconnectedLocked(mMockedLogicalDisplay); + verify(mMockedExternalDisplayStatsService).onDisplayDisconnected(eq(EXTERNAL_DISPLAY_ID)); + } + + @Test + public void testOnPresentationStarted() { + mExternalDisplayPolicy.onPresentation(EXTERNAL_DISPLAY_ID, /*isShown=*/ true); + verify(mMockedExternalDisplayStatsService).onPresentationWindowAdded( + eq(EXTERNAL_DISPLAY_ID)); + } + + @Test + public void testOnPresentationEnded() { + mExternalDisplayPolicy.onPresentation(EXTERNAL_DISPLAY_ID, /*isShown=*/ false); + verify(mMockedExternalDisplayStatsService).onPresentationWindowRemoved( + eq(EXTERNAL_DISPLAY_ID)); + } + + @Test + public void testSetDisabledExternalDisplay() { + mExternalDisplayPolicy.setExternalDisplayEnabledLocked(mMockedLogicalDisplay, + /*enabled=*/ false); + assertDisplaySetEnabled(/*enabled=*/ false); } @Test @@ -191,6 +231,7 @@ public class ExternalDisplayPolicyTest { when(mMockedLogicalDisplay.isEnabledLocked()).thenReturn(false); mExternalDisplayPolicy.handleExternalDisplayConnectedLocked(mMockedLogicalDisplay); assertAskedToEnableDisplay(); + verify(mMockedExternalDisplayStatsService).onDisplayConnected(eq(mMockedLogicalDisplay)); } @Test diff --git a/services/tests/displayservicetests/src/com/android/server/display/ExternalDisplayStatsServiceTest.java b/services/tests/displayservicetests/src/com/android/server/display/ExternalDisplayStatsServiceTest.java new file mode 100644 index 000000000000..98ba9aee406a --- /dev/null +++ b/services/tests/displayservicetests/src/com/android/server/display/ExternalDisplayStatsServiceTest.java @@ -0,0 +1,343 @@ +/* + * Copyright (C) 2024 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.display; + +import static android.media.AudioDeviceInfo.TYPE_BUILTIN_SPEAKER; +import static android.media.AudioDeviceInfo.TYPE_HDMI; +import static android.view.Display.TYPE_EXTERNAL; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.annotation.Nullable; +import android.content.BroadcastReceiver; +import android.media.AudioDeviceInfo; +import android.media.AudioManager.AudioPlaybackCallback; +import android.media.AudioPlaybackConfiguration; +import android.view.DisplayInfo; + +import androidx.test.filters.SmallTest; + +import com.android.internal.util.FrameworkStatsLog; +import com.android.server.testutils.TestHandler; + +import com.google.testing.junit.testparameterinjector.TestParameter; +import com.google.testing.junit.testparameterinjector.TestParameterInjector; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.List; + + +/** + * Tests for {@link ExternalDisplayStatsService} + * Run: atest ExternalDisplayStatsServiceTest + */ +@SmallTest +@RunWith(TestParameterInjector.class) +public class ExternalDisplayStatsServiceTest { + private static final int EXTERNAL_DISPLAY_ID = 2; + + private TestHandler mHandler; + private ExternalDisplayStatsService mExternalDisplayStatsService; + private List<AudioPlaybackConfiguration> mAudioPlaybackConfigsPhoneActive; + private List<AudioPlaybackConfiguration> mAudioPlaybackConfigsHdmiActive; + @Nullable + private AudioPlaybackCallback mAudioPlaybackCallback; + @Nullable + private BroadcastReceiver mInteractivityReceiver; + + @Mock + private ExternalDisplayStatsService.Injector mMockedInjector; + @Mock + private LogicalDisplay mMockedLogicalDisplay; + @Mock + private DisplayInfo mMockedDisplayInfo; + + /** Setup tests. */ + @Before + public void setup() throws Exception { + MockitoAnnotations.initMocks(this); + mHandler = new TestHandler(/*callback=*/ null); + when(mMockedInjector.getHandler()).thenReturn(mHandler); + when(mMockedInjector.isExtendedDisplayEnabled()).thenReturn(false); + when(mMockedLogicalDisplay.getDisplayInfoLocked()).thenReturn(mMockedDisplayInfo); + when(mMockedLogicalDisplay.getDisplayIdLocked()).thenReturn(EXTERNAL_DISPLAY_ID); + when(mMockedInjector.isInteractive(eq(EXTERNAL_DISPLAY_ID))).thenReturn(true); + mMockedDisplayInfo.type = TYPE_EXTERNAL; + doAnswer(invocation -> { + mAudioPlaybackCallback = invocation.getArgument(0); + return null; // void method, so return null + }).when(mMockedInjector).registerAudioPlaybackCallback(any()); + doAnswer(invocation -> { + mInteractivityReceiver = invocation.getArgument(0); + return null; // void method, so return null + }).when(mMockedInjector).registerInteractivityReceiver(any(), any()); + mAudioPlaybackConfigsPhoneActive = createAudioConfigs(/*isPhoneActive=*/ true); + mAudioPlaybackConfigsHdmiActive = createAudioConfigs(/*isPhoneActive=*/ false); + mExternalDisplayStatsService = new ExternalDisplayStatsService(mMockedInjector); + } + + @Test + public void testOnHotplugConnectionError() { + mExternalDisplayStatsService.onHotplugConnectionError(); + verify(mMockedInjector).writeLog( + FrameworkStatsLog.EXTERNAL_DISPLAY_STATE_CHANGED, + FrameworkStatsLog.EXTERNAL_DISPLAY_STATE_CHANGED__STATE__ERROR_HOTPLUG_CONNECTION, + /*numberOfDisplays=*/ 0, + /*isExternalDisplayUsedForAudio=*/ false); + } + + @Test + public void testOnDisplayPortLinkTrainingFailure() { + mExternalDisplayStatsService.onDisplayPortLinkTrainingFailure(); + verify(mMockedInjector).writeLog( + FrameworkStatsLog.EXTERNAL_DISPLAY_STATE_CHANGED, + FrameworkStatsLog + .EXTERNAL_DISPLAY_STATE_CHANGED__STATE__ERROR_DISPLAYPORT_LINK_FAILED, + /*numberOfDisplays=*/ 0, + /*isExternalDisplayUsedForAudio=*/ false); + } + + @Test + public void testOnCableNotCapableDisplayPort() { + mExternalDisplayStatsService.onCableNotCapableDisplayPort(); + verify(mMockedInjector).writeLog( + FrameworkStatsLog.EXTERNAL_DISPLAY_STATE_CHANGED, + FrameworkStatsLog + .EXTERNAL_DISPLAY_STATE_CHANGED__STATE__ERROR_CABLE_NOT_CAPABLE_DISPLAYPORT, + /*numberOfDisplays=*/ 0, + /*isExternalDisplayUsedForAudio=*/ false); + } + + @Test + public void testDisplayConnected() { + mExternalDisplayStatsService.onDisplayConnected(mMockedLogicalDisplay); + mHandler.flush(); + verify(mMockedInjector).registerInteractivityReceiver(any(), any()); + verify(mMockedInjector).registerAudioPlaybackCallback(any()); + verify(mMockedInjector).writeLog(FrameworkStatsLog.EXTERNAL_DISPLAY_STATE_CHANGED, + FrameworkStatsLog.EXTERNAL_DISPLAY_STATE_CHANGED__STATE__CONNECTED, + /*numberOfDisplays=*/ 1, + /*isExternalDisplayUsedForAudio=*/ false); + } + + @Test + public void testDisplayInteractivityChanges( + @TestParameter final boolean isExternalDisplayUsedForAudio) { + mExternalDisplayStatsService.onDisplayConnected(mMockedLogicalDisplay); + mHandler.flush(); + assertThat(mInteractivityReceiver).isNotNull(); + + initAudioPlayback(isExternalDisplayUsedForAudio); + clearInvocations(mMockedInjector); + + // Default is 'interactive', so no log should be written. + mInteractivityReceiver.onReceive(null, null); + assertThat(mExternalDisplayStatsService.isInteractiveExternalDisplays()).isTrue(); + verify(mMockedInjector, never()).writeLog(anyInt(), anyInt(), anyInt(), anyBoolean()); + + // Change to non-interactive should produce log + when(mMockedInjector.isInteractive(eq(EXTERNAL_DISPLAY_ID))).thenReturn(false); + mInteractivityReceiver.onReceive(null, null); + assertThat(mExternalDisplayStatsService.isInteractiveExternalDisplays()).isFalse(); + verify(mMockedInjector).writeLog(FrameworkStatsLog.EXTERNAL_DISPLAY_STATE_CHANGED, + FrameworkStatsLog.EXTERNAL_DISPLAY_STATE_CHANGED__STATE__KEYGUARD, + /*numberOfDisplays=*/ 1, + isExternalDisplayUsedForAudio); + clearInvocations(mMockedInjector); + + // Change back to interactive should produce log + when(mMockedInjector.isInteractive(eq(EXTERNAL_DISPLAY_ID))).thenReturn(true); + mInteractivityReceiver.onReceive(null, null); + assertThat(mExternalDisplayStatsService.isInteractiveExternalDisplays()).isTrue(); + verify(mMockedInjector).writeLog(FrameworkStatsLog.EXTERNAL_DISPLAY_STATE_CHANGED, + FrameworkStatsLog.EXTERNAL_DISPLAY_STATE_CHANGED__STATE__CONNECTED, + /*numberOfDisplays=*/ 1, + isExternalDisplayUsedForAudio); + } + + @Test + public void testAudioPlaybackChanges() { + mExternalDisplayStatsService.onDisplayConnected(mMockedLogicalDisplay); + mHandler.flush(); + assertThat(mAudioPlaybackCallback).isNotNull(); + + mAudioPlaybackCallback.onPlaybackConfigChanged(mAudioPlaybackConfigsPhoneActive); + mHandler.flush(); + assertThat(mExternalDisplayStatsService.isExternalDisplayUsedForAudio()).isFalse(); + + mAudioPlaybackCallback.onPlaybackConfigChanged(mAudioPlaybackConfigsHdmiActive); + mHandler.flush(); + assertThat(mExternalDisplayStatsService.isExternalDisplayUsedForAudio()).isTrue(); + } + @Test + public void testOnDisplayAddedMirroring( + @TestParameter final boolean isExternalDisplayUsedForAudio) { + mExternalDisplayStatsService.onDisplayConnected(mMockedLogicalDisplay); + mHandler.flush(); + initAudioPlayback(isExternalDisplayUsedForAudio); + clearInvocations(mMockedInjector); + + mExternalDisplayStatsService.onDisplayAdded(EXTERNAL_DISPLAY_ID); + verify(mMockedInjector).writeLog(FrameworkStatsLog.EXTERNAL_DISPLAY_STATE_CHANGED, + FrameworkStatsLog.EXTERNAL_DISPLAY_STATE_CHANGED__STATE__MIRRORING, + /*numberOfDisplays=*/ 1, + isExternalDisplayUsedForAudio); + } + + @Test + public void testOnDisplayAddedExtended( + @TestParameter final boolean isExternalDisplayUsedForAudio) { + mExternalDisplayStatsService.onDisplayConnected(mMockedLogicalDisplay); + mHandler.flush(); + initAudioPlayback(isExternalDisplayUsedForAudio); + clearInvocations(mMockedInjector); + + when(mMockedInjector.isExtendedDisplayEnabled()).thenReturn(true); + mExternalDisplayStatsService.onDisplayAdded(EXTERNAL_DISPLAY_ID); + verify(mMockedInjector).writeLog(FrameworkStatsLog.EXTERNAL_DISPLAY_STATE_CHANGED, + FrameworkStatsLog.EXTERNAL_DISPLAY_STATE_CHANGED__STATE__EXTENDED, + /*numberOfDisplays=*/ 1, + isExternalDisplayUsedForAudio); + } + + @Test + public void testOnDisplayDisabled( + @TestParameter final boolean isExternalDisplayUsedForAudio) { + mExternalDisplayStatsService.onDisplayConnected(mMockedLogicalDisplay); + mHandler.flush(); + initAudioPlayback(isExternalDisplayUsedForAudio); + mExternalDisplayStatsService.onDisplayAdded(EXTERNAL_DISPLAY_ID); + clearInvocations(mMockedInjector); + + mExternalDisplayStatsService.onDisplayDisabled(EXTERNAL_DISPLAY_ID); + verify(mMockedInjector).writeLog(FrameworkStatsLog.EXTERNAL_DISPLAY_STATE_CHANGED, + FrameworkStatsLog.EXTERNAL_DISPLAY_STATE_CHANGED__STATE__DISABLED, + /*numberOfDisplays=*/ 1, + isExternalDisplayUsedForAudio); + } + + @Test + public void testOnDisplayDisconnected( + @TestParameter final boolean isExternalDisplayUsedForAudio) { + mExternalDisplayStatsService.onDisplayConnected(mMockedLogicalDisplay); + mHandler.flush(); + initAudioPlayback(isExternalDisplayUsedForAudio); + clearInvocations(mMockedInjector); + + mExternalDisplayStatsService.onDisplayDisconnected(EXTERNAL_DISPLAY_ID); + verify(mMockedInjector).writeLog(FrameworkStatsLog.EXTERNAL_DISPLAY_STATE_CHANGED, + FrameworkStatsLog.EXTERNAL_DISPLAY_STATE_CHANGED__STATE__DISCONNECTED, + /*numberOfDisplays=*/ 1, + isExternalDisplayUsedForAudio); + mHandler.flush(); + assertThat(mAudioPlaybackCallback).isNotNull(); + assertThat(mInteractivityReceiver).isNotNull(); + verify(mMockedInjector).unregisterAudioPlaybackCallback(eq(mAudioPlaybackCallback)); + verify(mMockedInjector).unregisterInteractivityReceiver(eq(mInteractivityReceiver)); + } + + @Test + public void testOnPresentationWindowAddedWhileMirroring( + @TestParameter final boolean isExternalDisplayUsedForAudio) { + mExternalDisplayStatsService.onDisplayConnected(mMockedLogicalDisplay); + mHandler.flush(); + initAudioPlayback(isExternalDisplayUsedForAudio); + mExternalDisplayStatsService.onDisplayAdded(EXTERNAL_DISPLAY_ID); + clearInvocations(mMockedInjector); + + mExternalDisplayStatsService.onPresentationWindowAdded(EXTERNAL_DISPLAY_ID); + verify(mMockedInjector).writeLog(FrameworkStatsLog.EXTERNAL_DISPLAY_STATE_CHANGED, + FrameworkStatsLog + .EXTERNAL_DISPLAY_STATE_CHANGED__STATE__PRESENTATION_WHILE_MIRRORING, + /*numberOfDisplays=*/ 1, + isExternalDisplayUsedForAudio); + } + + @Test + public void testOnPresentationWindowAddedWhileExtended( + @TestParameter final boolean isExternalDisplayUsedForAudio) { + mExternalDisplayStatsService.onDisplayConnected(mMockedLogicalDisplay); + when(mMockedInjector.isExtendedDisplayEnabled()).thenReturn(true); + mHandler.flush(); + initAudioPlayback(isExternalDisplayUsedForAudio); + clearInvocations(mMockedInjector); + + mExternalDisplayStatsService.onPresentationWindowAdded(EXTERNAL_DISPLAY_ID); + verify(mMockedInjector).writeLog(FrameworkStatsLog.EXTERNAL_DISPLAY_STATE_CHANGED, + FrameworkStatsLog + .EXTERNAL_DISPLAY_STATE_CHANGED__STATE__PRESENTATION_WHILE_EXTENDED, + /*numberOfDisplays=*/ 1, + isExternalDisplayUsedForAudio); + } + + @Test + public void testOnPresentationWindowRemoved( + @TestParameter final boolean isExternalDisplayUsedForAudio) { + mExternalDisplayStatsService.onDisplayConnected(mMockedLogicalDisplay); + mHandler.flush(); + initAudioPlayback(isExternalDisplayUsedForAudio); + clearInvocations(mMockedInjector); + + mExternalDisplayStatsService.onPresentationWindowRemoved(EXTERNAL_DISPLAY_ID); + verify(mMockedInjector).writeLog(FrameworkStatsLog.EXTERNAL_DISPLAY_STATE_CHANGED, + FrameworkStatsLog + .EXTERNAL_DISPLAY_STATE_CHANGED__STATE__PRESENTATION_ENDED, + /*numberOfDisplays=*/ 1, + isExternalDisplayUsedForAudio); + } + + private void initAudioPlayback(boolean isExternalDisplayUsedForAudio) { + assertThat(mAudioPlaybackCallback).isNotNull(); + mAudioPlaybackCallback.onPlaybackConfigChanged( + isExternalDisplayUsedForAudio ? mAudioPlaybackConfigsHdmiActive + : mAudioPlaybackConfigsPhoneActive); + mHandler.flush(); + } + + private List<AudioPlaybackConfiguration> createAudioConfigs(boolean isPhoneActive) { + var mockedConfigHdmi = mock(AudioPlaybackConfiguration.class); + var mockedInfoHdmi = mock(AudioDeviceInfo.class); + when(mockedInfoHdmi.isSink()).thenReturn(true); + when(mockedInfoHdmi.getType()).thenReturn(TYPE_HDMI); + when(mockedConfigHdmi.getAudioDeviceInfo()).thenReturn(mockedInfoHdmi); + when(mockedConfigHdmi.isActive()).thenReturn(!isPhoneActive); + + var mockedInfoPhone = mock(AudioDeviceInfo.class); + var mockedConfigPhone = mock(AudioPlaybackConfiguration.class); + when(mockedInfoPhone.isSink()).thenReturn(true); + when(mockedInfoPhone.getType()).thenReturn(TYPE_BUILTIN_SPEAKER); + when(mockedConfigPhone.getAudioDeviceInfo()).thenReturn(mockedInfoPhone); + when(mockedConfigPhone.isActive()).thenReturn(isPhoneActive); + return List.of(mockedConfigHdmi, mockedConfigPhone); + } +} diff --git a/services/tests/displayservicetests/src/com/android/server/display/notifications/DisplayNotificationManagerTest.java b/services/tests/displayservicetests/src/com/android/server/display/notifications/DisplayNotificationManagerTest.java index 4efd15c49bb5..d6c8ceb7ea6f 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/notifications/DisplayNotificationManagerTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/notifications/DisplayNotificationManagerTest.java @@ -34,6 +34,7 @@ import android.app.NotificationManager; import androidx.test.core.app.ApplicationProvider; import androidx.test.filters.SmallTest; +import com.android.server.display.ExternalDisplayStatsService; import com.android.server.display.feature.DisplayManagerFlags; import com.android.server.display.notifications.DisplayNotificationManager.Injector; @@ -60,6 +61,8 @@ public class DisplayNotificationManagerTest { @Mock private NotificationManager mMockedNotificationManager; @Mock + private ExternalDisplayStatsService mMockedExternalDisplayStatsService; + @Mock private DisplayManagerFlags mMockedFlags; @Captor private ArgumentCaptor<String> mNotifyTagCaptor; @@ -88,6 +91,7 @@ public class DisplayNotificationManagerTest { /*isErrorHandlingEnabled=*/ true); dnm.onHotplugConnectionError(); assertExpectedNotification(); + verify(mMockedExternalDisplayStatsService).onHotplugConnectionError(); } @Test @@ -96,6 +100,7 @@ public class DisplayNotificationManagerTest { /*isErrorHandlingEnabled=*/ true); dnm.onDisplayPortLinkTrainingFailure(); assertExpectedNotification(); + verify(mMockedExternalDisplayStatsService).onDisplayPortLinkTrainingFailure(); } @Test @@ -104,6 +109,7 @@ public class DisplayNotificationManagerTest { /*isErrorHandlingEnabled=*/ true); dnm.onCableNotCapableDisplayPort(); assertExpectedNotification(); + verify(mMockedExternalDisplayStatsService).onCableNotCapableDisplayPort(); } @Test @@ -124,11 +130,40 @@ public class DisplayNotificationManagerTest { verify(mMockedNotificationManager, never()).notify(anyString(), anyInt(), any()); } + @Test + public void testNoErrorLogging() { + var dnm = createDisplayNotificationManager(/*isNotificationManagerAvailable=*/ true, + /*isErrorHandlingEnabled=*/ false); + // None of these methods should trigger logging now. + dnm.onHotplugConnectionError(); + dnm.onDisplayPortLinkTrainingFailure(); + dnm.onCableNotCapableDisplayPort(); + verify(mMockedExternalDisplayStatsService, never()).onHotplugConnectionError(); + verify(mMockedExternalDisplayStatsService, never()).onCableNotCapableDisplayPort(); + verify(mMockedExternalDisplayStatsService, never()).onDisplayPortLinkTrainingFailure(); + } + + + @Test + public void testErrorLogging() { + var dnm = createDisplayNotificationManager(/*isNotificationManagerAvailable=*/ true, + /*isErrorHandlingEnabled=*/ true); + // these methods should trigger logging now. + dnm.onHotplugConnectionError(); + verify(mMockedExternalDisplayStatsService).onHotplugConnectionError(); + dnm.onDisplayPortLinkTrainingFailure(); + verify(mMockedExternalDisplayStatsService).onDisplayPortLinkTrainingFailure(); + dnm.onCableNotCapableDisplayPort(); + verify(mMockedExternalDisplayStatsService).onCableNotCapableDisplayPort(); + } + private DisplayNotificationManager createDisplayNotificationManager( final boolean isNotificationManagerAvailable, final boolean isErrorHandlingEnabled) { when(mMockedFlags.isConnectedDisplayErrorHandlingEnabled()).thenReturn( isErrorHandlingEnabled); + when(mMockedInjector.getExternalDisplayStatsService()).thenReturn( + mMockedExternalDisplayStatsService); when(mMockedInjector.getNotificationManager()).thenReturn( (isNotificationManagerAvailable) ? mMockedNotificationManager : null); // Usb errors detector is tested in ConnectedDisplayUsbErrorsDetectorTest |