/* * Copyright (C) 2015 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.messaging.util; import android.content.Intent; import android.os.AsyncTask; import android.os.Debug; import android.os.SystemClock; import com.android.messaging.Factory; import com.android.messaging.util.Assert.RunsOnAnyThread; /** * Wrapper class which provides explicit API for: *
    *
  1. Threading policy choice - Users of this class should use the explicit API instead of * {@link #execute} which uses different threading policy on different OS versions. *
  2. Enforce creation on main thread as required by AsyncTask *
  3. Enforce that the background task does not take longer than expected. *
*/ public abstract class SafeAsyncTask extends AsyncTask { private static final long DEFAULT_MAX_EXECUTION_TIME_MILLIS = 10 * 1000; // 10 seconds /** This is strongly discouraged as it can block other AsyncTasks indefinitely. */ public static final long UNBOUNDED_TIME = Long.MAX_VALUE; private static final String WAKELOCK_ID = "bugle_safe_async_task_wakelock"; protected static final int WAKELOCK_OP = 1000; private static WakeLockHelper sWakeLock = new WakeLockHelper(WAKELOCK_ID); private final long mMaxExecutionTimeMillis; private final boolean mCancelExecutionOnTimeout; private boolean mThreadPoolRequested; public SafeAsyncTask() { this(DEFAULT_MAX_EXECUTION_TIME_MILLIS, false); } public SafeAsyncTask(final long maxTimeMillis) { this(maxTimeMillis, false); } /** * @param maxTimeMillis maximum expected time for the background operation. This is just * a diagnostic tool to catch unexpectedly long operations. If an operation does take * longer than expected, it is fine to increase this argument. If the value is larger * than a minute, you should consider using a dedicated thread so as not to interfere * with other AsyncTasks. * *

Use {@link #UNBOUNDED_TIME} if you do not know the maximum expected time. This * is strongly discouraged as it can block other AsyncTasks indefinitely. * * @param cancelExecutionOnTimeout whether to attempt to cancel the task execution on timeout. * If this is set, at execution timeout we will call cancel(), so doInBackgroundTimed() * should periodically check if the task is to be cancelled and finish promptly if * possible, and handle the cancel event in onCancelled(). Also, at the end of execution * we will not crash the execution if it went over limit since we explicitly canceled it. */ public SafeAsyncTask(final long maxTimeMillis, final boolean cancelExecutionOnTimeout) { Assert.isMainThread(); // AsyncTask has to be created on the main thread mMaxExecutionTimeMillis = maxTimeMillis; mCancelExecutionOnTimeout = cancelExecutionOnTimeout; } public final SafeAsyncTask executeOnThreadPool( final Params... params) { Assert.isMainThread(); // AsyncTask requires this mThreadPoolRequested = true; executeOnExecutor(THREAD_POOL_EXECUTOR, params); return this; } protected abstract Result doInBackgroundTimed(final Params... params); @Override protected final Result doInBackground(final Params... params) { // This enforces that executeOnThreadPool was called, not execute. Ideally, we would // make execute throw an exception, but since it is final, we cannot override it. Assert.isTrue(mThreadPoolRequested); if (mCancelExecutionOnTimeout) { ThreadUtil.getMainThreadHandler().postDelayed(new Runnable() { @Override public void run() { if (getStatus() == Status.RUNNING) { // Cancel the task if it's still running. LogUtil.w(LogUtil.BUGLE_TAG, String.format("%s timed out and is canceled", this)); cancel(true /* mayInterruptIfRunning */); } } }, mMaxExecutionTimeMillis); } final long startTime = SystemClock.elapsedRealtime(); try { return doInBackgroundTimed(params); } finally { final long executionTime = SystemClock.elapsedRealtime() - startTime; if (executionTime > mMaxExecutionTimeMillis) { LogUtil.w(LogUtil.BUGLE_TAG, String.format("%s took %dms", this, executionTime)); // Don't crash if debugger is attached or if we are asked to cancel on timeout. if (!Debug.isDebuggerConnected() && !mCancelExecutionOnTimeout) { Assert.fail(this + " took too long"); } } } } @Override protected void onPostExecute(final Result result) { // No need to use AsyncTask at all if there is no onPostExecute Assert.fail("Use SafeAsyncTask.executeOnThreadPool"); } /** * This provides a way for people to run async tasks but without onPostExecute. * This can be called on any thread. * * Run code in a thread using AsyncTask's thread pool. * * To enable wakelock during the execution, see {@link #executeOnThreadPool(Runnable, boolean)} * * @param runnable The Runnable to execute asynchronously */ @RunsOnAnyThread public static void executeOnThreadPool(final Runnable runnable) { executeOnThreadPool(runnable, false); } /** * This provides a way for people to run async tasks but without onPostExecute. * This can be called on any thread. * * Run code in a thread using AsyncTask's thread pool. * * @param runnable The Runnable to execute asynchronously * @param withWakeLock when set, a wake lock will be held for the duration of the runnable * execution */ public static void executeOnThreadPool(final Runnable runnable, final boolean withWakeLock) { if (withWakeLock) { final Intent intent = new Intent(); sWakeLock.acquire(Factory.get().getApplicationContext(), intent, WAKELOCK_OP); THREAD_POOL_EXECUTOR.execute(new Runnable() { @Override public void run() { try { runnable.run(); } finally { sWakeLock.release(intent, WAKELOCK_OP); } } }); } else { THREAD_POOL_EXECUTOR.execute(runnable); } } }