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();