1/*
2 * Copyright (C) 2012 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.inputmethod.research;
18
19import java.util.ArrayList;
20import java.util.LinkedList;
21
22/**
23 * A buffer that holds a fixed number of LogUnits.
24 *
25 * LogUnits are added in and shifted out in temporal order.  Only a subset of the LogUnits are
26 * actual words; the other LogUnits do not count toward the word limit.  Once the buffer reaches
27 * capacity, adding another LogUnit that is a word evicts the oldest LogUnits out one at a time to
28 * stay under the capacity limit.
29 *
30 * This variant of a LogBuffer has a limited memory footprint because of its limited size.  This
31 * makes it useful, for example, for recording a window of the user's most recent actions in case
32 * they want to report an observed error that they do not know how to reproduce.
33 */
34public class FixedLogBuffer extends LogBuffer {
35    /* package for test */ int mWordCapacity;
36    // The number of members of mLogUnits that are actual words.
37    private int mNumActualWords;
38
39    /**
40     * Create a new LogBuffer that can hold a fixed number of LogUnits that are words (and
41     * unlimited number of non-word LogUnits), and that outputs its result to a researchLog.
42     *
43     * @param wordCapacity maximum number of words
44     */
45    public FixedLogBuffer(final int wordCapacity) {
46        super();
47        if (wordCapacity <= 0) {
48            throw new IllegalArgumentException("wordCapacity must be 1 or greater.");
49        }
50        mWordCapacity = wordCapacity;
51        mNumActualWords = 0;
52    }
53
54    /**
55     * Adds a new LogUnit to the front of the LIFO queue, evicting existing LogUnit's
56     * (oldest first) if word capacity is reached.
57     */
58    @Override
59    public void shiftIn(final LogUnit newLogUnit) {
60        if (!newLogUnit.hasOneOrMoreWords()) {
61            // This LogUnit doesn't contain any word, so it doesn't count toward the word-limit.
62            super.shiftIn(newLogUnit);
63            return;
64        }
65        final int numWordsIncoming = newLogUnit.getNumWords();
66        if (mNumActualWords >= mWordCapacity) {
67            // Give subclass a chance to handle the buffer full condition by shifting out logUnits.
68            // TODO: Tell onBufferFull() how much space it needs to make to avoid forced eviction.
69            onBufferFull();
70            // If still full, evict.
71            if (mNumActualWords >= mWordCapacity) {
72                shiftOutWords(numWordsIncoming);
73            }
74        }
75        super.shiftIn(newLogUnit);
76        mNumActualWords += numWordsIncoming;
77    }
78
79    @Override
80    public LogUnit unshiftIn() {
81        final LogUnit logUnit = super.unshiftIn();
82        if (logUnit != null && logUnit.hasOneOrMoreWords()) {
83            mNumActualWords -= logUnit.getNumWords();
84        }
85        return logUnit;
86    }
87
88    public int getNumWords() {
89        return mNumActualWords;
90    }
91
92    /**
93     * Removes all LogUnits from the buffer without calling onShiftOut().
94     */
95    @Override
96    public void clear() {
97        super.clear();
98        mNumActualWords = 0;
99    }
100
101    /**
102     * Called when the buffer has just shifted in one more word than its maximum, and its about to
103     * shift out LogUnits to bring it back down to the maximum.
104     *
105     * Base class does nothing; subclasses may override if they want to record non-privacy sensitive
106     * events that fall off the end.
107     */
108    protected void onBufferFull() {
109    }
110
111    @Override
112    public LogUnit shiftOut() {
113        final LogUnit logUnit = super.shiftOut();
114        if (logUnit != null && logUnit.hasOneOrMoreWords()) {
115            mNumActualWords -= logUnit.getNumWords();
116        }
117        return logUnit;
118    }
119
120    /**
121     * Remove LogUnits from the front of the LogBuffer until {@code numWords} have been removed.
122     *
123     * If there are less than {@code numWords} in the buffer, shifts out all {@code LogUnit}s.
124     *
125     * @param numWords the minimum number of words in {@link LogUnit}s to shift out
126     * @return the number of actual words LogUnit}s shifted out
127     */
128    protected int shiftOutWords(final int numWords) {
129        int numWordsShiftedOut = 0;
130        do {
131            final LogUnit logUnit = shiftOut();
132            if (logUnit == null) break;
133            numWordsShiftedOut += logUnit.getNumWords();
134        } while (numWordsShiftedOut < numWords);
135        return numWordsShiftedOut;
136    }
137
138    public void shiftOutAll() {
139        final LinkedList<LogUnit> logUnits = getLogUnits();
140        while (!logUnits.isEmpty()) {
141            shiftOut();
142        }
143        mNumActualWords = 0;
144    }
145
146    /**
147     * Returns a list of {@link LogUnit}s at the front of the buffer that have words associated with
148     * them.
149     *
150     * There will be no more than {@code n} words in the returned list.  So if 2 words are
151     * requested, and the first LogUnit has 3 words, it is not returned.  If 2 words are requested,
152     * and the first LogUnit has only 1 word, and the next LogUnit 2 words, only the first LogUnit
153     * is returned.  If the first LogUnit has no words associated with it, and the second LogUnit
154     * has three words, then only the first LogUnit (which has no associated words) is returned.  If
155     * there are not enough LogUnits in the buffer to meet the word requirement, then all LogUnits
156     * will be returned.
157     *
158     * @param n The maximum number of {@link LogUnit}s with words to return.
159     * @return The list of the {@link LogUnit}s containing the first n words
160     */
161    public ArrayList<LogUnit> peekAtFirstNWords(int n) {
162        final LinkedList<LogUnit> logUnits = getLogUnits();
163        // Allocate space for n*2 logUnits.  There will be at least n, one for each word, and
164        // there may be additional for punctuation, between-word commands, etc.  This should be
165        // enough that reallocation won't be necessary.
166        final ArrayList<LogUnit> resultList = new ArrayList<LogUnit>(n * 2);
167        for (final LogUnit logUnit : logUnits) {
168            n -= logUnit.getNumWords();
169            if (n < 0) break;
170            resultList.add(logUnit);
171        }
172        return resultList;
173    }
174}
175