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 
SafeAsyncTask()51     public SafeAsyncTask() {
52         this(DEFAULT_MAX_EXECUTION_TIME_MILLIS, false);
53     }
54 
SafeAsyncTask(final long maxTimeMillis)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      */
SafeAsyncTask(final long maxTimeMillis, final boolean cancelExecutionOnTimeout)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 
executeOnThreadPool( final Params... params)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 
doInBackgroundTimed(final Params... params)89     protected abstract Result doInBackgroundTimed(final Params... params);
90 
91     @Override
doInBackground(final Params... params)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
onPostExecute(final Result result)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
executeOnThreadPool(final Runnable runnable)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      */
executeOnThreadPool(final Runnable runnable, final boolean withWakeLock)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