diff options
| author | 2023-10-02 12:25:46 +0000 | |
|---|---|---|
| committer | 2023-10-12 17:15:10 +0000 | |
| commit | 111eefb06167f50d438e1f293decd6a64062d8cc (patch) | |
| tree | 229b2c4479c0dbb79f26d47e4322162ec68d92cf | |
| parent | bb754aee251c12d184c05b11678dc31a0332ad3a (diff) | |
Connected display usb errors notification
Show notification if connected display link training
fails, or usb cable is not displayport capable but connected
device is capable of displayport.
Change-Id: Ia10ca6fdc6dce1d1feedc69a4f3304ce0c1c3e63
Bug: 294345033
Bug: 283461472
Test: atest ConnectedDisplayUsbErrorsDetectorTest
7 files changed, 358 insertions, 4 deletions
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml index 41356bd49514..a2a4e34f3527 100644 --- a/core/res/res/values/strings.xml +++ b/core/res/res/values/strings.xml @@ -6298,6 +6298,11 @@ ul.</string> <!-- Content of connected display unavailable notification. [CHAR LIMIT=NONE] --> <string name="connected_display_unavailable_notification_content">Use a different cable and try again</string> + <!-- Title of cable don't support displays notifications. [CHAR LIMIT=NONE] --> + <string name="connected_display_cable_dont_support_displays_notification_title">Cable may not support displays</string> + <!-- Content of cable don't support displays notification. [CHAR LIMIT=NONE] --> + <string name="connected_display_cable_dont_support_displays_notification_content">Your USB-C cable may not connect to displays properly</string> + <!-- Name of concurrent display notifications. [CHAR LIMIT=NONE] --> <string name="concurrent_display_notification_name">Dual screen</string> <!-- Title of concurrent display active notification. [CHAR LIMIT=NONE] --> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 39a4dd2aed8d..3b00682bd6b3 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -5068,6 +5068,8 @@ <java-symbol type="array" name="device_state_notification_power_save_contents"/> <java-symbol type="string" name="connected_display_unavailable_notification_title"/> <java-symbol type="string" name="connected_display_unavailable_notification_content"/> + <java-symbol type="string" name="connected_display_cable_dont_support_displays_notification_title"/> + <java-symbol type="string" name="connected_display_cable_dont_support_displays_notification_content"/> <java-symbol type="string" name="concurrent_display_notification_name"/> <java-symbol type="string" name="concurrent_display_notification_active_title"/> <java-symbol type="string" name="concurrent_display_notification_active_content"/> diff --git a/services/core/java/com/android/server/display/DisplayManagerShellCommand.java b/services/core/java/com/android/server/display/DisplayManagerShellCommand.java index 5e6ff4683850..d97c8e71c73c 100644 --- a/services/core/java/com/android/server/display/DisplayManagerShellCommand.java +++ b/services/core/java/com/android/server/display/DisplayManagerShellCommand.java @@ -30,7 +30,8 @@ import java.util.Arrays; class DisplayManagerShellCommand extends ShellCommand { private static final String TAG = "DisplayManagerShellCommand"; - private static final String NOTIFICATION_TYPES = "on-hotplug-error"; + private static final String NOTIFICATION_TYPES = + "on-hotplug-error, on-link-training-failure, on-cable-dp-incapable"; private final DisplayManagerService mService; private final DisplayManagerFlags mFlags; @@ -193,6 +194,12 @@ class DisplayManagerShellCommand extends ShellCommand { case "on-hotplug-error": mService.getDisplayNotificationManager().onHotplugConnectionError(); break; + case "on-link-training-failure": + mService.getDisplayNotificationManager().onDisplayPortLinkTrainingFailure(); + break; + case "on-cable-dp-incapable": + mService.getDisplayNotificationManager().onCableNotCapableDisplayPort(); + break; default: getErrPrintWriter().println( "Error: unexpected notification type=" + notificationType + ", use one of: " diff --git a/services/core/java/com/android/server/display/notifications/ConnectedDisplayUsbErrorsDetector.java b/services/core/java/com/android/server/display/notifications/ConnectedDisplayUsbErrorsDetector.java new file mode 100644 index 000000000000..f683e8104889 --- /dev/null +++ b/services/core/java/com/android/server/display/notifications/ConnectedDisplayUsbErrorsDetector.java @@ -0,0 +1,132 @@ +/* + * 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.notifications; + +import static android.hardware.usb.DisplayPortAltModeInfo.DISPLAYPORT_ALT_MODE_STATUS_CAPABLE_DISABLED; +import static android.hardware.usb.DisplayPortAltModeInfo.DISPLAYPORT_ALT_MODE_STATUS_NOT_CAPABLE; +import static android.hardware.usb.DisplayPortAltModeInfo.LINK_TRAINING_STATUS_FAILURE; + +import android.annotation.NonNull; +import android.annotation.SuppressLint; +import android.content.Context; +import android.hardware.usb.DisplayPortAltModeInfo; +import android.hardware.usb.UsbManager; +import android.hardware.usb.UsbManager.DisplayPortAltModeInfoListener; +import android.util.Slog; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.server.display.feature.DisplayManagerFlags; + +/** + * Detects usb issues related to an external display connected. + */ +public class ConnectedDisplayUsbErrorsDetector implements DisplayPortAltModeInfoListener { + private static final String TAG = "ConnectedDisplayUsbErrorsDetector"; + + /** + * Dependency injection for {@link ConnectedDisplayUsbErrorsDetector}. + */ + public interface Injector { + + /** + * @return {@link UsbManager} service. + */ + UsbManager getUsbManager(); + } + + /** + * USB errors listener + */ + public interface Listener { + + /** + * Link training failure callback. + */ + void onDisplayPortLinkTrainingFailure(); + + /** + * DisplayPort capable device plugged-in, but cable is not supporting DisplayPort. + */ + void onCableNotCapableDisplayPort(); + } + + private Listener mListener; + private final Injector mInjector; + private final Context mContext; + private final boolean mIsConnectedDisplayErrorHandlingEnabled; + + ConnectedDisplayUsbErrorsDetector(@NonNull final DisplayManagerFlags flags, + @NonNull final Context context) { + this(flags, context, () -> context.getSystemService(UsbManager.class)); + } + + @VisibleForTesting + ConnectedDisplayUsbErrorsDetector(@NonNull final DisplayManagerFlags flags, + @NonNull final Context context, @NonNull final Injector injector) { + mContext = context; + mInjector = injector; + mIsConnectedDisplayErrorHandlingEnabled = + flags.isConnectedDisplayErrorHandlingEnabled(); + } + + /** Register listener for usb error events. */ + @SuppressLint("AndroidFrameworkRequiresPermission") + void registerListener(final Listener listener) { + if (!mIsConnectedDisplayErrorHandlingEnabled) { + return; + } + + final var usbManager = mInjector.getUsbManager(); + if (usbManager == null) { + Slog.e(TAG, "UsbManager is null"); + return; + } + + mListener = listener; + + try { + usbManager.registerDisplayPortAltModeInfoListener(mContext.getMainExecutor(), this); + } catch (IllegalStateException e) { + Slog.e(TAG, "Failed to register listener", e); + } + } + + /** + * Callback upon changes in {@link DisplayPortAltModeInfo}. + * @param portId String describing the {@link android.hardware.usb.UsbPort} that was changed. + * @param info New {@link DisplayPortAltModeInfo} for the corresponding portId. + */ + @Override + public void onDisplayPortAltModeInfoChanged(@NonNull String portId, + @NonNull DisplayPortAltModeInfo info) { + if (mListener == null) { + return; + } + + if (DISPLAYPORT_ALT_MODE_STATUS_CAPABLE_DISABLED == info.getPartnerSinkStatus() + && DISPLAYPORT_ALT_MODE_STATUS_NOT_CAPABLE == info.getCableStatus() + ) { + mListener.onCableNotCapableDisplayPort(); + return; + } + + if (LINK_TRAINING_STATUS_FAILURE == info.getLinkTrainingStatus()) { + mListener.onDisplayPortLinkTrainingFailure(); + return; + } + } +} 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 7d83e90eda9d..5cdef38cd45c 100644 --- a/services/core/java/com/android/server/display/notifications/DisplayNotificationManager.java +++ b/services/core/java/com/android/server/display/notifications/DisplayNotificationManager.java @@ -34,12 +34,16 @@ import com.android.server.display.feature.DisplayManagerFlags; /** * Manages notifications for {@link com.android.server.display.DisplayManagerService}. */ -public class DisplayNotificationManager { +public class DisplayNotificationManager implements ConnectedDisplayUsbErrorsDetector.Listener { /** Dependency injection interface for {@link DisplayNotificationManager} */ public interface Injector { /** Get {@link NotificationManager} service or null if not available. */ @Nullable NotificationManager getNotificationManager(); + + /** Get {@link ConnectedDisplayUsbErrorsDetector} or null if not available. */ + @Nullable + ConnectedDisplayUsbErrorsDetector getUsbErrorsDetector(); } private static final String TAG = "DisplayNotificationManager"; @@ -52,9 +56,22 @@ public class DisplayNotificationManager { private final Context mContext; private final boolean mConnectedDisplayErrorHandlingEnabled; private NotificationManager mNotificationManager; + private ConnectedDisplayUsbErrorsDetector mConnectedDisplayUsbErrorsDetector; public DisplayNotificationManager(final DisplayManagerFlags flags, final Context context) { - this(flags, context, () -> context.getSystemService(NotificationManager.class)); + this(flags, context, new Injector() { + @Nullable + @Override + public NotificationManager getNotificationManager() { + return context.getSystemService(NotificationManager.class); + } + + @Nullable + @Override + public ConnectedDisplayUsbErrorsDetector getUsbErrorsDetector() { + return new ConnectedDisplayUsbErrorsDetector(flags, context); + } + }); } @VisibleForTesting @@ -75,6 +92,44 @@ public class DisplayNotificationManager { Slog.e(TAG, "onBootCompleted: NotificationManager is null"); return; } + + mConnectedDisplayUsbErrorsDetector = mInjector.getUsbErrorsDetector(); + if (mConnectedDisplayUsbErrorsDetector != null) { + mConnectedDisplayUsbErrorsDetector.registerListener(this); + } + } + + /** + * Display error notification upon DisplayPort link training failure. + */ + @Override + public void onDisplayPortLinkTrainingFailure() { + if (!mConnectedDisplayErrorHandlingEnabled) { + Slog.d(TAG, "onDisplayPortLinkTrainingFailure:" + + " mConnectedDisplayErrorHandlingEnabled is false"); + return; + } + + sendErrorNotification(createErrorNotification( + R.string.connected_display_unavailable_notification_title, + R.string.connected_display_unavailable_notification_content)); + } + + /** + * Display error notification upon cable not capable of DisplayPort connected to a device + * capable of DisplayPort. + */ + @Override + public void onCableNotCapableDisplayPort() { + if (!mConnectedDisplayErrorHandlingEnabled) { + Slog.d(TAG, "onCableNotCapableDisplayPort:" + + " mConnectedDisplayErrorHandlingEnabled is false"); + return; + } + + sendErrorNotification(createErrorNotification( + R.string.connected_display_cable_dont_support_displays_notification_title, + R.string.connected_display_cable_dont_support_displays_notification_content)); } /** diff --git a/services/tests/displayservicetests/src/com/android/server/display/notifications/ConnectedDisplayUsbErrorsDetectorTest.java b/services/tests/displayservicetests/src/com/android/server/display/notifications/ConnectedDisplayUsbErrorsDetectorTest.java new file mode 100644 index 000000000000..d5a92cbc927f --- /dev/null +++ b/services/tests/displayservicetests/src/com/android/server/display/notifications/ConnectedDisplayUsbErrorsDetectorTest.java @@ -0,0 +1,130 @@ +/* + * 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.notifications; + +import static android.hardware.usb.DisplayPortAltModeInfo.DISPLAYPORT_ALT_MODE_STATUS_CAPABLE_DISABLED; +import static android.hardware.usb.DisplayPortAltModeInfo.DISPLAYPORT_ALT_MODE_STATUS_ENABLED; +import static android.hardware.usb.DisplayPortAltModeInfo.DISPLAYPORT_ALT_MODE_STATUS_NOT_CAPABLE; +import static android.hardware.usb.DisplayPortAltModeInfo.LINK_TRAINING_STATUS_FAILURE; + +import static org.junit.Assume.assumeFalse; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.hardware.usb.DisplayPortAltModeInfo; +import android.hardware.usb.UsbManager; + +import androidx.test.core.app.ApplicationProvider; +import androidx.test.filters.SmallTest; + +import com.android.server.display.feature.DisplayManagerFlags; +import com.android.server.display.notifications.ConnectedDisplayUsbErrorsDetector.Injector; + +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; + +/** + * Tests for {@link ConnectedDisplayUsbErrorsDetector} + * Run: atest ConnectedDisplayUsbErrorsDetectorTest + */ +@SmallTest +@RunWith(TestParameterInjector.class) +public class ConnectedDisplayUsbErrorsDetectorTest { + @Mock + private Injector mMockedInjector; + @Mock + private UsbManager mMockedUsbManager; + @Mock + private DisplayManagerFlags mMockedFlags; + @Mock + private ConnectedDisplayUsbErrorsDetector.Listener mMockedListener; + + /** Setup tests. */ + @Before + public void setup() throws Exception { + MockitoAnnotations.initMocks(this); + } + + @Test + public void testNoErrorTypes( + @TestParameter final boolean isUsbManagerAvailable, + @TestParameter final boolean isUsbErrorsNotificationEnabled) { + // This is tested in #testErrorOnUsbCableNotCapableDp and #testErrorOnDpLinkTrainingFailure + assumeFalse(isUsbManagerAvailable && isUsbErrorsNotificationEnabled); + var detector = createErrorsDetector(isUsbManagerAvailable, isUsbErrorsNotificationEnabled); + // None of these should trigger an error now. + detector.onDisplayPortAltModeInfoChanged("portId", createInfoOnUsbCableNotCapableDp()); + detector.onDisplayPortAltModeInfoChanged("portId", createInfoOnDpLinkTrainingFailure()); + verify(mMockedUsbManager, never()).registerDisplayPortAltModeInfoListener(any(), any()); + verify(mMockedListener, never()).onCableNotCapableDisplayPort(); + verify(mMockedListener, never()).onDisplayPortLinkTrainingFailure(); + } + + @Test + public void testErrorOnUsbCableNotCapableDp() { + var detector = createErrorsDetector(/*isUsbManagerAvailable=*/ true, + /*isUsbErrorsNotificationEnabled=*/ true); + detector.onDisplayPortAltModeInfoChanged("portId", createInfoOnUsbCableNotCapableDp()); + verify(mMockedUsbManager).registerDisplayPortAltModeInfoListener(any(), any()); + verify(mMockedListener).onCableNotCapableDisplayPort(); + verify(mMockedListener, never()).onDisplayPortLinkTrainingFailure(); + } + + @Test + public void testErrorOnDpLinkTrainingFailure() { + var detector = createErrorsDetector(/*isUsbManagerAvailable=*/ true, + /*isUsbErrorsNotificationEnabled=*/ true); + detector.onDisplayPortAltModeInfoChanged("portId", createInfoOnDpLinkTrainingFailure()); + verify(mMockedUsbManager).registerDisplayPortAltModeInfoListener(any(), any()); + verify(mMockedListener, never()).onCableNotCapableDisplayPort(); + verify(mMockedListener).onDisplayPortLinkTrainingFailure(); + } + + private ConnectedDisplayUsbErrorsDetector createErrorsDetector( + final boolean isUsbManagerAvailable, + final boolean isConnectedDisplayUsbErrorsNotificationEnabled) { + when(mMockedFlags.isConnectedDisplayErrorHandlingEnabled()) + .thenReturn(isConnectedDisplayUsbErrorsNotificationEnabled); + when(mMockedInjector.getUsbManager()).thenReturn( + (isUsbManagerAvailable) ? mMockedUsbManager : null); + var detector = new ConnectedDisplayUsbErrorsDetector(mMockedFlags, + ApplicationProvider.getApplicationContext(), mMockedInjector); + detector.registerListener(mMockedListener); + return detector; + } + + private DisplayPortAltModeInfo createInfoOnUsbCableNotCapableDp() { + return new DisplayPortAltModeInfo( + DISPLAYPORT_ALT_MODE_STATUS_CAPABLE_DISABLED, + DISPLAYPORT_ALT_MODE_STATUS_NOT_CAPABLE, -1, false, 0); + } + + private DisplayPortAltModeInfo createInfoOnDpLinkTrainingFailure() { + return new DisplayPortAltModeInfo( + DISPLAYPORT_ALT_MODE_STATUS_CAPABLE_DISABLED, + DISPLAYPORT_ALT_MODE_STATUS_ENABLED, -1, false, + LINK_TRAINING_STATUS_FAILURE); + } +} 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 d7c35ed3d610..1d2034be4acb 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 @@ -83,14 +83,35 @@ public class DisplayNotificationManagerTest { } @Test + public void testNotificationOnDisplayPortLinkTrainingFailure() { + var dnm = createDisplayNotificationManager(/*isNotificationManagerAvailable=*/ true, + /*isErrorHandlingEnabled=*/ true); + dnm.onDisplayPortLinkTrainingFailure(); + assertExpectedNotification(); + } + + @Test + public void testNotificationOnCableNotCapableDisplayPort() { + var dnm = createDisplayNotificationManager(/*isNotificationManagerAvailable=*/ true, + /*isErrorHandlingEnabled=*/ true); + dnm.onCableNotCapableDisplayPort(); + assertExpectedNotification(); + } + + @Test public void testNoErrorNotification( @TestParameter final boolean isNotificationManagerAvailable, @TestParameter final boolean isErrorHandlingEnabled) { - /* This case is tested by #testNotificationOnHotplugConnectionError */ + /* This case is tested by #testNotificationOnHotplugConnectionError, + #testNotificationOnDisplayPortLinkTrainingFailure, + #testNotificationOnCableNotCapableDisplayPort */ assumeFalse(isNotificationManagerAvailable && isErrorHandlingEnabled); var dnm = createDisplayNotificationManager(isNotificationManagerAvailable, isErrorHandlingEnabled); + // None of these methods should trigger a notification now. dnm.onHotplugConnectionError(); + dnm.onDisplayPortLinkTrainingFailure(); + dnm.onCableNotCapableDisplayPort(); verify(mMockedNotificationManager, never()).notify(anyString(), anyInt(), any()); } @@ -101,6 +122,8 @@ public class DisplayNotificationManagerTest { isErrorHandlingEnabled); when(mMockedInjector.getNotificationManager()).thenReturn( (isNotificationManagerAvailable) ? mMockedNotificationManager : null); + // Usb errors detector is tested in ConnectedDisplayUsbErrorsDetectorTest + when(mMockedInjector.getUsbErrorsDetector()).thenReturn(/* usbErrorsDetector= */ null); final var displayNotificationManager = new DisplayNotificationManager(mMockedFlags, ApplicationProvider.getApplicationContext(), mMockedInjector); displayNotificationManager.onBootCompleted(); |