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