summaryrefslogtreecommitdiffstats
path: root/src/com/android/messaging/util/SafeAsyncTask.java
blob: 1cce6e983655a42954576e8175a1b237074e1bca (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
/*
 * 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:
 * <ol>
 *   <li>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.
 *   <li>Enforce creation on main thread as required by AsyncTask
 *   <li>Enforce that the background task does not take longer than expected.
 * </ol>
 */
public abstract class SafeAsyncTask<Params, Progress, Result>
        extends AsyncTask<Params, Progress, Result> {
    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.
     *
     *        <p>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<Params, Progress, Result> 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);
        }
    }
}