blob: 97793673a1ea43b35666eee479f99573738d604a [file] [log] [blame]
/*
* 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());
}
}