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