Merge "Refactor business logic out of SpeedBumpView." into oc-mr1-jetpack-dev
diff --git a/car/src/main/java/androidx/car/moderator/SpeedBumpController.java b/car/src/main/java/androidx/car/moderator/SpeedBumpController.java
new file mode 100644
index 0000000..750a967
--- /dev/null
+++ b/car/src/main/java/androidx/car/moderator/SpeedBumpController.java
@@ -0,0 +1,183 @@
+/*
+ * Copyright (C) 2018 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 androidx.car.moderator;
+
+import android.content.Context;
+import android.graphics.drawable.AnimatedVectorDrawable;
+import android.os.Handler;
+import android.support.annotation.VisibleForTesting;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.animation.Animation;
+import android.view.animation.AnimationUtils;
+import android.widget.ImageView;
+
+import androidx.car.R;
+
+/**
+ * A controller for the actual monitoring of when interaction should be allowed in a
+ * {@link SpeedBumpView}.
+ */
+class SpeedBumpController {
+    /**
+     * The number of permitted actions that are acquired per second that the user has not
+     * interacted with the {@code SpeedBumpView}.
+     */
+    private static final float ACQUIRED_PERMITS_PER_SECOND = 0.5f;
+
+    /** The maximum number of permits that can be acquired when the user is idling. */
+    @VisibleForTesting
+    static final float MAX_PERMIT_POOL = 5f;
+
+    /** The delay between when the permit pool has been depleted and when it begins to refill. */
+    private static final long PERMIT_FILL_DELAY_MS = 600L;
+
+    private final ContentRateLimiter mContentRateLimiter = new ContentRateLimiter(
+            ACQUIRED_PERMITS_PER_SECOND,
+            MAX_PERMIT_POOL,
+            PERMIT_FILL_DELAY_MS);
+
+    /**
+     * Whether or not the user is currently allowed to interact with any child views of
+     * {@code SpeedBumpView}.
+     */
+    private boolean mInteractionPermitted = true;
+
+    private final int mLockOutMessageDurationMs;
+    private final Handler mHandler = new Handler();
+
+    private final Context mContext;
+    private final View mLockoutMessageView;
+    private final ImageView mLockoutImageView;
+
+    /**
+     * Creates the {@code SpeedBumpController} and associate it with the given
+     * {@code SpeedBumpView}.
+     */
+    SpeedBumpController(SpeedBumpView speedBumpView) {
+        mContext = speedBumpView.getContext();
+
+        LayoutInflater layoutInflater = LayoutInflater.from(mContext);
+        mLockoutMessageView =
+                layoutInflater.inflate(R.layout.lock_out_message, speedBumpView, false);
+        mLockoutImageView = mLockoutMessageView.findViewById(R.id.lock_out_drawable);
+        mLockOutMessageDurationMs =
+                mContext.getResources().getInteger(R.integer.speed_bump_lock_out_duration_ms);
+    }
+
+    /**
+     * Returns the view that is used by this {@code SpeedBumpController} for displaying a lock-out
+     * message saying that further interaction is blocked.
+     *
+     * @return The view that contains the lock-out message.
+     */
+    View getLockoutMessageView() {
+        return mLockoutMessageView;
+    }
+
+    /**
+     * Notifies this {@code SpeedBumpController} that the given {@link MotionEvent} has occurred.
+     * This method will return whether or not further interaction should be allowed.
+     *
+     * @param ev The {@link MotionEvent} that represents a touch event.
+     * @return {@code true} if the touch event should be allowed.
+     */
+    boolean onTouchEvent(MotionEvent ev) {
+        int action = ev.getActionMasked();
+
+        // Check if the user has just finished an MotionEvent and count that as an action. Check
+        // the ContentRateLimiter to see if interaction is currently permitted.
+        if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
+            boolean nextActionPermitted = mContentRateLimiter.tryAcquire();
+
+            // Indicates that this is the first action that is not permitted. In this case, the
+            // child view should at least handle the ACTION_CANCEL or ACTION_UP, so call
+            // super.dispatchTouchEvent(), but lock out further interaction.
+            if (mInteractionPermitted && !nextActionPermitted) {
+                mInteractionPermitted = false;
+                showLockOutMessage();
+                return true;
+            }
+        }
+
+        // Otherwise, return if interaction is permitted.
+        return mInteractionPermitted;
+    }
+
+    /**
+     * Displays a message that informs the user that they are not permitted to interact any further
+     * with the current view.
+     */
+    private void showLockOutMessage() {
+        // If the message is visible, then it's already showing or animating in. So, do nothing.
+        if (mLockoutMessageView.getVisibility() == View.VISIBLE) {
+            return;
+        }
+
+        Animation lockOutMessageIn =
+                AnimationUtils.loadAnimation(mContext, R.anim.lock_out_message_in);
+        lockOutMessageIn.setAnimationListener(new Animation.AnimationListener() {
+            @Override
+            public void onAnimationStart(Animation animation) {
+                mLockoutMessageView.setVisibility(View.VISIBLE);
+            }
+
+            @Override
+            public void onAnimationEnd(Animation animation) {
+                // When the lock-out message is completely shown, let it display for
+                // mLockOutMessageDurationMs milliseconds before hiding it.
+                mHandler.postDelayed(SpeedBumpController.this::hideLockOutMessage,
+                        mLockOutMessageDurationMs);
+            }
+
+            @Override
+            public void onAnimationRepeat(Animation animation) {}
+        });
+
+        mLockoutMessageView.clearAnimation();
+        mLockoutMessageView.startAnimation(lockOutMessageIn);
+        ((AnimatedVectorDrawable) mLockoutImageView.getDrawable()).start();
+    }
+
+    /**
+     * Hides any lock-out messages. Once the message is hidden, interaction with the view is
+     * permitted.
+     */
+    private void hideLockOutMessage() {
+        if (mLockoutMessageView.getVisibility() != View.VISIBLE) {
+            return;
+        }
+
+        Animation lockOutMessageOut =
+                AnimationUtils.loadAnimation(mContext, R.anim.lock_out_message_out);
+        lockOutMessageOut.setAnimationListener(new Animation.AnimationListener() {
+            @Override
+            public void onAnimationStart(Animation animation) {}
+
+            @Override
+            public void onAnimationEnd(Animation animation) {
+                mLockoutMessageView.setVisibility(View.GONE);
+                mInteractionPermitted = true;
+            }
+
+            @Override
+            public void onAnimationRepeat(Animation animation) {}
+        });
+        mLockoutMessageView.startAnimation(lockOutMessageOut);
+    }
+}
diff --git a/car/src/main/java/androidx/car/moderator/SpeedBumpView.java b/car/src/main/java/androidx/car/moderator/SpeedBumpView.java
index 81a97ee..ad65c6a 100644
--- a/car/src/main/java/androidx/car/moderator/SpeedBumpView.java
+++ b/car/src/main/java/androidx/car/moderator/SpeedBumpView.java
@@ -17,19 +17,12 @@
 package androidx.car.moderator;
 
 import android.content.Context;
-import android.graphics.drawable.AnimatedVectorDrawable;
-import android.os.Handler;
+import android.support.annotation.VisibleForTesting;
 import android.util.AttributeSet;
-import android.view.LayoutInflater;
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewGroup;
-import android.view.animation.Animation;
-import android.view.animation.AnimationUtils;
 import android.widget.FrameLayout;
-import android.widget.ImageView;
-
-import androidx.car.R;
 
 /**
  * A wrapping view that will monitor all touch events on its children views and prevent the user
@@ -40,66 +33,36 @@
  * message explaining that they are no longer able to interact with the view is also displayed.
  */
 public class SpeedBumpView extends FrameLayout {
-    /**
-     * The number of permitted actions that are acquired per second that the user has not
-     * interacted with the {@code SpeedBumpView}.
-     */
-    private static final float ACQUIRED_PERMITS_PER_SECOND = 0.5f;
-
-    /** The maximum number of permits that can be acquired when the user is idling. */
-    private static final float MAX_PERMIT_POOL = 5f;
-
-    /** The delay between when the permit pool has been depleted and when it begins to refill. */
-    private static final long PERMIT_FILL_DELAY_MS = 600L;
-
-    private int mLockOutMessageDurationMs;
-
-    private final ContentRateLimiter mContentRateLimiter = new ContentRateLimiter(
-            ACQUIRED_PERMITS_PER_SECOND,
-            MAX_PERMIT_POOL,
-            PERMIT_FILL_DELAY_MS);
-
-    /**
-     * Whether or not the user is currently allowed to interact with any child views of
-     * {@code SpeedBumpView}.
-     */
-    private boolean mInteractionPermitted = true;
-
-    private final Handler mHandler = new Handler();
-
-    private View mLockoutMessageView;
-    private ImageView mLockoutImageView;
+    private final SpeedBumpController mSpeedBumpController;
 
     public SpeedBumpView(Context context) {
         super(context);
-        init();
     }
 
     public SpeedBumpView(Context context, AttributeSet attrs) {
         super(context, attrs);
-        init();
     }
 
     public SpeedBumpView(Context context, AttributeSet attrs, int defStyleAttrs) {
         super(context, attrs, defStyleAttrs);
-        init();
     }
 
     public SpeedBumpView(Context context, AttributeSet attrs, int defStyleAttrs, int defStyleRes) {
         super(context, attrs, defStyleAttrs, defStyleRes);
-        init();
     }
 
-    private void init() {
-        mLockOutMessageDurationMs =
-                getResources().getInteger(R.integer.speed_bump_lock_out_duration_ms);
+    {
+        mSpeedBumpController = new SpeedBumpController(this);
+        addView(mSpeedBumpController.getLockoutMessageView());
+    }
 
-        LayoutInflater layoutInflater = LayoutInflater.from(getContext());
-        mLockoutMessageView = layoutInflater.inflate(R.layout.lock_out_message, this, false);
-        mLockoutImageView = mLockoutMessageView.findViewById(R.id.lock_out_drawable);
-
-        addView(mLockoutMessageView);
-        mLockoutMessageView.bringToFront();
+    /**
+     * Returns the view that is responsible for displaying a message telling the user that they
+     * have been locked out from further interaction.
+     */
+    @VisibleForTesting
+    View getLockOutMessageView() {
+        return mSpeedBumpController.getLockoutMessageView();
     }
 
     @Override
@@ -108,93 +71,13 @@
 
         // Always ensure that the lock out view has the highest Z-index so that it will show
         // above all other views.
-        mLockoutMessageView.bringToFront();
+        mSpeedBumpController.getLockoutMessageView().bringToFront();
     }
 
 
     // Overriding dispatchTouchEvent to intercept all touch events on child views.
     @Override
     public boolean dispatchTouchEvent(MotionEvent ev) {
-        int action = ev.getActionMasked();
-
-        // Check if the user has just finished an MotionEvent and count that as an action. Check
-        // the ContentRateLimiter to see if interaction is currently permitted.
-        if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
-            boolean nextActionPermitted = mContentRateLimiter.tryAcquire();
-
-            // Indicates that this is the first action that is not permitted. In this case, the
-            // child view should at least handle the ACTION_CANCEL or ACTION_UP, so call
-            // super.dispatchTouchEvent(), but lock out further interaction.
-            if (mInteractionPermitted && !nextActionPermitted) {
-                mInteractionPermitted = false;
-                showLockOutMessage();
-                return super.dispatchTouchEvent(ev);
-            }
-        }
-
-        // Otherwise, if interaction permitted, allow child views to handle touch events.
-        return mInteractionPermitted && super.dispatchTouchEvent(ev);
-    }
-
-    /**
-     * Displays a message that informs the user that they are not permitted to interact any further
-     * with the current view.
-     */
-    private void showLockOutMessage() {
-        // If the message is visible, then it's already showing or animating in. So, do nothing.
-        if (mLockoutMessageView.getVisibility() == VISIBLE) {
-            return;
-        }
-
-        Animation lockOutMessageIn =
-                AnimationUtils.loadAnimation(getContext(), R.anim.lock_out_message_in);
-        lockOutMessageIn.setAnimationListener(new Animation.AnimationListener() {
-            @Override
-            public void onAnimationStart(Animation animation) {
-                mLockoutMessageView.setVisibility(VISIBLE);
-            }
-
-            @Override
-            public void onAnimationEnd(Animation animation) {
-                // When the lock-out message is completely shown, let it display for
-                // mLockOutMessageDurationMs milliseconds before hiding it.
-                mHandler.postDelayed(SpeedBumpView.this::hideLockOutMessage,
-                        mLockOutMessageDurationMs);
-            }
-
-            @Override
-            public void onAnimationRepeat(Animation animation) {}
-        });
-
-        mLockoutMessageView.clearAnimation();
-        mLockoutMessageView.startAnimation(lockOutMessageIn);
-        ((AnimatedVectorDrawable) mLockoutImageView.getDrawable()).start();
-    }
-
-    /**
-     * Hides any lock-out messages. Once the message is hidden, interaction with the view is
-     * permitted.
-     */
-    private void hideLockOutMessage() {
-        if (mLockoutMessageView.getVisibility() != VISIBLE) {
-            return;
-        }
-
-        Animation lockOutMessageOut =
-                AnimationUtils.loadAnimation(getContext(), R.anim.lock_out_message_out);
-        lockOutMessageOut.setAnimationListener(new Animation.AnimationListener() {
-            @Override
-            public void onAnimationStart(Animation animation) {}
-
-            @Override
-            public void onAnimationEnd(Animation animation) {
-                mLockoutMessageView.setVisibility(GONE);
-                mInteractionPermitted = true;
-            }
-
-            @Override
-            public void onAnimationRepeat(Animation animation) {}
-        });
-        mLockoutMessageView.startAnimation(lockOutMessageOut);
+        return mSpeedBumpController.onTouchEvent(ev) && super.dispatchTouchEvent(ev);
     }
 }