317 lines
9.9 KiB
Java
317 lines
9.9 KiB
Java
/*
|
|
* Copyright (C) 2010 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 android.app;
|
|
|
|
import android.compat.annotation.UnsupportedAppUsage;
|
|
import android.os.Build;
|
|
import android.os.Handler;
|
|
import android.os.HandlerThread;
|
|
import android.os.Looper;
|
|
import android.os.Message;
|
|
import android.os.Process;
|
|
import android.os.StrictMode;
|
|
import android.util.Log;
|
|
|
|
import com.android.internal.annotations.GuardedBy;
|
|
import com.android.internal.annotations.VisibleForTesting;
|
|
import com.android.internal.util.ExponentiallyBucketedHistogram;
|
|
|
|
import java.util.LinkedList;
|
|
|
|
/**
|
|
* Internal utility class to keep track of process-global work that's outstanding and hasn't been
|
|
* finished yet.
|
|
*
|
|
* New work will be {@link #queue queued}.
|
|
*
|
|
* It is possible to add 'finisher'-runnables that are {@link #waitToFinish guaranteed to be run}.
|
|
* This is used to make sure the work has been finished.
|
|
*
|
|
* This was created for writing SharedPreference edits out asynchronously so we'd have a mechanism
|
|
* to wait for the writes in Activity.onPause and similar places, but we may use this mechanism for
|
|
* other things in the future.
|
|
*
|
|
* The queued asynchronous work is performed on a separate, dedicated thread.
|
|
*
|
|
* @hide
|
|
*/
|
|
public class QueuedWork {
|
|
private static final String LOG_TAG = QueuedWork.class.getSimpleName();
|
|
private static final boolean DEBUG = false;
|
|
|
|
/** Delay for delayed runnables, as big as possible but low enough to be barely perceivable */
|
|
private static final long DELAY = 100;
|
|
|
|
/** If a {@link #waitToFinish()} takes more than {@value #MAX_WAIT_TIME_MILLIS} ms, warn */
|
|
private static final long MAX_WAIT_TIME_MILLIS = 512;
|
|
|
|
/** Lock for this class */
|
|
private static final Object sLock = new Object();
|
|
|
|
/**
|
|
* Used to make sure that only one thread is processing work items at a time. This means that
|
|
* they are processed in the order added.
|
|
*
|
|
* This is separate from {@link #sLock} as this is held the whole time while work is processed
|
|
* and we do not want to stall the whole class.
|
|
*/
|
|
private static Object sProcessingWork = new Object();
|
|
|
|
/** Finishers {@link #addFinisher added} and not yet {@link #removeFinisher removed} */
|
|
@GuardedBy("sLock")
|
|
@UnsupportedAppUsage
|
|
private static final LinkedList<Runnable> sFinishers = new LinkedList<>();
|
|
|
|
/** {@link #getHandler() Lazily} created handler */
|
|
@GuardedBy("sLock")
|
|
private static Handler sHandler = null;
|
|
|
|
/** Work queued via {@link #queue} */
|
|
@GuardedBy("sLock")
|
|
private static LinkedList<Runnable> sWork = new LinkedList<>();
|
|
|
|
/** If new work can be delayed or not */
|
|
@GuardedBy("sLock")
|
|
private static boolean sCanDelay = true;
|
|
|
|
/** Time (and number of instances) waited for work to get processed */
|
|
@GuardedBy("sLock")
|
|
private final static ExponentiallyBucketedHistogram
|
|
mWaitTimes = new ExponentiallyBucketedHistogram(
|
|
16);
|
|
private static int mNumWaits = 0;
|
|
|
|
/**
|
|
* Lazily create a handler on a separate thread.
|
|
*
|
|
* @return the handler
|
|
*/
|
|
@UnsupportedAppUsage
|
|
private static Handler getHandler() {
|
|
synchronized (sLock) {
|
|
if (sHandler == null) {
|
|
HandlerThread handlerThread = new HandlerThread("queued-work-looper",
|
|
Process.THREAD_PRIORITY_FOREGROUND);
|
|
handlerThread.start();
|
|
|
|
sHandler = new QueuedWorkHandler(handlerThread.getLooper());
|
|
}
|
|
return sHandler;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Tear down the handler.
|
|
*/
|
|
@VisibleForTesting
|
|
public static void resetHandler() {
|
|
synchronized (sLock) {
|
|
if (sHandler == null) {
|
|
return;
|
|
}
|
|
sHandler.getLooper().quitSafely();
|
|
sHandler = null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Remove all Messages from the Handler with the given code.
|
|
*
|
|
* This method intentionally avoids creating the Handler if it doesn't
|
|
* already exist.
|
|
*/
|
|
private static void handlerRemoveMessages(int what) {
|
|
synchronized (sLock) {
|
|
if (sHandler == null) {
|
|
// Nothing to remove
|
|
return;
|
|
}
|
|
getHandler().removeMessages(what);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add a finisher-runnable to wait for {@link #queue asynchronously processed work}.
|
|
*
|
|
* Used by SharedPreferences$Editor#startCommit().
|
|
*
|
|
* Note that this doesn't actually start it running. This is just a scratch set for callers
|
|
* doing async work to keep updated with what's in-flight. In the common case, caller code
|
|
* (e.g. SharedPreferences) will pretty quickly call remove() after an add(). The only time
|
|
* these Runnables are run is from {@link #waitToFinish}.
|
|
*
|
|
* @param finisher The runnable to add as finisher
|
|
*/
|
|
@UnsupportedAppUsage
|
|
public static void addFinisher(Runnable finisher) {
|
|
synchronized (sLock) {
|
|
sFinishers.add(finisher);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Remove a previously {@link #addFinisher added} finisher-runnable.
|
|
*
|
|
* @param finisher The runnable to remove.
|
|
*/
|
|
@UnsupportedAppUsage
|
|
public static void removeFinisher(Runnable finisher) {
|
|
synchronized (sLock) {
|
|
sFinishers.remove(finisher);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Trigger queued work to be processed immediately. The queued work is processed on a separate
|
|
* thread asynchronous. While doing that run and process all finishers on this thread. The
|
|
* finishers can be implemented in a way to check weather the queued work is finished.
|
|
*
|
|
* Is called from the Activity base class's onPause(), after BroadcastReceiver's onReceive,
|
|
* after Service command handling, etc. (so async work is never lost)
|
|
*/
|
|
public static void waitToFinish() {
|
|
long startTime = System.currentTimeMillis();
|
|
boolean hadMessages = false;
|
|
|
|
synchronized (sLock) {
|
|
if (DEBUG) {
|
|
hadMessages = getHandler().hasMessages(QueuedWorkHandler.MSG_RUN);
|
|
}
|
|
handlerRemoveMessages(QueuedWorkHandler.MSG_RUN);
|
|
if (DEBUG && hadMessages) {
|
|
Log.d(LOG_TAG, "waiting");
|
|
}
|
|
|
|
// We should not delay any work as this might delay the finishers
|
|
sCanDelay = false;
|
|
}
|
|
|
|
StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites();
|
|
try {
|
|
processPendingWork();
|
|
} finally {
|
|
StrictMode.setThreadPolicy(oldPolicy);
|
|
}
|
|
|
|
try {
|
|
while (true) {
|
|
Runnable finisher;
|
|
|
|
synchronized (sLock) {
|
|
finisher = sFinishers.poll();
|
|
}
|
|
|
|
if (finisher == null) {
|
|
break;
|
|
}
|
|
|
|
finisher.run();
|
|
}
|
|
} finally {
|
|
sCanDelay = true;
|
|
}
|
|
|
|
synchronized (sLock) {
|
|
long waitTime = System.currentTimeMillis() - startTime;
|
|
|
|
if (waitTime > 0 || hadMessages) {
|
|
mWaitTimes.add(Long.valueOf(waitTime).intValue());
|
|
mNumWaits++;
|
|
|
|
if (DEBUG || mNumWaits % 1024 == 0 || waitTime > MAX_WAIT_TIME_MILLIS) {
|
|
mWaitTimes.log(LOG_TAG, "waited: ");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Queue a work-runnable for processing asynchronously.
|
|
*
|
|
* @param work The new runnable to process
|
|
* @param shouldDelay If the message should be delayed
|
|
*/
|
|
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
|
|
public static void queue(Runnable work, boolean shouldDelay) {
|
|
Handler handler = getHandler();
|
|
|
|
synchronized (sLock) {
|
|
sWork.add(work);
|
|
|
|
if (shouldDelay && sCanDelay) {
|
|
handler.sendEmptyMessageDelayed(QueuedWorkHandler.MSG_RUN, DELAY);
|
|
} else {
|
|
handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @return True iff there is any {@link #queue async work queued}.
|
|
*/
|
|
public static boolean hasPendingWork() {
|
|
synchronized (sLock) {
|
|
return !sWork.isEmpty();
|
|
}
|
|
}
|
|
|
|
private static void processPendingWork() {
|
|
long startTime = 0;
|
|
|
|
if (DEBUG) {
|
|
startTime = System.currentTimeMillis();
|
|
}
|
|
|
|
synchronized (sProcessingWork) {
|
|
LinkedList<Runnable> work;
|
|
|
|
synchronized (sLock) {
|
|
work = sWork;
|
|
sWork = new LinkedList<>();
|
|
|
|
// Remove all msg-s as all work will be processed now
|
|
handlerRemoveMessages(QueuedWorkHandler.MSG_RUN);
|
|
}
|
|
|
|
if (work.size() > 0) {
|
|
for (Runnable w : work) {
|
|
w.run();
|
|
}
|
|
|
|
if (DEBUG) {
|
|
Log.d(LOG_TAG, "processing " + work.size() + " items took " +
|
|
+(System.currentTimeMillis() - startTime) + " ms");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private static class QueuedWorkHandler extends Handler {
|
|
static final int MSG_RUN = 1;
|
|
|
|
QueuedWorkHandler(Looper looper) {
|
|
super(looper);
|
|
}
|
|
|
|
public void handleMessage(Message msg) {
|
|
if (msg.what == MSG_RUN) {
|
|
processPendingWork();
|
|
}
|
|
}
|
|
}
|
|
}
|