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