summaryrefslogtreecommitdiff
path: root/src/com/android/bitmap/ContiguousFIFOAggregator.java
blob: 9716fb88a941af182894822dc6bcfea143bf6cf0 (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
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
/*
 * Copyright (C) 2013 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.bitmap;

import android.util.Log;
import android.util.SparseArray;

import com.android.bitmap.util.Trace;

import java.util.ArrayDeque;
import java.util.Iterator;
import java.util.Queue;

/**
 * An implementation of a task aggregator that executes tasks in the order that they are expected
 * . All tasks that is given to {@link #execute(Object, Runnable)} must correspond to a key. This
 * key must have been previously declared with {@link #expect(Object, Callback)}.
 * The task will be scheduled to run when its corresponding key becomes the first expected key
 * amongst the other keys in this aggregator.
 * <p/>
 * If on {@link #execute(Object, Runnable)} the key is not expected, the task will be executed
 * immediately as an edge case.
 * <p/>
 * A characteristic scenario is as follows:
 * <ol>
 * <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
 * the first expected key.</li>
 * <li>Execute task <b>2</b> for key <b>B</b>. The first expected key is <b>A</b>,
 * which has no task associated with it, so we store task <b>2</b>. </li>
 * <li>Execute task <b>4</b> for key <b>Z</b>. We store task <b>4</b>. </li>
 * <li>Execute task <b>1</b> for key <b>A</b>. We run task <b>1</b> because its key <b>A</b> is
 * the first expected, then we remove key <b>A</b> from the list of keys that we expect.</li>
 * <li>This causes key <b>B</b> to be the first expected key, and we see that we have previously
 * stored task <b>2</b> for that key. We run task <b>2</b> and remove key <b>B</b>. </li>
 * <li>Key <b>C</b> is now the first expected key, but it has no task,
 * so nothing happens. Task <b>4</b>, which was previously stored,
 * cannot run until its corresponding key <b>Z</b> becomes the first expected key. This can
 * happen if a task comes in for key <b>C</b> or if forget is called on key <b>C</b>.</li>
 * </ol>
 * <p/>
 * ContiguousFIFOAggregator is not thread safe.
 */
public class ContiguousFIFOAggregator<T> {
    private final Queue<T> mExpected;
    private final SparseArray<Value> mTasks;

    private static final String TAG = ContiguousFIFOAggregator.class.getSimpleName();
    private static final boolean DEBUG = false;

    /**
     * Create a new ContiguousFIFOAggregator.
     * <p/>
     * The nature of the prioritization means that the number of stored keys and tasks may grow
     * unbounded. This may happen, for instance, if the top priority key is never given a task to
     * {@link #execute(Object, Runnable)}. However, in practice, if you are generating tasks in
     * response to UI elements appearing on the screen, you will only have a bounded set of keys.
     * UI elements that scroll off the screen will call {@link #forget(Object)} while new elements
     * will call {@link #expect(Object, Callback)}. This means that the expected
     * number of keys and tasks is
     * the maximum number of UI elements that you expect to show on the screen at any time.
     */
    public ContiguousFIFOAggregator() {
        mExpected = new ArrayDeque<T>();
        mTasks = new SparseArray<Value>();
    }

    /**
     * Declare a key to be expected in the future. The order in which you expect keys is very
     * important. Keys that are declared first are guaranteed to have their tasks run first. You
     * must call either {@link #forget(Object)} or {@link #execute(Object, Runnable)}
     * with this key in the future, or you will leak the key.
     *
     * If you call expect with a previously expected key, the key will be placed at the back of
     * the expected queue and its callback will be replaced with this one.
     *
     * @param key      the key to expect a task for. Use the same key when setting the task later
     *                 with {@link #execute (Object, Runnable)}.
     * @param callback the callback to notify when the key becomes the first expected key, or null.
     */
    public void expect(final T key, final Callback<T> callback) {
        if (key == null) {
            throw new IllegalArgumentException("Do not use null keys.");
        }

        Trace.beginSection("pool expect");
        final int hash = key.hashCode();
        if (contains(key)) {
            mExpected.remove(key);
            mTasks.remove(hash);
        }
        final boolean isFirst = mExpected.isEmpty();
        mExpected.offer(key);
        mTasks.put(hash, new Value(callback, null));
        if (DEBUG) {
            Log.d(TAG, String.format("ContiguousFIFOAggregator >> tasks: %s", prettyPrint()));
        }

        if (isFirst) {
            onFirstExpectedChanged(key);
        }
        Trace.endSection();
    }

    /**
     * Remove a previously declared key that we no longer expect to execute a task for. This
     * potentially allows another key to now become the first expected key,
     * and so this may trigger one or more tasks to be executed.
     *
     * @param key the key previously declared in {@link #expect(Object, Callback)}.
     *
     */
    public void forget(final T key) {
        if (key == null) {
            throw new IllegalArgumentException("Do not use null keys.");
        }

        if (!contains(key)) {
            return;
        }

        Trace.beginSection("pool forget");
        final boolean removedFirst = key.equals(mExpected.peek());
        mExpected.remove(key);
        mTasks.delete(key.hashCode());
        if (DEBUG) {
            Log.d(TAG, String.format("ContiguousFIFOAggregator  < tasks: %s", prettyPrint()));
        }

        final T second;
        if (removedFirst && (second = mExpected.peek()) != null) {
            // We removed the first key. The second key is now first.
            onFirstExpectedChanged(second);
        }

        maybeExecuteNow();
        Trace.endSection();
    }

    /**
     * Attempt to execute a task corresponding to a previously declared key. If the key is the
     * first expected key, the task will be executed immediately. Otherwise, the task is stored
     * until its key becomes the first expected key. Execution of a task results in the removal
     * of that key, which potentially allows another key to now become the first expected key,
     * and may cause one or more other tasks to be executed.
     * <p/>
     * If the key is not expected, the task will be executed immediately as an edge case.
     *
     * @param key  the key previously declared in {@link #expect(Object, Callback)}.
     * @param task the task to execute or store, depending on its corresponding key.
     */
    public void execute(final T key, final Runnable task) {
        Trace.beginSection("pool execute");
        final int hash = key.hashCode();
        final Value value = mTasks.get(hash);
        if (value == null || task == null) {
            if (task != null) {
                task.run();
            }
            Trace.endSection();
            return;
        }
        value.task = task;
        if (DEBUG) {
            Log.d(TAG, String.format("ContiguousFIFOAggregator ++ tasks: %s", prettyPrint()));
        }
        maybeExecuteNow();
        Trace.endSection();
    }

    /**
     * Triggered by {@link #execute(Object, Runnable)} and {@link #forget(Object)}. The keys or
     * tasks have changed, which may cause one or more tasks to be executed. This method will
     * continue to execute tasks associated with the first expected key to the last expected key,
     * stopping when it finds that the first expected key has not yet been assigned a task.
     */
    private void maybeExecuteNow() {
        T first;
        int count = 0;
        while (!mExpected.isEmpty()) {
            Trace.beginSection("pool maybeExecuteNow loop");
            first = mExpected.peek();
            if (count > 0) {
                // When count == 0, the key is already first.
                onFirstExpectedChanged(first);
            }

            final int hash = first.hashCode();
            final Value value = mTasks.get(hash);
            if (value.task == null) {
                Trace.endSection();
                break;
            }

            mExpected.poll();
            mTasks.delete(hash);
            if (DEBUG) {
                Log.d(TAG, String.format("ContiguousFIFOAggregator  - tasks: %s", prettyPrint()));
            }
            value.task.run();
            count++;
            Trace.endSection();
        }
    }

    /**
     * This method should only be called once for any key.
     * @param key The key that has become the new first expected key.
     */
    private void onFirstExpectedChanged(final T key) {
        final int hash = key.hashCode();
        final Value value = mTasks.get(hash);
        if (value == null) {
            return;
        }
        final Callback<T> callback = value.callback;
        if (callback == null) {
            return;
        }
        if (DEBUG) {
            Log.d(TAG, String.format("ContiguousFIFOAggregator    first: %d", hash));
        }
        callback.onBecomeFirstExpected(key);
    }

    private boolean contains(final T key) {
        return mTasks.get(key.hashCode()) != null;
    }

    /**
     * Get a pretty string representing the internal data.
     * @return  a String for the internal data.
     */
    private String prettyPrint() {
        if (mExpected.isEmpty()) {
            return "{}";
        }

        StringBuilder buffer = new StringBuilder(mExpected.size() * 28);
        buffer.append('{');
        Iterator<T> it = mExpected.iterator();
        while (it.hasNext()) {
            final T key = it.next();
            final int hash = key.hashCode();
            buffer.append(hash);
            buffer.append('=');
            final Value value = mTasks.get(hash);
            buffer.append(value);
            if (it.hasNext()) {
                buffer.append(", ");
            }
        }
        buffer.append('}');
        if (mExpected.size() != mTasks.size()) {
            buffer.append(" error");
        }
        return buffer.toString();
    }

    /**
     * Implement this interface if you want to be notified when the key becomes the first
     * expected key.
     * @param <T> The type of the key used for the aggregator.
     */
    public interface Callback<T> {

        /**
         * The key you declared as expected has become the first expected key in this aggregator.
         *
         * We don't need a noLongerFirstExpected() method because this aggregator strictly adds
         * additional to the end of the queue. For a first expected key to no longer be the first
         * expected, it would either have to be forgotten with {@link #forget(Object)} or a task
         * assigned and executed with {@link #execute(Object, Runnable)}.
         *
         * @param key The key that became first. We provide the key so the callback can either not
         *            keep state, or it can keep state which may have changed so the callback can do
         *            a comparison.
         */
        void onBecomeFirstExpected(final T key);
    }

    /**
     * Holds the callback and task for when a key later becomes the first expected key.
     */
    private class Value {

        final Callback<T> callback;
        Runnable task;

        Value(final Callback<T> callback, final Runnable task) {
            this.callback = callback;
            this.task = task;
        }

        @Override
        public String toString() {
            return String.valueOf(task);
        }
    }
}