Home | History | Annotate | Download | only in bitmap
      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 
     17 package com.android.bitmap;
     18 
     19 import android.util.SparseArray;
     20 
     21 import com.android.mail.utils.LogTag;
     22 import com.android.mail.utils.LogUtils;
     23 import com.android.mail.utils.Utils;
     24 
     25 import java.util.ArrayDeque;
     26 import java.util.Iterator;
     27 import 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  */
     58 public 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