Adding MSDL feedback to the pattern bouncer.

MSDL haptic feedback is added to the bouncer while dragging over cells
and upon successful/failed authentication.

Test: manual. Verified correct haptics play while dragging and when the
  pattern is correct/incorrect.
Flag: com.android.systemui.msdl_feedback
Bug: 361321945

Change-Id: I96d6e27d3e4ce8d2f22e1ef9052299a7dba92e71
diff --git a/core/java/com/android/internal/widget/LockPatternView.java b/core/java/com/android/internal/widget/LockPatternView.java
index 11c220b..0ec55f9 100644
--- a/core/java/com/android/internal/widget/LockPatternView.java
+++ b/core/java/com/android/internal/widget/LockPatternView.java
@@ -120,6 +120,7 @@
     private static final String TAG = "LockPatternView";
 
     private OnPatternListener mOnPatternListener;
+    private ExternalHapticsPlayer mExternalHapticsPlayer;
     @UnsupportedAppUsage
     private final ArrayList<Cell> mPattern = new ArrayList<Cell>(9);
 
@@ -317,6 +318,13 @@
         void onPatternDetected(List<Cell> pattern);
     }
 
+    /** An external haptics player for pattern updates. */
+    public interface ExternalHapticsPlayer{
+
+        /** Perform haptic feedback when a cell is added to the pattern. */
+        void performCellAddedFeedback();
+    }
+
     public LockPatternView(Context context) {
         this(context, null);
     }
@@ -461,6 +469,15 @@
     }
 
     /**
+     * Set the external haptics player for feedback on pattern detection.
+     * @param player The external player.
+     */
+    @UnsupportedAppUsage
+    public void setExternalHapticsPlayer(ExternalHapticsPlayer player) {
+        mExternalHapticsPlayer = player;
+    }
+
+    /**
      * Set the pattern explicitely (rather than waiting for the user to input
      * a pattern).
      * @param displayMode How to display the pattern.
@@ -847,6 +864,16 @@
         return null;
     }
 
+    @Override
+    public boolean performHapticFeedback(int feedbackConstant, int flags) {
+        if (mExternalHapticsPlayer != null) {
+            mExternalHapticsPlayer.performCellAddedFeedback();
+            return true;
+        } else {
+            return super.performHapticFeedback(feedbackConstant, flags);
+        }
+    }
+
     private void addCellToPattern(Cell newCell) {
         mPatternDrawLookup[newCell.getRow()][newCell.getColumn()] = true;
         mPattern.add(newCell);
diff --git a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt
index e2bdc49..bb15208 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt
@@ -30,10 +30,12 @@
 import com.android.systemui.classifier.FalsingCollectorFake
 import com.android.systemui.flags.FakeFeatureFlags
 import com.android.systemui.flags.Flags
+import com.android.systemui.haptics.msdl.msdlPlayer
 import com.android.systemui.res.R
 import com.android.systemui.statusbar.policy.DevicePostureController
 import com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POSTURE_HALF_OPENED
 import com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POSTURE_OPENED
+import com.android.systemui.testKosmos
 import com.android.systemui.user.domain.interactor.SelectedUserInteractor
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.whenever
@@ -89,6 +91,9 @@
 
     @Captor lateinit var postureCallbackCaptor: ArgumentCaptor<DevicePostureController.Callback>
 
+    private val kosmos = testKosmos()
+    private val msdlPlayer = kosmos.msdlPlayer
+
     @Before
     fun setup() {
         MockitoAnnotations.initMocks(this)
@@ -112,7 +117,8 @@
                 mKeyguardMessageAreaControllerFactory,
                 mPostureController,
                 fakeFeatureFlags,
-                mSelectedUserInteractor
+                mSelectedUserInteractor,
+                msdlPlayer,
             )
         mKeyguardPatternView.onAttachedToWindow()
     }
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java
index dd84bc6..92e5432 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java
@@ -271,7 +271,8 @@
                         mKeyguardUpdateMonitor, securityMode, mLockPatternUtils,
                         keyguardSecurityCallback, mLatencyTracker, mFalsingCollector,
                         emergencyButtonController, mMessageAreaControllerFactory,
-                        mDevicePostureController, mFeatureFlags, mSelectedUserInteractor);
+                        mDevicePostureController, mFeatureFlags, mSelectedUserInteractor,
+                        mMSDLPlayer);
             } else if (keyguardInputView instanceof KeyguardPasswordView) {
                 return new KeyguardPasswordViewController((KeyguardPasswordView) keyguardInputView,
                         mKeyguardUpdateMonitor, securityMode, mLockPatternUtils,
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java
index caa74780..f74d93e 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java
@@ -36,6 +36,7 @@
 import com.android.internal.widget.LockscreenCredential;
 import com.android.keyguard.EmergencyButtonController.EmergencyButtonCallback;
 import com.android.keyguard.KeyguardSecurityModel.SecurityMode;
+import com.android.systemui.bouncer.ui.helper.BouncerHapticHelper;
 import com.android.systemui.classifier.FalsingClassifier;
 import com.android.systemui.classifier.FalsingCollector;
 import com.android.systemui.flags.FeatureFlags;
@@ -43,6 +44,8 @@
 import com.android.systemui.statusbar.policy.DevicePostureController;
 import com.android.systemui.user.domain.interactor.SelectedUserInteractor;
 
+import com.google.android.msdl.domain.MSDLPlayer;
+
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -67,6 +70,7 @@
     private LockPatternView mLockPatternView;
     private CountDownTimer mCountdownTimer;
     private AsyncTask<?, ?, ?> mPendingLockCheck;
+    private MSDLPlayer mMSDLPlayer;
 
     private EmergencyButtonCallback mEmergencyButtonCallback = new EmergencyButtonCallback() {
         @Override
@@ -75,6 +79,10 @@
         }
     };
 
+    private final LockPatternView.ExternalHapticsPlayer mExternalHapticsPlayer = () -> {
+        BouncerHapticHelper.INSTANCE.playPatternDotFeedback(mMSDLPlayer, mView);
+    };
+
     /**
      * Useful for clearing out the wrong pattern after a delay
      */
@@ -166,6 +174,10 @@
                 boolean isValidPattern) {
             boolean dismissKeyguard = mSelectedUserInteractor.getSelectedUserId() == userId;
             if (matched) {
+                BouncerHapticHelper.INSTANCE.playMSDLAuthenticationFeedback(
+                        /* authenticationSucceeded= */true,
+                        /* player =*/mMSDLPlayer
+                );
                 getKeyguardSecurityCallback().reportUnlockAttempt(userId, true, 0);
                 if (dismissKeyguard) {
                     mLockPatternView.setDisplayMode(LockPatternView.DisplayMode.Correct);
@@ -173,6 +185,10 @@
                     getKeyguardSecurityCallback().dismiss(true, userId, SecurityMode.Pattern);
                 }
             } else {
+                BouncerHapticHelper.INSTANCE.playMSDLAuthenticationFeedback(
+                        /* authenticationSucceeded= */false,
+                        /* player =*/mMSDLPlayer
+                );
                 mLockPatternView.setDisplayMode(LockPatternView.DisplayMode.Wrong);
                 if (isValidPattern) {
                     getKeyguardSecurityCallback().reportUnlockAttempt(userId, false, timeoutMs);
@@ -200,7 +216,7 @@
             EmergencyButtonController emergencyButtonController,
             KeyguardMessageAreaController.Factory messageAreaControllerFactory,
             DevicePostureController postureController, FeatureFlags featureFlags,
-            SelectedUserInteractor selectedUserInteractor) {
+            SelectedUserInteractor selectedUserInteractor, MSDLPlayer msdlPlayer) {
         super(view, securityMode, keyguardSecurityCallback, emergencyButtonController,
                 messageAreaControllerFactory, featureFlags, selectedUserInteractor);
         mKeyguardUpdateMonitor = keyguardUpdateMonitor;
@@ -212,6 +228,7 @@
                 featureFlags.isEnabled(LOCKSCREEN_ENABLE_LANDSCAPE));
         mLockPatternView = mView.findViewById(R.id.lockPatternView);
         mPostureController = postureController;
+        mMSDLPlayer = msdlPlayer;
     }
 
     @Override
@@ -249,6 +266,7 @@
         if (deadline != 0) {
             handleAttemptLockout(deadline);
         }
+        mLockPatternView.setExternalHapticsPlayer(mExternalHapticsPlayer);
     }
 
     @Override
@@ -262,6 +280,7 @@
             cancelBtn.setOnClickListener(null);
         }
         mPostureController.removeCallback(mPostureCallback);
+        mLockPatternView.setExternalHapticsPlayer(null);
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/helper/BouncerHapticHelper.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/helper/BouncerHapticHelper.kt
new file mode 100644
index 0000000..1faacff
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/helper/BouncerHapticHelper.kt
@@ -0,0 +1,73 @@
+/*
+ * 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.systemui.bouncer.ui.helper
+
+import android.view.HapticFeedbackConstants
+import android.view.View
+import com.android.keyguard.AuthInteractionProperties
+import com.android.systemui.Flags
+//noinspection CleanArchitectureDependencyViolation: Data layer only referenced for this enum class
+import com.google.android.msdl.data.model.MSDLToken
+import com.google.android.msdl.domain.MSDLPlayer
+
+/** A helper object to deliver haptic feedback in bouncer interactions. */
+object BouncerHapticHelper {
+
+    private val authInteractionProperties = AuthInteractionProperties()
+
+    /**
+     * Deliver MSDL feedback as a result of authenticating through a bouncer.
+     *
+     * @param[authenticationSucceeded] Whether the authentication was successful or not.
+     * @param[player] The [MSDLPlayer] that delivers the correct feedback.
+     */
+    fun playMSDLAuthenticationFeedback(
+        authenticationSucceeded: Boolean,
+        player: MSDLPlayer?,
+    ) {
+        if (player == null || !Flags.msdlFeedback()) {
+            return
+        }
+
+        val token =
+            if (authenticationSucceeded) {
+                MSDLToken.UNLOCK
+            } else {
+                MSDLToken.FAILURE
+            }
+        player.playToken(token, authInteractionProperties)
+    }
+
+    /**
+     * Deliver feedback when dragging through cells in the pattern bouncer. This function can play
+     * MSDL feedback using a [MSDLPlayer], or fallback to a default haptic feedback using the
+     * [View.performHapticFeedback] API and a [View].
+     *
+     * @param[player] [MSDLPlayer] for MSDL feedback.
+     * @param[view] A [View] for default haptic feedback using [View.performHapticFeedback]
+     */
+    fun playPatternDotFeedback(player: MSDLPlayer?, view: View?) {
+        if (player == null || !Flags.msdlFeedback()) {
+            view?.performHapticFeedback(
+                HapticFeedbackConstants.VIRTUAL_KEY,
+                HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING,
+            )
+        } else {
+            player.playToken(MSDLToken.DRAG_INDICATOR)
+        }
+    }
+}