Revert "Revert "Connected display usb errors notification""
This reverts commit db16cfd05624b5539ad4231ac30b870b955097ca.
Reason for revert: Test was lacking USB permission, it is fixed by adding this permission to AndroidManifest
Change-Id: I27589579b76e64d21dfee669ad99bc052d9fa5fa
Bug: 294345033
Bug: 283461472
Test: atest ConnectedDisplayUsbErrorsDetectorTest DisplayManagerServiceTest
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index 41356bd..a2a4e34f 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -6298,6 +6298,11 @@
<!-- 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 39a4dd2..3b00682b 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 5e6ff46..d97c8e7 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 @@
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 @@
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 0000000..f683e81
--- /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 7d83e90..5cdef38 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 @@
/**
* 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 @@
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 @@
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/AndroidManifest.xml b/services/tests/displayservicetests/AndroidManifest.xml
index 55fde00..e71ea26 100644
--- a/services/tests/displayservicetests/AndroidManifest.xml
+++ b/services/tests/displayservicetests/AndroidManifest.xml
@@ -28,6 +28,7 @@
<uses-permission android:name="android.permission.READ_COMPAT_CHANGE_CONFIG" />
<uses-permission android:name="android.permission.READ_DEVICE_CONFIG" />
<uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
+ <uses-permission android:name="android.permission.MANAGE_USB" />
<!-- Permissions needed for DisplayTransformManagerTest -->
<uses-permission android:name="android.permission.CHANGE_CONFIGURATION" />
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 0000000..d5a92cb
--- /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 d7c35ed..1d2034b 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 @@
}
@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 @@
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();