Fixed QC SeekBar clickable while disabled behavior

Because SeekBars don't respect drawable states for it's disabled
appearance, it needs to actually be disabled to appear correctly.
Because of this (and the fact that the AbsSeekBar parent class overrides
the View.java allowClickWhileDisabled behavior), it needs to be extended
to special case the clickable while disabled state when touched.

Bug: 205891123
Bug: 207175350
Test: manual, atest CarQCLibUnitTests

(cherry picked from commit feda43803acd3a427ad0a52128bf0f081c372693)
Merged-In: I3467334b0c3865dfd8872b6f1723da2eedca5316
Change-Id: I3467334b0c3865dfd8872b6f1723da2eedca5316
diff --git a/car-qc-lib/res/layout/qc_row_view.xml b/car-qc-lib/res/layout/qc_row_view.xml
index 3005175..6656b29 100644
--- a/car-qc-lib/res/layout/qc_row_view.xml
+++ b/car-qc-lib/res/layout/qc_row_view.xml
@@ -120,7 +120,7 @@
             app:layout_constraintEnd_toEndOf="parent"
             app:layout_constraintTop_toBottomOf="@+id/barrier2"
             app:layout_constraintBottom_toBottomOf="parent">
-            <com.android.car.ui.uxr.DrawableStateSeekBar
+            <com.android.car.qc.view.QCSeekBarView
                 android:id="@+id/seekbar"
                 android:layout_width="match_parent"
                 android:layout_height="wrap_content"
diff --git a/car-qc-lib/src/com/android/car/qc/view/QCRowView.java b/car-qc-lib/src/com/android/car/qc/view/QCRowView.java
index e4c3aa3..a11643a 100644
--- a/car-qc-lib/src/com/android/car/qc/view/QCRowView.java
+++ b/car-qc-lib/src/com/android/car/qc/view/QCRowView.java
@@ -76,7 +76,7 @@
     private LinearLayout mSeekBarContainer;
     @Nullable
     private QCSlider mQCSlider;
-    private SeekBar mSeekBar;
+    private QCSeekBarView mSeekBar;
     private QCActionListener mActionListener;
     private boolean mInDirectManipulationMode;
 
@@ -84,7 +84,8 @@
     private final View.OnKeyListener mSeekBarKeyListener = new View.OnKeyListener() {
         @Override
         public boolean onKey(View v, int keyCode, KeyEvent event) {
-            if (mSeekBar == null || !mSeekBar.isEnabled()) {
+            if (mSeekBar == null || (!mSeekBar.isEnabled()
+                    && !mSeekBar.isClickableWhileDisabled())) {
                 return false;
             }
             // Consume nudge events in direct manipulation mode.
@@ -381,8 +382,9 @@
         mSeekBar.setMin(slider.getMin());
         mSeekBar.setMax(slider.getMax());
         mSeekBar.setProgress(slider.getValue());
-        mSeekBar.setEnabled(slider.isEnabled() || slider.isClickableWhileDisabled());
-        CarUiUtils.makeAllViewsEnabled(mSeekBar, slider.isEnabled());
+        mSeekBar.setEnabled(slider.isEnabled());
+        mSeekBar.setClickableWhileDisabled(slider.isClickableWhileDisabled());
+        mSeekBar.setDisabledClickListener(seekBar -> fireAction(slider, new Intent()));
         if (!slider.isEnabled() && mInDirectManipulationMode) {
             setInDirectManipulationMode(mSeekBarContainer, mSeekBar, false);
         }
@@ -391,15 +393,6 @@
         }
         mSeekbarChangeListener.setSlider(slider);
         mSeekBar.setOnSeekBarChangeListener(mSeekbarChangeListener);
-        mSeekBar.setOnTouchListener((v, event) -> {
-            if (!slider.isEnabled()) {
-                if (event.getActionMasked() == MotionEvent.ACTION_UP) {
-                    fireAction(slider, new Intent());
-                }
-                return true;
-            }
-            return false;
-        });
         // set up rotary support
         mSeekBarContainer.setOnKeyListener(mSeekBarKeyListener);
         mSeekBarContainer.setOnFocusChangeListener(mSeekBarFocusChangeListener);
diff --git a/car-qc-lib/src/com/android/car/qc/view/QCSeekBarView.java b/car-qc-lib/src/com/android/car/qc/view/QCSeekBarView.java
new file mode 100644
index 0000000..b13f784
--- /dev/null
+++ b/car-qc-lib/src/com/android/car/qc/view/QCSeekBarView.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2022 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.car.qc.view;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.widget.SeekBar;
+
+import androidx.annotation.Nullable;
+
+import com.android.car.ui.uxr.DrawableStateSeekBar;
+
+import java.util.function.Consumer;
+
+/**
+ * A {@link SeekBar} specifically for Quick Controls that allows for a disabled click action
+ * to execute on {@link MotionEvent.ACTION_UP}.
+ */
+public class QCSeekBarView extends DrawableStateSeekBar {
+    private boolean mClickableWhileDisabled;
+    private Consumer<SeekBar> mDisabledClickListener;
+
+    public QCSeekBarView(Context context) {
+        super(context);
+    }
+
+    public QCSeekBarView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public QCSeekBarView(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+    }
+
+    public QCSeekBarView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        // AbsSeekBar will ignore all touch events if not enabled. If this SeekBar should be
+        // clickable while disabled, the touch event will be handled here.
+        if (!isEnabled() && mClickableWhileDisabled) {
+            if (event.getAction() == MotionEvent.ACTION_UP && mDisabledClickListener != null) {
+                mDisabledClickListener.accept(this);
+            }
+            return true;
+        }
+        return super.onTouchEvent(event);
+    }
+
+    public void setClickableWhileDisabled(boolean clickable) {
+        mClickableWhileDisabled = clickable;
+    }
+
+    public void setDisabledClickListener(@Nullable Consumer<SeekBar> disabledClickListener) {
+        mDisabledClickListener = disabledClickListener;
+    }
+
+    public boolean isClickableWhileDisabled() {
+        return mClickableWhileDisabled;
+    }
+}
diff --git a/car-qc-lib/tests/unit/src/com/android/car/qc/view/QCSeekBarViewTest.java b/car-qc-lib/tests/unit/src/com/android/car/qc/view/QCSeekBarViewTest.java
new file mode 100644
index 0000000..9adbd3b
--- /dev/null
+++ b/car-qc-lib/tests/unit/src/com/android/car/qc/view/QCSeekBarViewTest.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2022 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.car.qc.view;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.view.MotionEvent;
+import android.widget.SeekBar;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.Consumer;
+
+@RunWith(AndroidJUnit4.class)
+public class QCSeekBarViewTest {
+    private final Context mContext = ApplicationProvider.getApplicationContext();
+    private final QCSeekBarView mView = new QCSeekBarView(mContext);
+
+    @Mock
+    private MotionEvent mMotionEvent;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        when(mMotionEvent.getAction()).thenReturn(MotionEvent.ACTION_UP);
+    }
+
+    @Test
+    public void enabled_standardTouchEvent() {
+        assertThat(mView.onTouchEvent(mMotionEvent)).isTrue();
+    }
+
+    @Test
+    public void disabled_standardTouchEvent() {
+        mView.setEnabled(false);
+
+        assertThat(mView.onTouchEvent(mMotionEvent)).isFalse();
+    }
+
+    @Test
+    public void clickableWhileDisabled_customTouchEvent() {
+        mView.setEnabled(false);
+        mView.setClickableWhileDisabled(true);
+
+        assertThat(mView.onTouchEvent(mMotionEvent)).isTrue();
+    }
+
+    @Test
+    public void clickableWhileDisabled_actionDown_doesNotTriggerDisabledClickListener() {
+        AtomicBoolean called = new AtomicBoolean(false);
+        Consumer<SeekBar> disabledClickListener = seekBar -> called.set(true);
+        mView.setEnabled(false);
+        mView.setClickableWhileDisabled(true);
+        mView.setDisabledClickListener(disabledClickListener);
+        when(mMotionEvent.getAction()).thenReturn(MotionEvent.ACTION_DOWN);
+
+        assertThat(mView.onTouchEvent(mMotionEvent)).isTrue();
+        assertThat(called.get()).isFalse();
+    }
+
+    @Test
+    public void clickableWhileDisabled_actionUp_triggersDisabledClickListener() {
+        AtomicBoolean called = new AtomicBoolean(false);
+        Consumer<SeekBar> disabledClickListener = seekBar -> called.set(true);
+        mView.setEnabled(false);
+        mView.setClickableWhileDisabled(true);
+        mView.setDisabledClickListener(disabledClickListener);
+
+        assertThat(mView.onTouchEvent(mMotionEvent)).isTrue();
+        assertThat(called.get()).isTrue();
+    }
+}