Update Rotary Playground for direct manipulation

Bug: 169884295
Test: manual
Change-Id: I2ef1589cf9ae1cc82480b69ad120e6c947df3746
diff --git a/RotaryPlayground/src/com/android/car/rotaryplayground/DirectManipulationHandler.java b/RotaryPlayground/src/com/android/car/rotaryplayground/DirectManipulationHandler.java
index fc06754..29445c5 100644
--- a/RotaryPlayground/src/com/android/car/rotaryplayground/DirectManipulationHandler.java
+++ b/RotaryPlayground/src/com/android/car/rotaryplayground/DirectManipulationHandler.java
@@ -54,9 +54,18 @@
 public class DirectManipulationHandler implements View.OnKeyListener,
         View.OnGenericMotionListener {
 
-    private final DirectManipulationState mDirectManipulationMode;
-    private final View.OnKeyListener mNudgeDelegate;
-    private final View.OnGenericMotionListener mRotationDelegate;
+    /**
+     * Sets the provided {@link DirectManipulationHandler} to the key listener and motion
+     * listener of the provided view.
+     */
+    public static void setDirectManipulationHandler(@Nullable View view,
+            DirectManipulationHandler handler) {
+        if (view == null) {
+            return;
+        }
+        view.setOnKeyListener(handler);
+        view.setOnGenericMotionListener(handler);
+    }
 
     /**
      * A builder for {@link DirectManipulationHandler}.
@@ -65,45 +74,59 @@
         private final DirectManipulationState mDmState;
         private View.OnKeyListener mNudgeDelegate;
         private View.OnGenericMotionListener mRotationDelegate;
+        private View.OnKeyListener mBackDelegate;
 
         public Builder(DirectManipulationState dmState) {
             Preconditions.checkNotNull(dmState);
             this.mDmState = dmState;
         }
 
-        public Builder setNudgeHandler(View.OnKeyListener directionalDelegate) {
-            Preconditions.checkNotNull(directionalDelegate);
-            this.mNudgeDelegate = directionalDelegate;
+        public Builder setNudgeHandler(View.OnKeyListener nudgeDelegate) {
+            Preconditions.checkNotNull(nudgeDelegate);
+            this.mNudgeDelegate = nudgeDelegate;
             return this;
         }
 
-        public Builder setRotationHandler(View.OnGenericMotionListener motionDelegate) {
-            Preconditions.checkNotNull(motionDelegate);
-            this.mRotationDelegate = motionDelegate;
+        public Builder setBackHandler(View.OnKeyListener backDelegate) {
+            Preconditions.checkNotNull(backDelegate);
+            this.mBackDelegate = backDelegate;
+            return this;
+        }
+
+        public Builder setRotationHandler(View.OnGenericMotionListener rotationDelegate) {
+            Preconditions.checkNotNull(rotationDelegate);
+            this.mRotationDelegate = rotationDelegate;
             return this;
         }
 
         public DirectManipulationHandler build() {
             if (mNudgeDelegate == null && mRotationDelegate == null) {
-                throw new IllegalStateException("At least one delegate must be provided.");
+                throw new IllegalStateException("Nudge and/or rotation delegate must be provided.");
             }
-            return new DirectManipulationHandler(mDmState, mNudgeDelegate, mRotationDelegate);
+            return new DirectManipulationHandler(mDmState, mNudgeDelegate, mBackDelegate,
+                    mRotationDelegate);
         }
     }
 
+    private final DirectManipulationState mDirectManipulationMode;
+    private final View.OnKeyListener mNudgeDelegate;
+    private final View.OnKeyListener mBackDelegate;
+    private final View.OnGenericMotionListener mRotationDelegate;
+
     private DirectManipulationHandler(DirectManipulationState dmState,
             @Nullable View.OnKeyListener nudgeDelegate,
+            @Nullable View.OnKeyListener backDelegate,
             @Nullable View.OnGenericMotionListener rotationDelegate) {
-        Preconditions.checkNotNull(dmState);
         mDirectManipulationMode = dmState;
         mNudgeDelegate = nudgeDelegate;
+        mBackDelegate = backDelegate;
         mRotationDelegate = rotationDelegate;
     }
 
     @Override
     public boolean onKey(View view, int keyCode, KeyEvent keyEvent) {
         boolean isActionUp = keyEvent.getAction() == KeyEvent.ACTION_UP;
-        Log.d(L.TAG, "View: " + view + " is handling " + keyCode
+        Log.d(L.TAG, "View: " + view + " is handling " + KeyEvent.keyCodeToString(keyCode)
                 + " and action " + keyEvent.getAction()
                 + " direct manipulation mode is "
                 + (mDirectManipulationMode.isActive() ? "active" : "inactive"));
@@ -121,18 +144,29 @@
                 if (mDirectManipulationMode.isActive() && isActionUp) {
                     mDirectManipulationMode.disable();
                 }
-                return true;
-            default:
-                // This handler is only responsible for behavior during Direct Manipulation
+                // If no delegate is present, silently consume the events.
+                if (mBackDelegate == null) {
+                    return true;
+                }
+
+                return mBackDelegate.onKey(view, keyCode, keyEvent);
+            case KeyEvent.KEYCODE_DPAD_UP:
+            case KeyEvent.KEYCODE_DPAD_DOWN:
+            case KeyEvent.KEYCODE_DPAD_LEFT:
+            case KeyEvent.KEYCODE_DPAD_RIGHT:
+                // This handler is only responsible for nudging behavior during Direct Manipulation
                 // mode. When the mode is disabled, ignore events.
                 if (!mDirectManipulationMode.isActive()) {
                     return false;
                 }
-                // If no delegate present, silently consume the events.
+                // If no delegate is present, silently consume the events.
                 if (mNudgeDelegate == null) {
                     return true;
                 }
                 return mNudgeDelegate.onKey(view, keyCode, keyEvent);
+            default:
+                // Ignore all other key events.
+                return false;
         }
     }
 
@@ -143,7 +177,7 @@
         if (!mDirectManipulationMode.isActive()) {
             return false;
         }
-        // If no delegate present, silently consume the events.
+        // If no delegate is present, silently consume the events.
         if (mRotationDelegate == null) {
             return true;
         }
diff --git a/RotaryPlayground/src/com/android/car/rotaryplayground/DirectManipulationState.java b/RotaryPlayground/src/com/android/car/rotaryplayground/DirectManipulationState.java
index 05d236b..a802c71 100644
--- a/RotaryPlayground/src/com/android/car/rotaryplayground/DirectManipulationState.java
+++ b/RotaryPlayground/src/com/android/car/rotaryplayground/DirectManipulationState.java
@@ -16,7 +16,10 @@
 
 package com.android.car.rotaryplayground;
 
+import static android.view.ViewGroup.FOCUS_AFTER_DESCENDANTS;
+
 import android.graphics.Color;
+import android.graphics.drawable.Drawable;
 import android.view.View;
 import android.view.ViewGroup;
 
@@ -34,19 +37,20 @@
  */
 public class DirectManipulationState {
 
-
     /** Background color of a view when it's in direct manipulation mode. */
     private static final int BACKGROUND_COLOR_IN_DIRECT_MANIPULATION_MODE = Color.BLUE;
 
-    /** Background color of a view when it's not in direct manipulation mode. */
-    private static final int BACKGROUND_COLOR_NOT_IN_DIRECT_MANIPULATION_MODE = Color.TRANSPARENT;
+    /** Indicates that the descendant focusability has not been set. */
+    private static final int UNKNOWN_DESCENDANT_FOCUSABILITY = -1;
 
     /** The view that is in direct manipulation mode, or null if none. */
-    @Nullable private View mViewInDirectManipulationMode;
-
-    private void setStartingView(@Nullable View view) {
-        mViewInDirectManipulationMode = view;
-    }
+    @Nullable
+    private View mViewInDirectManipulationMode;
+    /** The original background of the view in direct manipulation mode. */
+    @Nullable
+    private Drawable mOriginalBackground;
+    /** The original descendant focusability value of the view in direct manipulation mode. */
+    private int mOriginalDescendantFocusability = UNKNOWN_DESCENDANT_FOCUSABILITY;
 
     /**
      * Returns true if Direct Manipulation mode is active, false otherwise.
@@ -62,19 +66,18 @@
      * We generally want to give some kind of visual indication that this change has happened. In
      * this example we change the background color of {@code view}.
      *
-     * @param view - the {@link View} from which we entered into Direct Manipulation mode.
+     * @param view the {@link View} from which we entered into Direct Manipulation mode
      */
     public void enable(@NonNull View view) {
-        /*
-         * A more robust approach would be to fetch the current background color from
-         * the view object and store it back onto the View itself using the {@link
-         * View#setTag(int, java.lang.Object)} API. This could then be fetched back
-         * and used to restore the background color without needing to keep a constant
-         * reference to the color here which could fall out of sync with the xml files.
-         */
+        mViewInDirectManipulationMode = view;
+        mOriginalBackground = view.getBackground();
+        if (mViewInDirectManipulationMode instanceof ViewGroup) {
+            ViewGroup viewGroup = (ViewGroup) mViewInDirectManipulationMode;
+            mOriginalDescendantFocusability = viewGroup.getDescendantFocusability();
+            viewGroup.setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
+        }
         view.setBackgroundColor(BACKGROUND_COLOR_IN_DIRECT_MANIPULATION_MODE);
         DirectManipulationHelper.enableDirectManipulationMode(view, /* enable= */ true);
-        setStartingView(view);
     }
 
     /**
@@ -82,17 +85,18 @@
      * from which we entered into Direct Manipulation mode.
      */
     public void disable() {
-        mViewInDirectManipulationMode.setBackgroundColor(
-                BACKGROUND_COLOR_NOT_IN_DIRECT_MANIPULATION_MODE);
+        mViewInDirectManipulationMode.setBackground(mOriginalBackground);
         DirectManipulationHelper.enableDirectManipulationMode(
                 mViewInDirectManipulationMode, /* enable= */ false);
-        // For ViewGroup objects, restore descendant focusability to FOCUS_BLOCK_DESCENDANTS so
-        // during non-Direct Manipulation mode, aka, general rotary navigation, we don't go
-        // through the individual inner UI elements.
-        if (mViewInDirectManipulationMode instanceof ViewGroup) {
+        // For ViewGroup objects, restore descendant focusability to the previous value.
+        if (mViewInDirectManipulationMode instanceof ViewGroup
+                && mOriginalDescendantFocusability != UNKNOWN_DESCENDANT_FOCUSABILITY) {
             ViewGroup viewGroup = (ViewGroup) mViewInDirectManipulationMode;
             viewGroup.setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS);
         }
-        setStartingView(null);
+
+        mViewInDirectManipulationMode = null;
+        mOriginalBackground = null;
+        mOriginalDescendantFocusability = UNKNOWN_DESCENDANT_FOCUSABILITY;
     }
 }
diff --git a/RotaryPlayground/src/com/android/car/rotaryplayground/RotaryDirectManipulationWidgets.java b/RotaryPlayground/src/com/android/car/rotaryplayground/RotaryDirectManipulationWidgets.java
index 9184597..68d7d0f 100644
--- a/RotaryPlayground/src/com/android/car/rotaryplayground/RotaryDirectManipulationWidgets.java
+++ b/RotaryPlayground/src/com/android/car/rotaryplayground/RotaryDirectManipulationWidgets.java
@@ -16,6 +16,8 @@
 
 package com.android.car.rotaryplayground;
 
+import static com.android.car.rotaryplayground.DirectManipulationHandler.setDirectManipulationHandler;
+
 import android.os.Bundle;
 import android.view.KeyEvent;
 import android.view.LayoutInflater;
@@ -55,7 +57,7 @@
         View view = inflater.inflate(R.layout.rotary_direct_manipulation, container, false);
 
         DirectManipulationView dmv = view.findViewById(R.id.direct_manipulation_view);
-        registerDirectManipulationHandler(dmv,
+        setDirectManipulationHandler(dmv,
                 new DirectManipulationHandler.Builder(mDirectManipulationMode)
                         .setNudgeHandler(new DirectManipulationView.NudgeHandler())
                         .setRotationHandler(new DirectManipulationView.RotationHandler())
@@ -63,7 +65,7 @@
 
 
         TimePicker spinnerTimePicker = view.findViewById(R.id.spinner_time_picker);
-        registerDirectManipulationHandler(spinnerTimePicker,
+        setDirectManipulationHandler(spinnerTimePicker,
                 new DirectManipulationHandler.Builder(mDirectManipulationMode)
                         .setNudgeHandler(new TimePickerNudgeHandler())
                         .build());
@@ -71,11 +73,15 @@
         DirectManipulationHandler numberPickerListener =
                 new DirectManipulationHandler.Builder(mDirectManipulationMode)
                         .setNudgeHandler(new NumberPickerNudgeHandler())
+                        .setBackHandler((v, keyCode, event) -> {
+                            spinnerTimePicker.requestFocus();
+                            return true;
+                        })
                         .setRotationHandler((v, motionEvent) -> {
-                            float scroll = motionEvent.getAxisValue(MotionEvent.AXIS_SCROLL);
                             View focusedView = v.findFocus();
                             if (focusedView instanceof NumberPicker) {
                                 NumberPicker numberPicker = (NumberPicker) focusedView;
+                                float scroll = motionEvent.getAxisValue(MotionEvent.AXIS_SCROLL);
                                 numberPicker.setValue(numberPicker.getValue() + Math.round(scroll));
                                 return true;
                             }
@@ -86,10 +92,10 @@
         List<NumberPicker> numberPickers = new ArrayList<>();
         getNumberPickerDescendants(numberPickers, spinnerTimePicker);
         for (int i = 0; i < numberPickers.size(); i++) {
-            registerDirectManipulationHandler(numberPickers.get(i), numberPickerListener);
+            setDirectManipulationHandler(numberPickers.get(i), numberPickerListener);
         }
 
-        registerDirectManipulationHandler(view.findViewById(R.id.clock_time_picker),
+        setDirectManipulationHandler(view.findViewById(R.id.clock_time_picker),
                 new DirectManipulationHandler.Builder(
                         mDirectManipulationMode)
                         // TODO(pardis): fix the behavior here. It does not nudge as expected.
@@ -103,13 +109,13 @@
                         })
                         .build());
 
-        registerDirectManipulationHandler(
+        setDirectManipulationHandler(
                 view.findViewById(R.id.seek_bar),
                 new DirectManipulationHandler.Builder(mDirectManipulationMode)
                         .setRotationHandler(new DelegateToA11yScrollRotationHandler())
                         .build());
 
-        registerDirectManipulationHandler(
+        setDirectManipulationHandler(
                 view.findViewById(R.id.radial_time_picker),
                 new DirectManipulationHandler.Builder(mDirectManipulationMode)
                         .setRotationHandler(new DelegateToA11yScrollRotationHandler())
@@ -129,23 +135,6 @@
     }
 
     /**
-     * Register the given {@link DirectManipulationHandler} as both the
-     * {@link View.OnKeyListener} and {@link View.OnGenericMotionListener} for the given
-     * {@link View}.
-     * <p>
-     * Handles a {@link Nullable} {@link View} so that it can be used directly with the output of
-     * methods such as {@code findViewById}.
-     */
-    private void registerDirectManipulationHandler(@Nullable View view,
-            DirectManipulationHandler handler) {
-        if (view == null) {
-            return;
-        }
-        view.setOnKeyListener(handler);
-        view.setOnGenericMotionListener(handler);
-    }
-
-    /**
      * A {@link View.OnGenericMotionListener} implementation that delegates handling the
      * {@link MotionEvent} to the {@link AccessibilityNodeInfo#ACTION_SCROLL_FORWARD}
      * or {@link AccessibilityNodeInfo#ACTION_SCROLL_BACKWARD} depending on the sign of the
@@ -211,8 +200,7 @@
         }
 
         @Override
-        public boolean onKey(View v, int keyCode, KeyEvent event) {
-            boolean isActionUp = event.getAction() == KeyEvent.ACTION_UP;
+        public boolean onKey(View view, int keyCode, KeyEvent event) {
             switch (keyCode) {
                 case KeyEvent.KEYCODE_DPAD_UP:
                 case KeyEvent.KEYCODE_DPAD_DOWN:
@@ -220,10 +208,10 @@
                     return true;
                 case KeyEvent.KEYCODE_DPAD_LEFT:
                 case KeyEvent.KEYCODE_DPAD_RIGHT:
-                    if (isActionUp) {
+                    if (event.getAction() == KeyEvent.ACTION_UP) {
                         int direction = KEYCODE_TO_DIRECTION_MAP.get(keyCode);
-                        View nextView = v.focusSearch(direction);
-                        if (areInTheSameTimePicker(v, nextView)) {
+                        View nextView = view.focusSearch(direction);
+                        if (areInTheSameTimePicker(view, nextView)) {
                             nextView.requestFocus(direction);
                         }
                     }
@@ -239,6 +227,9 @@
             }
             TimePicker view1Ancestor = getTimePickerAncestor(view1);
             TimePicker view2Ancestor = getTimePickerAncestor(view2);
+            if (view1Ancestor == null || view2Ancestor == null) {
+                return false;
+            }
             return view1Ancestor == view2Ancestor;
         }
 
@@ -291,7 +282,6 @@
             if (!(view instanceof TimePicker)) {
                 return false;
             }
-            boolean isActionUp = keyEvent.getAction() == KeyEvent.ACTION_UP;
             switch (keyCode) {
                 case KeyEvent.KEYCODE_DPAD_UP:
                 case KeyEvent.KEYCODE_DPAD_DOWN:
@@ -302,7 +292,7 @@
                     return true;
                 case KeyEvent.KEYCODE_DPAD_LEFT:
                 case KeyEvent.KEYCODE_DPAD_RIGHT:
-                    if (isActionUp) {
+                    if (keyEvent.getAction() == KeyEvent.ACTION_UP) {
                         TimePicker timePicker = (TimePicker) view;
                         List<NumberPicker> numberPickers = new ArrayList<>();
                         getNumberPickerDescendants(numberPickers, timePicker);