Merge "Rotary direct manipulation of SeekBar prefs" into rvc-qpr-dev
diff --git a/src/com/android/car/settings/common/SeekBarPreference.java b/src/com/android/car/settings/common/SeekBarPreference.java
index 308cbd4..cfdb7cd 100644
--- a/src/com/android/car/settings/common/SeekBarPreference.java
+++ b/src/com/android/car/settings/common/SeekBarPreference.java
@@ -23,6 +23,7 @@
 import android.util.AttributeSet;
 import android.util.Log;
 import android.view.KeyEvent;
+import android.view.MotionEvent;
 import android.view.View;
 import android.widget.SeekBar;
 import android.widget.TextView;
@@ -31,6 +32,7 @@
 
 import com.android.car.settings.R;
 import com.android.car.ui.preference.CarUiPreference;
+import com.android.car.ui.utils.DirectManipulationHelper;
 
 /**
  * Car Setting's own version of SeekBarPreference.
@@ -52,13 +54,14 @@
     private boolean mAdjustable; // whether the seekbar should respond to the left/right keys
     private boolean mShowSeekBarValue; // whether to show the seekbar value TextView next to the bar
     private boolean mContinuousUpdate; // whether scrolling provides continuous calls to listener
+    private boolean mInDirectManipulationMode;
 
     private static final String TAG = "SeekBarPreference";
 
     /**
      * Listener reacting to the SeekBar changing value by the user
      */
-    private SeekBar.OnSeekBarChangeListener mSeekBarChangeListener =
+    private final SeekBar.OnSeekBarChangeListener mSeekBarChangeListener =
             new SeekBar.OnSeekBarChangeListener() {
                 @Override
                 public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
@@ -84,35 +87,83 @@
     /**
      * Listener reacting to the user pressing DPAD left/right keys if {@code
      * adjustable} attribute is set to true; it transfers the key presses to the SeekBar
-     * to be handled accordingly.
+     * to be handled accordingly. Also handles entering and exiting direct manipulation
+     * mode for rotary.
      */
-    private View.OnKeyListener mSeekBarKeyListener = new View.OnKeyListener() {
+    private final View.OnKeyListener mSeekBarKeyListener = new View.OnKeyListener() {
         @Override
         public boolean onKey(View v, int keyCode, KeyEvent event) {
-            if (event.getAction() != KeyEvent.ACTION_DOWN) {
+            // Don't allow events through if there is no SeekBar or we're in non-adjustable mode.
+            if (mSeekBar == null || !mAdjustable) {
                 return false;
             }
 
-            if (!mAdjustable && (keyCode == KeyEvent.KEYCODE_DPAD_LEFT
-                    || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT)) {
-                // Right or left keys are pressed when in non-adjustable mode; Skip the keys.
+            // Consume nudge events in direct manipulation mode.
+            if (mInDirectManipulationMode
+                    && (keyCode == KeyEvent.KEYCODE_DPAD_LEFT
+                    || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT
+                    || keyCode == KeyEvent.KEYCODE_DPAD_UP
+                    || keyCode == KeyEvent.KEYCODE_DPAD_DOWN)) {
+                return true;
+            }
+
+            // Handle events to enter or exit direct manipulation mode.
+            if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) {
+                if (event.getAction() == KeyEvent.ACTION_DOWN) {
+                    setInDirectManipulationMode(v, !mInDirectManipulationMode);
+                }
+                return true;
+            }
+            if (keyCode == KeyEvent.KEYCODE_BACK) {
+                if (event.getAction() == KeyEvent.ACTION_DOWN && mInDirectManipulationMode) {
+                    setInDirectManipulationMode(v, false);
+                }
+                return true;
+            }
+
+            // Don't propagate confirm keys to the SeekBar to prevent a ripple effect on the thumb.
+            if (KeyEvent.isConfirmKey(keyCode)) {
                 return false;
             }
 
-            // We don't want to propagate the click keys down to the seekbar view since it will
-            // create the ripple effect for the thumb.
-            if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER || keyCode == KeyEvent.KEYCODE_ENTER) {
-                return false;
+            if (event.getAction() == KeyEvent.ACTION_DOWN) {
+                return mSeekBar.onKeyDown(keyCode, event);
+            } else {
+                return mSeekBar.onKeyUp(keyCode, event);
             }
-
-            if (mSeekBar == null) {
-                Log.e(TAG, "SeekBar view is null and hence cannot be adjusted.");
-                return false;
-            }
-            return mSeekBar.onKeyDown(keyCode, event);
         }
     };
 
+    /** Listener to exit rotary direct manipulation mode when the user switches to touch. */
+    private final View.OnFocusChangeListener mSeekBarFocusChangeListener =
+            (v, hasFocus) -> {
+                if (!hasFocus && mInDirectManipulationMode && mSeekBar != null) {
+                    setInDirectManipulationMode(v, false);
+                }
+            };
+
+    /** Listener to handle rotate events from the rotary controller in direct manipulation mode. */
+    private final View.OnGenericMotionListener mSeekBarScrollListener = (v, event) -> {
+        if (!mInDirectManipulationMode || !mAdjustable || mSeekBar == null) {
+            return false;
+        }
+        int adjustment = Math.round(event.getAxisValue(MotionEvent.AXIS_SCROLL));
+        if (adjustment == 0) {
+            return false;
+        }
+        int count = Math.abs(adjustment);
+        int keyCode = adjustment < 0 ? KeyEvent.KEYCODE_DPAD_LEFT : KeyEvent.KEYCODE_DPAD_RIGHT;
+        KeyEvent downEvent = new KeyEvent(event.getDownTime(), event.getEventTime(),
+                KeyEvent.ACTION_DOWN, keyCode, /* repeat= */ 0);
+        KeyEvent upEvent = new KeyEvent(event.getDownTime(), event.getEventTime(),
+                KeyEvent.ACTION_UP, keyCode, /* repeat= */ 0);
+        for (int i = 0; i < count; i++) {
+            mSeekBar.onKeyDown(keyCode, downEvent);
+            mSeekBar.onKeyUp(keyCode, upEvent);
+        }
+        return true;
+    };
+
     public SeekBarPreference(
             Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
         super(context, attrs, defStyleAttr, defStyleRes);
@@ -149,6 +200,8 @@
     public void onBindViewHolder(PreferenceViewHolder view) {
         super.onBindViewHolder(view);
         view.itemView.setOnKeyListener(mSeekBarKeyListener);
+        view.itemView.setOnFocusChangeListener(mSeekBarFocusChangeListener);
+        view.itemView.setOnGenericMotionListener(mSeekBarScrollListener);
         mSeekBar = (SeekBar) view.findViewById(R.id.seekbar);
         mSeekBarValueTextView = (TextView) view.findViewById(R.id.seekbar_value);
         if (mShowSeekBarValue) {
@@ -314,6 +367,18 @@
         }
     }
 
+    private void setInDirectManipulationMode(View view, boolean enable) {
+        mInDirectManipulationMode = enable;
+        DirectManipulationHelper.enableDirectManipulationMode(mSeekBar, enable);
+        // The preference is highlighted when it's focused with one exception. In direct
+        // manipulation (DM) mode, the SeekBar's thumb is highlighted instead. In DM mode, the
+        // preference and SeekBar are selected. The preference's highlight is drawn when it's
+        // focused but not selected, while the SeekBar's thumb highlight is drawn when the SeekBar
+        // is selected.
+        view.setSelected(enable);
+        mSeekBar.setSelected(enable);
+    }
+
     @Override
     protected Parcelable onSaveInstanceState() {
         final Parcelable superState = super.onSaveInstanceState();