1/*
2 * Copyright (C) 2013 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
17package com.android.bitmap;
18
19import android.util.SparseArray;
20
21import com.android.mail.utils.LogTag;
22import com.android.mail.utils.LogUtils;
23import com.android.mail.utils.Utils;
24
25import java.util.ArrayDeque;
26import java.util.Iterator;
27import java.util.Queue;
28
29/**
30 * An implementation of a task aggregator that executes tasks in the order that they are expected
31 * . All tasks that is given to {@link #execute(Object, Runnable)} must correspond to a key. This
32 * key must have been previously declared with {@link #expect(Object, Callback)}.
33 * The task will be scheduled to run when its corresponding key becomes the first expected key
34 * amongst the other keys in this aggregator.
35 * <p/>
36 * If on {@link #execute(Object, Runnable)} the key is not expected, the task will be executed
37 * immediately as an edge case.
38 * <p/>
39 * A characteristic scenario is as follows:
40 * <ol>
41 * <li>Expect keys <b>A</b>, <b>B</b>, <b>C</b>, and <b>Z</b>, in that order. Key <b>A</b> is now
42 * the first expected key.</li>
43 * <li>Execute task <b>2</b> for key <b>B</b>. The first expected key is <b>A</b>,
44 * which has no task associated with it, so we store task <b>2</b>. </li>
45 * <li>Execute task <b>4</b> for key <b>Z</b>. We store task <b>4</b>. </li>
46 * <li>Execute task <b>1</b> for key <b>A</b>. We run task <b>1</b> because its key <b>A</b> is
47 * the first expected, then we remove key <b>A</b> from the list of keys that we expect.</li>
48 * <li>This causes key <b>B</b> to be the first expected key, and we see that we have previously
49 * stored task <b>2</b> for that key. We run task <b>2</b> and remove key <b>B</b>. </li>
50 * <li>Key <b>C</b> is now the first expected key, but it has no task,
51 * so nothing happens. Task <b>4</b>, which was previously stored,
52 * cannot run until its corresponding key <b>Z</b> becomes the first expected key. This can
53 * happen if a task comes in for key <b>C</b> or if forget is called on key <b>C</b>.</li>
54 * </ol>
55 * <p/>
56 * ContiguousFIFOAggregator is not thread safe.
57 */
58public class ContiguousFIFOAggregator<T> {
59    private final Queue<T> mExpected;
60    private final SparseArray<Value> mTasks;
61
62    private static final String TAG = LogTag.getLogTag();
63    private static final boolean DEBUG = false;
64
65    /**
66     * Create a new ContiguousFIFOAggregator.
67     * <p/>
68     * The nature of the prioritization means that the number of stored keys and tasks may grow
69     * unbounded. This may happen, for instance, if the top priority key is never given a task to
70     * {@link #execute(Object, Runnable)}. However, in practice, if you are generating tasks in
71     * response to UI elements appearing on the screen, you will only have a bounded set of keys.
72     * UI elements that scroll off the screen will call {@link #forget(Object)} while new elements
73     * will call {@link #expect(Object, Callback)}. This means that the expected
74     * number of keys and tasks is
75     * the maximum number of UI elements that you expect to show on the screen at any time.
76     */
77    public ContiguousFIFOAggregator() {
78        mExpected = new ArrayDeque<T>();
79        mTasks = new SparseArray<Value>();
80    }
81
82    /**
83     * Declare a key to be expected in the future. The order in which you expect keys is very
84     * important. Keys that are declared first are guaranteed to have their tasks run first. You
85     * must call either {@link #forget(Object)} or {@link #execute(Object, Runnable)}
86     * with this key in the future, or you will leak the key.
87     *
88     * If you call expect with a previously expected key, the key will be placed at the back of
89     * the expected queue and its callback will be replaced with this one.
90     *
91     * @param key      the key to expect a task for. Use the same key when setting the task later
92     *                 with {@link #execute (Object, Runnable)}.
93     * @param callback the callback to notify when the key becomes the first expected key, or null.
94     */
95    public void expect(final T key, final Callback<T> callback) {
96        if (key == null) {
97            throw new IllegalArgumentException("Do not use null keys.");
98        }
99
100        Utils.traceBeginSection("pool expect");
101        final int hash = key.hashCode();
102        if (contains(key)) {
103            mExpected.remove(key);
104            mTasks.remove(hash);
105        }
106        final boolean isFirst = mExpected.isEmpty();
107        mExpected.offer(key);
108        mTasks.put(hash, new Value(callback, null));
109        if (DEBUG) LogUtils.d(TAG, "ContiguousFIFOAggregator >> tasks: %s", prettyPrint());
110
111        if (isFirst) {
112            onFirstExpectedChanged(key);
113        }
114        Utils.traceEndSection();
115    }
116
117    /**
118     * Remove a previously declared key that we no longer expect to execute a task for. This
119     * potentially allows another key to now become the first expected key,
120     * and so this may trigger one or more tasks to be executed.
121     *
122     * @param key the key previously declared in {@link #expect(Object, Callback)}.
123     *
124     */
125    public void forget(final T key) {
126        if (key == null) {
127            throw new IllegalArgumentException("Do not use null keys.");
128        }
129
130        if (!contains(key)) {
131            return;
132        }
133
134        Utils.traceBeginSection("pool forget");
135        final boolean removedFirst = key.equals(mExpected.peek());
136        mExpected.remove(key);
137        mTasks.delete(key.hashCode());
138        if (DEBUG) LogUtils.d(TAG, "ContiguousFIFOAggregator  < tasks: %s", prettyPrint());
139
140        final T second;
141        if (removedFirst && (second = mExpected.peek()) != null) {
142            // We removed the first key. The second key is now first.
143            onFirstExpectedChanged(second);
144        }
145
146        maybeExecuteNow();
147        Utils.traceEndSection();
148    }
149
150    /**
151     * Attempt to execute a task corresponding to a previously declared key. If the key is the
152     * first expected key, the task will be executed immediately. Otherwise, the task is stored
153     * until its key becomes the first expected key. Execution of a task results in the removal
154     * of that key, which potentially allows another key to now become the first expected key,
155     * and may cause one or more other tasks to be executed.
156     * <p/>
157     * If the key is not expected, the task will be executed immediately as an edge case.
158     *
159     * @param key  the key previously declared in {@link #expect(Object, Callback)}.
160     * @param task the task to execute or store, depending on its corresponding key.
161     */
162    public void execute(final T key, final Runnable task) {
163        Utils.traceBeginSection("pool execute");
164        final int hash = key.hashCode();
165        final Value value = mTasks.get(hash);
166        if (value == null || task == null) {
167            if (task != null) {
168                task.run();
169            }
170            Utils.traceEndSection();
171            return;
172        }
173        value.task = task;
174        if (DEBUG) LogUtils.d(TAG, "ContiguousFIFOAggregator ++ tasks: %s", prettyPrint());
175        maybeExecuteNow();
176        Utils.traceEndSection();
177    }
178
179    /**
180     * Triggered by {@link #execute(Object, Runnable)} and {@link #forget(Object)}. The keys or
181     * tasks have changed, which may cause one or more tasks to be executed. This method will
182     * continue to execute tasks associated with the first expected key to the last expected key,
183     * stopping when it finds that the first expected key has not yet been assigned a task.
184     */
185    private void maybeExecuteNow() {
186        T first;
187        int count = 0;
188        while (!mExpected.isEmpty()) {
189            Utils.traceBeginSection("pool maybeExecuteNow loop");
190            first = mExpected.peek();
191            if (count > 0) {
192                // When count == 0, the key is already first.
193                onFirstExpectedChanged(first);
194            }
195
196            final int hash = first.hashCode();
197            final Value value = mTasks.get(hash);
198            if (value.task == null) {
199                Utils.traceEndSection();
200                break;
201            }
202
203            mExpected.poll();
204            mTasks.delete(hash);
205            if (DEBUG) LogUtils.d(TAG, "ContiguousFIFOAggregator  - tasks: %s", prettyPrint());
206            value.task.run();
207            count++;
208            Utils.traceEndSection();
209        }
210    }
211
212    /**
213     * This method should only be called once for any key.
214     * @param key The key that has become the new first expected key.
215     */
216    private void onFirstExpectedChanged(final T key) {
217        final int hash = key.hashCode();
218        final Value value = mTasks.get(hash);
219        if (value == null) {
220            return;
221        }
222        final Callback<T> callback = value.callback;
223        if (callback == null) {
224            return;
225        }
226        if (DEBUG) LogUtils.d(TAG, "ContiguousFIFOAggregator    first: %d", hash);
227        callback.onBecomeFirstExpected(key);
228    }
229
230    private boolean contains(final T key) {
231        return mTasks.get(key.hashCode()) != null;
232    }
233
234    /**
235     * Get a pretty string representing the internal data.
236     * @return  a String for the internal data.
237     */
238    private String prettyPrint() {
239        if (mExpected.isEmpty()) {
240            return "{}";
241        }
242
243        StringBuilder buffer = new StringBuilder(mExpected.size() * 28);
244        buffer.append('{');
245        Iterator<T> it = mExpected.iterator();
246        while (it.hasNext()) {
247            final T key = it.next();
248            final int hash = key.hashCode();
249            buffer.append(hash);
250            buffer.append('=');
251            final Value value = mTasks.get(hash);
252            buffer.append(value);
253            if (it.hasNext()) {
254                buffer.append(", ");
255            }
256        }
257        buffer.append('}');
258        if (mExpected.size() != mTasks.size()) {
259            buffer.append(" error");
260        }
261        return buffer.toString();
262    }
263
264    /**
265     * Implement this interface if you want to be notified when the key becomes the first
266     * expected key.
267     * @param <T> The type of the key used for the aggregator.
268     */
269    public interface Callback<T> {
270
271        /**
272         * The key you declared as expected has become the first expected key in this aggregator.
273         *
274         * We don't need a noLongerFirstExpected() method because this aggregator strictly adds
275         * additional to the end of the queue. For a first expected key to no longer be the first
276         * expected, it would either have to be forgotten with {@link #forget(Object)} or a task
277         * assigned and executed with {@link #execute(Object, Runnable)}.
278         *
279         * @param key The key that became first. We provide the key so the callback can either not
280         *            keep state, or it can keep state which may have changed so the callback can do
281         *            a comparison.
282         */
283        void onBecomeFirstExpected(final T key);
284    }
285
286    /**
287     * Holds the callback and task for when a key later becomes the first expected key.
288     */
289    private class Value {
290
291        final Callback<T> callback;
292        Runnable task;
293
294        Value(final Callback<T> callback, final Runnable task) {
295            this.callback = callback;
296            this.task = task;
297        }
298
299        @Override
300        public String toString() {
301            return String.valueOf(task);
302        }
303    }
304}
305