1/*
2 * Copyright (C) 2014 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 */
16package android.hardware.camera2.utils;
17
18import android.os.Handler;
19import android.util.Log;
20
21import java.util.HashSet;
22import java.util.Set;
23
24import static com.android.internal.util.Preconditions.*;
25
26/**
27 * Keep track of multiple concurrent tasks starting and finishing by their key;
28 * allow draining existing tasks and figuring out when all tasks have finished
29 * (and new ones won't begin).
30 *
31 * <p>The initial state is to allow all tasks to be started and finished. A task may only be started
32 * once, after which it must be finished before starting again. Likewise, finishing a task
33 * that hasn't been started is also not allowed.</p>
34 *
35 * <p>When draining begins, no more new tasks can be started. This guarantees that at some
36 * point when all the tasks are finished there will be no more collective new tasks,
37 * at which point the {@link DrainListener#onDrained} callback will be invoked.</p>
38 *
39 *
40 * @param <T>
41 *          a type for the key that will represent tracked tasks;
42 *          must implement {@code Object#equals}
43 */
44public class TaskDrainer<T> {
45    /**
46     * Fired asynchronously after draining has begun with {@link TaskDrainer#beginDrain}
47     * <em>and</em> all tasks that were started have finished.
48     */
49    public interface DrainListener {
50        /** All tasks have fully finished draining; there will be no more pending tasks. */
51        public void onDrained();
52    }
53
54    private static final String TAG = "TaskDrainer";
55    private final boolean VERBOSE = Log.isLoggable(TAG, Log.VERBOSE);
56
57    private final Handler mHandler;
58    private final DrainListener mListener;
59    private final String mName;
60
61    /** Set of tasks which have been started but not yet finished with #taskFinished */
62    private final Set<T> mTaskSet = new HashSet<T>();
63    private final Object mLock = new Object();
64
65    private boolean mDraining = false;
66    private boolean mDrainFinished = false;
67
68    /**
69     * Create a new task drainer; {@code onDrained} callbacks will be posted to the listener
70     * via the {@code handler}.
71     *
72     * @param handler a non-{@code null} handler to use to post runnables to
73     * @param listener a non-{@code null} listener where {@code onDrained} will be called
74     */
75    public TaskDrainer(Handler handler, DrainListener listener) {
76        mHandler = checkNotNull(handler, "handler must not be null");
77        mListener = checkNotNull(listener, "listener must not be null");
78        mName = null;
79    }
80
81    /**
82     * Create a new task drainer; {@code onDrained} callbacks will be posted to the listener
83     * via the {@code handler}.
84     *
85     * @param handler a non-{@code null} handler to use to post runnables to
86     * @param listener a non-{@code null} listener where {@code onDrained} will be called
87     * @param name an optional name used for debug logging
88     */
89    public TaskDrainer(Handler handler, DrainListener listener, String name) {
90        // XX: Probably don't need a handler at all here
91        mHandler = checkNotNull(handler, "handler must not be null");
92        mListener = checkNotNull(listener, "listener must not be null");
93        mName = name;
94    }
95
96    /**
97     * Mark an asynchronous task as having started.
98     *
99     * <p>A task cannot be started more than once without first having finished. Once
100     * draining begins with {@link #beginDrain}, no new tasks can be started.</p>
101     *
102     * @param task a key to identify a task
103     *
104     * @see #taskFinished
105     * @see #beginDrain
106     *
107     * @throws IllegalStateException
108     *          If attempting to start a task which is already started (and not finished),
109     *          or if attempting to start a task after draining has begun.
110     */
111    public void taskStarted(T task) {
112        synchronized (mLock) {
113            if (VERBOSE) {
114                Log.v(TAG + "[" + mName + "]", "taskStarted " + task);
115            }
116
117            if (mDraining) {
118                throw new IllegalStateException("Can't start more tasks after draining has begun");
119            }
120
121            if (!mTaskSet.add(task)) {
122                throw new IllegalStateException("Task " + task + " was already started");
123            }
124        }
125    }
126
127
128    /**
129     * Mark an asynchronous task as having finished.
130     *
131     * <p>A task cannot be finished if it hasn't started. Once finished, a task
132     * cannot be finished again (unless it's started again).</p>
133     *
134     * @param task a key to identify a task
135     *
136     * @see #taskStarted
137     * @see #beginDrain
138     *
139     * @throws IllegalStateException
140     *          If attempting to start a task which is already finished (and not re-started),
141     */
142    public void taskFinished(T task) {
143        synchronized (mLock) {
144            if (VERBOSE) {
145                Log.v(TAG + "[" + mName + "]", "taskFinished " + task);
146            }
147
148            if (!mTaskSet.remove(task)) {
149                throw new IllegalStateException("Task " + task + " was already finished");
150            }
151
152            // If this is the last finished task and draining has already begun, fire #onDrained
153            checkIfDrainFinished();
154        }
155    }
156
157    /**
158     * Do not allow any more tasks to be started; once all existing started tasks are finished,
159     * fire the {@link DrainListener#onDrained} callback asynchronously.
160     *
161     * <p>This operation is idempotent; calling it more than once has no effect.</p>
162     */
163    public void beginDrain() {
164        synchronized (mLock) {
165            if (!mDraining) {
166                if (VERBOSE) {
167                    Log.v(TAG + "[" + mName + "]", "beginDrain started");
168                }
169
170                mDraining = true;
171
172                // If all tasks that had started had already finished by now, fire #onDrained
173                checkIfDrainFinished();
174            } else {
175                if (VERBOSE) {
176                    Log.v(TAG + "[" + mName + "]", "beginDrain ignored");
177                }
178            }
179        }
180    }
181
182    private void checkIfDrainFinished() {
183        if (mTaskSet.isEmpty() && mDraining && !mDrainFinished) {
184            mDrainFinished = true;
185            postDrained();
186        }
187    }
188
189    private void postDrained() {
190        mHandler.post(new Runnable() {
191            @Override
192            public void run() {
193                if (VERBOSE) {
194                    Log.v(TAG + "[" + mName + "]", "onDrained");
195                }
196
197                mListener.onDrained();
198            }
199        });
200    }
201}
202