SpeedBumpController.java
/*
* 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.app.Activity;
import android.car.Car;
import android.car.CarNotConnectedException;
import android.car.drivingstate.CarUxRestrictions;
import android.car.drivingstate.CarUxRestrictionsManager;
import android.car.settings.CarConfigurationManager;
import android.car.settings.SpeedBumpConfiguration;
import android.content.ComponentName;
import android.content.Context;
import android.content.ServiceConnection;
import android.graphics.drawable.AnimatedVectorDrawable;
import android.os.Handler;
import android.os.IBinder;
import android.util.Log;
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.annotation.Nullable;
import androidx.car.R;
/**
* A controller for the actual monitoring of when interaction should be allowed in a
* {@link SpeedBumpView}.
*/
class SpeedBumpController {
private static final String TAG = "SpeedBumpController";
/**
* The number of permitted actions that are acquired per second that the user has not
* interacted with the {@code SpeedBumpView}.
*/
private static final double ACQUIRED_PERMITS_PER_SECOND = 0.5d;
/** The maximum number of permits that can be acquired when the user is idling. */
private static final double MAX_PERMIT_POOL = 5d;
/** 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;
@SuppressWarnings("WeakerAccess") /* synthetic access */
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}.
*/
@SuppressWarnings("WeakerAccess") /* synthetic access */
boolean mInteractionPermitted = true;
@SuppressWarnings("WeakerAccess") /* synthetic access */
final int mLockOutMessageDurationMs;
@SuppressWarnings("WeakerAccess") /* synthetic access */
final Handler mHandler = new Handler();
private final Context mContext;
@SuppressWarnings("WeakerAccess") /* synthetic access */
final View mLockoutMessageView;
private final ImageView mLockoutImageView;
@SuppressWarnings("WeakerAccess") /* synthetic access */
@Nullable final Car mCar;
@SuppressWarnings("WeakerAccess") /* synthetic access */
@Nullable CarUxRestrictionsManager mCarUxRestrictionsManager;
/**
* 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);
mCar = Car.createCar(mContext, mServiceConnection);
// By default, no limiting until UXR restrictions kick in.
mContentRateLimiter.setUnlimitedMode(true);
}
/**
* Starts monitoring any changes in {@link CarUxRestrictions}.
*
* <p>This method can be called from {@code Activity}'s {@link Activity#onStart()}, or at the
* time of construction.
*
* <p>This method must be accompanied with a matching {@link #stop()} to avoid leak.
*/
void start() {
try {
if (mCar != null && !mCar.isConnected()) {
mCar.connect();
}
} catch (IllegalStateException e) {
// Do nothing.
Log.w(TAG, "start(); cannot connect to Car");
}
}
/**
* Stops monitoring any changes in {@link CarUxRestrictions}.
*
* <p>This method should be called from {@code Activity}'s {@link Activity#onStop()}, or at the
* time of this adapter being discarded.
*/
void stop() {
if (mCarUxRestrictionsManager != null) {
try {
mCarUxRestrictionsManager.unregisterListener();
} catch (CarNotConnectedException e) {
// Do nothing.
Log.w(TAG, "stop(); cannot unregister listener.");
}
mCarUxRestrictionsManager = null;
}
try {
if (mCar != null && mCar.isConnected()) {
mCar.disconnect();
}
} catch (IllegalStateException e) {
// Do nothing.
Log.w(TAG, "stop(); cannot disconnect from Car.");
}
}
/**
* 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.
*/
@SuppressWarnings("WeakerAccess") /* synthetic access */
void updateUnlimitedModeEnabled(CarUxRestrictions restrictions) {
// If driver optimization is not required, then there is no need to limit anything.
mContentRateLimiter.setUnlimitedMode(!restrictions.isRequiresDistractionOptimization());
}
private final ServiceConnection mServiceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
try {
mCarUxRestrictionsManager = (CarUxRestrictionsManager)
mCar.getCarManager(Car.CAR_UX_RESTRICTION_SERVICE);
mCarUxRestrictionsManager.registerListener(
SpeedBumpController.this::updateUnlimitedModeEnabled);
updateUnlimitedModeEnabled(mCarUxRestrictionsManager.getCurrentCarUxRestrictions());
CarConfigurationManager configManager = (CarConfigurationManager)
mCar.getCarManager(Car.CAR_CONFIGURATION_SERVICE);
SpeedBumpConfiguration speedBumpConfiguration =
configManager.getSpeedBumpConfiguration();
mContentRateLimiter.setAcquiredPermitsRate(
speedBumpConfiguration.getAcquiredPermitsPerSecond());
mContentRateLimiter.setMaxStoredPermits(
speedBumpConfiguration.getMaxPermitPool());
mContentRateLimiter.setPermitFillDelay(
speedBumpConfiguration.getPermitFillDelay());
} catch (CarNotConnectedException e) {
e.printStackTrace();
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
mCarUxRestrictionsManager = null;
}
};
}