summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Oleg Blinnikov <olb@google.com> 2023-12-22 17:14:43 +0000
committer Oleg Blinnikov <olb@google.com> 2024-02-13 12:05:12 +0000
commite2c762196c8088eb9db11689e59326feefdd7d06 (patch)
tree1b48108d95222ae4a55a155a425c6dfc856009b1
parentee804cb324a26b13734b3f2d88f19f441f26a547 (diff)
Add ExternalDisplay metrics logging
Change-Id: I3cbcc6f5cd7e55844bffd31e5f9af6224f5bd555 Test: statsd_testdrive 805 Bug: 310976810 Test: atest ExternalDisplayStatsServiceTest ExternalDisplayPolicyTest DisplayNotificationManagerTest DisplayManagerServiceTest
-rw-r--r--core/java/android/hardware/display/DisplayManagerInternal.java7
-rw-r--r--services/core/java/com/android/server/display/DisplayManagerService.java27
-rw-r--r--services/core/java/com/android/server/display/ExternalDisplayPolicy.java68
-rw-r--r--services/core/java/com/android/server/display/ExternalDisplayStatsService.java662
-rw-r--r--services/core/java/com/android/server/display/notifications/DisplayNotificationManager.java23
-rw-r--r--services/core/java/com/android/server/wm/WindowManagerService.java5
-rw-r--r--services/core/java/com/android/server/wm/WindowState.java5
-rw-r--r--services/tests/displayservicetests/src/com/android/server/display/ExternalDisplayPolicyTest.java47
-rw-r--r--services/tests/displayservicetests/src/com/android/server/display/ExternalDisplayStatsServiceTest.java343
-rw-r--r--services/tests/displayservicetests/src/com/android/server/display/notifications/DisplayNotificationManagerTest.java35
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