Home | History | Annotate | Download | only in util
      1 /*
      2  * Copyright (C) 2015 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.messaging.util;
     18 
     19 import android.content.Intent;
     20 import android.os.AsyncTask;
     21 import android.os.Debug;
     22 import android.os.SystemClock;
     23 
     24 import com.android.messaging.Factory;
     25 import com.android.messaging.util.Assert.RunsOnAnyThread;
     26 
     27 /**
     28  * Wrapper class which provides explicit API for:
     29  * <ol>
     30  *   <li>Threading policy choice - Users of this class should use the explicit API instead of
     31  *       {@link #execute} which uses different threading policy on different OS versions.
     32  *   <li>Enforce creation on main thread as required by AsyncTask
     33  *   <li>Enforce that the background task does not take longer than expected.
     34  * </ol>
     35  */
     36 public abstract class SafeAsyncTask<Params, Progress, Result>
     37         extends AsyncTask<Params, Progress, Result> {
     38     private static final long DEFAULT_MAX_EXECUTION_TIME_MILLIS = 10 * 1000; // 10 seconds
     39 
     40     /** This is strongly discouraged as it can block other AsyncTasks indefinitely. */
     41     public static final long UNBOUNDED_TIME = Long.MAX_VALUE;
     42 
     43     private static final String WAKELOCK_ID = "bugle_safe_async_task_wakelock";
     44     protected static final int WAKELOCK_OP = 1000;
     45     private static WakeLockHelper sWakeLock = new WakeLockHelper(WAKELOCK_ID);
     46 
     47     private final long mMaxExecutionTimeMillis;
     48     private final boolean mCancelExecutionOnTimeout;
     49     private boolean mThreadPoolRequested;
     50 
     51     public SafeAsyncTask() {
     52         this(DEFAULT_MAX_EXECUTION_TIME_MILLIS, false);
     53     }
     54 
     55     public SafeAsyncTask(final long maxTimeMillis) {
     56         this(maxTimeMillis, false);
     57     }
     58 
     59     /**
     60      * @param maxTimeMillis maximum expected time for the background operation. This is just
     61      *        a diagnostic tool to catch unexpectedly long operations. If an operation does take
     62      *        longer than expected, it is fine to increase this argument. If the value is larger
     63      *        than a minute, you should consider using a dedicated thread so as not to interfere
     64      *        with other AsyncTasks.
     65      *
     66      *        <p>Use {@link #UNBOUNDED_TIME} if you do not know the maximum expected time. This
     67      *        is strongly discouraged as it can block other AsyncTasks indefinitely.
     68      *
     69      * @param cancelExecutionOnTimeout whether to attempt to cancel the task execution on timeout.
     70      *        If this is set, at execution timeout we will call cancel(), so doInBackgroundTimed()
     71      *        should periodically check if the task is to be cancelled and finish promptly if
     72      *        possible, and handle the cancel event in onCancelled(). Also, at the end of execution
     73      *        we will not crash the execution if it went over limit since we explicitly canceled it.
     74      */
     75     public SafeAsyncTask(final long maxTimeMillis, final boolean cancelExecutionOnTimeout) {
     76         Assert.isMainThread(); // AsyncTask has to be created on the main thread
     77         mMaxExecutionTimeMillis = maxTimeMillis;
     78         mCancelExecutionOnTimeout = cancelExecutionOnTimeout;
     79     }
     80 
     81     public final SafeAsyncTask<Params, Progress, Result> executeOnThreadPool(
     82             final Params... params) {
     83         Assert.isMainThread(); // AsyncTask requires this
     84         mThreadPoolRequested = true;
     85         executeOnExecutor(THREAD_POOL_EXECUTOR, params);
     86         return this;
     87     }
     88 
     89     protected abstract Result doInBackgroundTimed(final Params... params);
     90 
     91     @Override
     92     protected final Result doInBackground(final Params... params) {
     93         // This enforces that executeOnThreadPool was called, not execute. Ideally, we would
     94         // make execute throw an exception, but since it is final, we cannot override it.
     95         Assert.isTrue(mThreadPoolRequested);
     96 
     97         if (mCancelExecutionOnTimeout) {
     98             ThreadUtil.getMainThreadHandler().postDelayed(new Runnable() {
     99                 @Override
    100                 public void run() {
    101                     if (getStatus() == Status.RUNNING) {
    102                         // Cancel the task if it's still running.
    103                         LogUtil.w(LogUtil.BUGLE_TAG, String.format("%s timed out and is canceled",
    104                                 this));
    105                         cancel(true /* mayInterruptIfRunning */);
    106                     }
    107                 }
    108             }, mMaxExecutionTimeMillis);
    109         }
    110 
    111         final long startTime = SystemClock.elapsedRealtime();
    112         try {
    113             return doInBackgroundTimed(params);
    114         } finally {
    115             final long executionTime = SystemClock.elapsedRealtime() - startTime;
    116             if (executionTime > mMaxExecutionTimeMillis) {
    117                 LogUtil.w(LogUtil.BUGLE_TAG, String.format("%s took %dms", this, executionTime));
    118                 // Don't crash if debugger is attached or if we are asked to cancel on timeout.
    119                 if (!Debug.isDebuggerConnected() && !mCancelExecutionOnTimeout) {
    120                     Assert.fail(this + " took too long");
    121                 }
    122             }
    123         }
    124 
    125     }
    126 
    127     @Override
    128     protected void onPostExecute(final Result result) {
    129         // No need to use AsyncTask at all if there is no onPostExecute
    130         Assert.fail("Use SafeAsyncTask.executeOnThreadPool");
    131     }
    132 
    133     /**
    134      * This provides a way for people to run async tasks but without onPostExecute.
    135      * This can be called on any thread.
    136      *
    137      * Run code in a thread using AsyncTask's thread pool.
    138      *
    139      * To enable wakelock during the execution, see {@link #executeOnThreadPool(Runnable, boolean)}
    140      *
    141      * @param runnable The Runnable to execute asynchronously
    142      */
    143     @RunsOnAnyThread
    144     public static void executeOnThreadPool(final Runnable runnable) {
    145         executeOnThreadPool(runnable, false);
    146     }
    147 
    148     /**
    149      * This provides a way for people to run async tasks but without onPostExecute.
    150      * This can be called on any thread.
    151      *
    152      * Run code in a thread using AsyncTask's thread pool.
    153      *
    154      * @param runnable The Runnable to execute asynchronously
    155      * @param withWakeLock when set, a wake lock will be held for the duration of the runnable
    156      *        execution
    157      */
    158     public static void executeOnThreadPool(final Runnable runnable, final boolean withWakeLock) {
    159         if (withWakeLock) {
    160             final Intent intent = new Intent();
    161             sWakeLock.acquire(Factory.get().getApplicationContext(), intent, WAKELOCK_OP);
    162             THREAD_POOL_EXECUTOR.execute(new Runnable() {
    163                 @Override
    164                 public void run() {
    165                     try {
    166                         runnable.run();
    167                     } finally {
    168                         sWakeLock.release(intent, WAKELOCK_OP);
    169                     }
    170                 }
    171             });
    172         } else {
    173             THREAD_POOL_EXECUTOR.execute(runnable);
    174         }
    175     }
    176 }
    177