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