blob: 955b66eec5d12f5dbbcd47beae47dbd75bb9bb4b [file] [log] [blame]
/*
* Copyright (C) 2020 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 com.android.car.ui;
import android.os.SystemClock;
import android.view.View;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.HashMap;
/**
* Cache used by {@link FocusAreaHelper} to save focus history and nudge history of the rotary
* controller.
*/
class RotaryCache {
/** The cache is disabled. */
@VisibleForTesting
static final int CACHE_TYPE_DISABLED = 1;
/** Entries in the cache will expire after a period of time. */
@VisibleForTesting
static final int CACHE_TYPE_EXPIRED_AFTER_SOME_TIME = 2;
/** Entries in the cache will never expire. */
@VisibleForTesting
static final int CACHE_TYPE_NEVER_EXPIRE = 3;
@IntDef(flag = true, value = {
CACHE_TYPE_DISABLED, CACHE_TYPE_EXPIRED_AFTER_SOME_TIME, CACHE_TYPE_NEVER_EXPIRE})
@Retention(RetentionPolicy.SOURCE)
public @interface CacheType {
}
/** Cache of focused view. */
@NonNull
private final FocusCache mFocusCache;
/** Cache of focus areas that were nudged to. */
@NonNull
private final FocusAreaCache mFocusAreaCache;
/** A record of when a View was focused. */
private static class FocusHistory {
/** The focused view. */
@NonNull
final View mFocusedView;
/** The {@link SystemClock#uptimeMillis} when this history was recorded. */
final long mTimestamp;
FocusHistory(@NonNull View focusedView, long timestamp) {
mFocusedView = focusedView;
mTimestamp = timestamp;
}
}
/** Cache of focused view. */
static class FocusCache {
/** The cache type. */
@CacheType
final int mCacheType;
/** How many milliseconds before the entry in the cache expires. */
long mExpirationPeriodMs;
/** The record of focused view. */
@Nullable
FocusHistory mFocusHistory;
FocusCache(@CacheType int cacheType, final long expirationPeriodMs) {
mCacheType = cacheType;
mExpirationPeriodMs = expirationPeriodMs;
if (mCacheType == CACHE_TYPE_EXPIRED_AFTER_SOME_TIME && mExpirationPeriodMs <= 0) {
throw new IllegalArgumentException(
"Expiration time must be positive if CacheType is "
+ "CACHE_TYPE_EXPIRED_AFTER_SOME_TIME");
}
}
View getFocusedView(long elapsedRealtime) {
return isValidHistory(elapsedRealtime) ? mFocusHistory.mFocusedView : null;
}
void setFocusedView(@Nullable View focusedView, long elapsedRealtime) {
if (mCacheType == CACHE_TYPE_DISABLED) {
return;
}
mFocusHistory = focusedView != null
? new FocusHistory(focusedView, elapsedRealtime)
: null;
}
boolean isValidHistory(long elapsedRealtime) {
if (mFocusHistory == null) {
return false;
}
switch (mCacheType) {
case CACHE_TYPE_NEVER_EXPIRE:
return true;
case CACHE_TYPE_EXPIRED_AFTER_SOME_TIME:
return elapsedRealtime < mFocusHistory.mTimestamp + mExpirationPeriodMs;
default:
return false;
}
}
}
/** A record of a focus area that was nudged to. */
private static class FocusAreaHistory {
/** The focus area that was nudged to. */
@NonNull
final IFocusArea mFocusArea;
/** The {@link SystemClock#uptimeMillis} when this history was recorded. */
final long mTimestamp;
FocusAreaHistory(@NonNull IFocusArea focusArea, long timestamp) {
mFocusArea = focusArea;
mTimestamp = timestamp;
}
}
/** Cache of FocusAreas that were nudged to. */
private static class FocusAreaCache extends HashMap<Integer, FocusAreaHistory> {
/** Type of the cache. */
@CacheType
private final int mCacheType;
/** How many milliseconds before an entry in the cache expires. */
private final int mExpirationPeriodMs;
FocusAreaCache(@CacheType int cacheType, int expirationPeriodMs) {
mCacheType = cacheType;
mExpirationPeriodMs = expirationPeriodMs;
if (mCacheType == CACHE_TYPE_EXPIRED_AFTER_SOME_TIME && mExpirationPeriodMs <= 0) {
throw new IllegalArgumentException(
"Expiration time must be positive if CacheType is "
+ "CACHE_TYPE_EXPIRED_AFTER_SOME_TIME");
}
}
void put(int direction, @NonNull IFocusArea targetFocusArea, long elapsedRealtime) {
if (mCacheType == CACHE_TYPE_DISABLED) {
return;
}
put(direction, new FocusAreaHistory(targetFocusArea, elapsedRealtime));
}
IFocusArea get(int direction, long elapsedRealtime) {
FocusAreaHistory history = get(direction);
return isValidHistory(history, elapsedRealtime) ? history.mFocusArea : null;
}
boolean isValidHistory(@Nullable FocusAreaHistory history, long elapsedRealtime) {
if (history == null) {
return false;
}
switch (mCacheType) {
case CACHE_TYPE_NEVER_EXPIRE:
return true;
case CACHE_TYPE_EXPIRED_AFTER_SOME_TIME:
return elapsedRealtime - history.mTimestamp < mExpirationPeriodMs;
default:
return false;
}
}
}
RotaryCache(@CacheType int focusHistoryCacheType,
int focusHistoryExpirationPeriodMs,
@CacheType int focusAreaHistoryCacheType,
int focusAreaHistoryExpirationPeriodMs) {
mFocusCache = new FocusCache(focusHistoryCacheType, focusHistoryExpirationPeriodMs);
mFocusAreaCache = new FocusAreaCache(
focusAreaHistoryCacheType, focusAreaHistoryExpirationPeriodMs);
}
/**
* Searches the cache to find the last focused view of the focus area. Returns the view, or null
* if there is nothing in the cache, the cache is stale.
*/
@Nullable
View getFocusedView(long elapsedRealtime) {
return mFocusCache.getFocusedView(elapsedRealtime);
}
/** Saves the focused view. */
void saveFocusedView(@NonNull View view, long elapsedRealtime) {
mFocusCache.setFocusedView(view, elapsedRealtime);
}
/**
* Searches the cache to find the target focus area for a nudge in a given {@code direction}.
* Returns the target focus area, or null if there is nothing in the cache, the cache is stale.
*/
@Nullable
IFocusArea getCachedFocusArea(int direction, long elapsedRealtime) {
return mFocusAreaCache.get(direction, elapsedRealtime);
}
/** Saves the focus area nudge history. */
void saveFocusArea(int direction, @NonNull IFocusArea targetFocusArea, long elapsedRealtime) {
mFocusAreaCache.put(direction, targetFocusArea, elapsedRealtime);
}
/** Clears the focus area nudge history cache. */
void clearFocusAreaHistory() {
mFocusAreaCache.clear();
}
}