/*
* Copyright (C) 2015 Google Inc.
*
* 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.switchaccess;
import android.annotation.TargetApi;
import android.os.Build;
import android.os.Handler;
import android.os.SystemClock;
import java.util.ArrayList;
import java.util.List;
/**
* Processor for accessibility actions. Because we aren't synchronized with the UI, actions
* are sometimes delayed slightly to allow our view of the UI to settle.
*/
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public class ActionProcessor implements UiChangeDetector.PossibleUiChangeListener {
/* Empirically determined magic time that we delay after possible changes to the UI */
private static final long FRAMEWORK_SETTLING_TIME_MILLIS = 500;
private final List<Runnable> mListOfHeldOffActions = new ArrayList<>();
private final Handler mHandler = new Handler();
private final UiChangedListener mUiChangedListener;
private final Runnable mRunnableToProcessHeldOffActions = new Runnable() {
@Override
public void run() {
long timeToWait = getMillisUntilSafeToProcessActions();
if (timeToWait > 0) {
/* Not safe to process now; reschedule for later */
mHandler.removeCallbacks(mRunnableToProcessHeldOffActions);
mHandler.postDelayed(mRunnableToProcessHeldOffActions, timeToWait);
return;
}
/*
* As with any workaround of a race condition, this isn't watertight, but the view
* hierarchy is most likely stable.
*/
for (Runnable action : mListOfHeldOffActions) {
processGuaranteedSafeAction(action);
}
mListOfHeldOffActions.clear();
}
};
private long mLastWindowChangeTime = 0;
private boolean mUiMayHaveChanged = true;
/**
*
* @param uiChangedListener A listener to be notified when the UI updates (typically an
* OptionManager)
*/
public ActionProcessor(UiChangedListener uiChangedListener) {
mUiChangedListener = uiChangedListener;
}
/**
* Process a Runnable when appropriate
* @param action The runnable to process
*/
public void process(Runnable action) {
mListOfHeldOffActions.add(action);
mHandler.postDelayed(
mRunnableToProcessHeldOffActions, getMillisUntilSafeToProcessActions());
}
/**
* If the UI may have changed, this method should be called so we know to wait for it to
* settle.
*/
@Override
public void onPossibleChangeToUi() {
mUiMayHaveChanged = true;
mLastWindowChangeTime = SystemClock.elapsedRealtime();
/*
* Process an empty runnable so we'll check if we need to clear the overlay once the
* UI is stable.
*/
process(new Runnable() {
@Override
public void run() {
}
});
}
private void processGuaranteedSafeAction(Runnable action) {
if (mUiMayHaveChanged) {
mUiChangedListener.onUiChangedAndIsNowStable();
mUiMayHaveChanged = false;
}
action.run();
}
private long getMillisUntilSafeToProcessActions() {
long timeToWait = mLastWindowChangeTime + FRAMEWORK_SETTLING_TIME_MILLIS
- SystemClock.elapsedRealtime();
timeToWait = (timeToWait > 0) ? timeToWait : 0;
return timeToWait;
}
public interface UiChangedListener {
void onUiChangedAndIsNowStable();
}
}