| /* |
| * 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.car.drivingstate.CarUxRestrictions; |
| import android.content.Context; |
| import android.graphics.drawable.AnimatedVectorDrawable; |
| import android.os.Handler; |
| 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; |
| import androidx.car.utils.CarUxRestrictionsHelper; |
| |
| /** |
| * 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. */ |
| 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 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; |
| |
| private final CarUxRestrictionsHelper mUxRestrictionsHelper; |
| |
| /** |
| * 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); |
| |
| mUxRestrictionsHelper = new CarUxRestrictionsHelper(mContext, |
| carUxRestrictions -> updateUnlimitedModeEnabled(carUxRestrictions)); |
| |
| // By default, no limiting until UXR restrictions kick in. |
| mContentRateLimiter.setUnlimitedMode(true); |
| } |
| |
| /** |
| * Starts this {@code SpeedBumpController} for monitoring any changes in driving restrictions. |
| */ |
| void start() { |
| mUxRestrictionsHelper.start(); |
| } |
| |
| /** |
| * Stops this {@code SpeedBumpController} from monitoring any changes in driving restrictions. |
| */ |
| void stop() { |
| mUxRestrictionsHelper.stop(); |
| } |
| |
| /** |
| * 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); |
| } |
| |
| /** |
| * Updates whether or not the {@link #mContentRateLimiter} is set in unlimited mode based on |
| * the given {@link CarUxRestrictions}. |
| * |
| * <p>If driver optimization is required, then unlimited mode is off. |
| */ |
| private void updateUnlimitedModeEnabled(CarUxRestrictions restrictions) { |
| // If driver optimization is not required, then there is no need to limit anything. |
| mContentRateLimiter.setUnlimitedMode(!restrictions.isRequiresDistractionOptimization()); |
| } |
| } |