Suggestions.java revision 8b2936607176720172aee068abc5631bdf77e843
1/*
2 * Copyright (C) 2010 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.quicksearchbox;
18
19import com.google.common.annotations.VisibleForTesting;
20
21import android.database.DataSetObservable;
22import android.database.DataSetObserver;
23import android.util.Log;
24
25import java.util.ArrayList;
26import java.util.HashSet;
27import java.util.List;
28import java.util.Set;
29
30/**
31 * Collects all corpus results for a single query.
32 */
33public class Suggestions {
34    private static final boolean DBG = false;
35    private static final String TAG = "QSB.Suggestions";
36
37    /** True if {@link Suggestions#close} has been called. */
38    private boolean mClosed = false;
39    protected final String mQuery;
40
41    private ShortcutCursor mShortcuts;
42
43    private final MyShortcutsObserver mShortcutsObserver = new MyShortcutsObserver();
44
45    /**
46     * The observers that want notifications of changes to the published suggestions.
47     * This object may be accessed on any thread.
48     */
49    private final DataSetObservable mDataSetObservable = new DataSetObservable();
50
51    /** The sources that are expected to report. */
52    private final List<Corpus> mExpectedCorpora;
53
54    /**
55     * All {@link SuggestionCursor} objects that have been published so far,
56     * in the order that they were published.
57     * This object may only be accessed on the UI thread.
58     * */
59    private final ArrayList<CorpusResult> mCorpusResults;
60
61    private CorpusResult mWebResult;
62
63    private int mRefCount = 0;
64
65    public Suggestions(String query, List<Corpus> expectedCorpora) {
66        mQuery = query;
67        mExpectedCorpora = expectedCorpora;
68        mCorpusResults = new ArrayList<CorpusResult>(mExpectedCorpora.size());
69        if (DBG) {
70            Log.d(TAG, "new Suggestions [" + hashCode() + "] query \"" + query
71                    + "\" expected corpora: " + mExpectedCorpora);
72        }
73    }
74
75    public void acquire() {
76        mRefCount++;
77    }
78
79    public void release() {
80        mRefCount--;
81        if (mRefCount <= 0) {
82            close();
83        }
84    }
85
86    public List<Corpus> getExpectedCorpora() {
87        return mExpectedCorpora;
88    }
89
90    /**
91     * Gets the number of corpora that are expected to report.
92     */
93    @VisibleForTesting
94    public int getExpectedResultCount() {
95        return mExpectedCorpora.size();
96    }
97
98    /**
99     * Gets the set of corpora that have reported results to this suggestions set.
100     *
101     * @return A collection of corpora.
102     */
103    public Set<Corpus> getIncludedCorpora() {
104        HashSet<Corpus> corpora = new HashSet<Corpus>();
105        for (CorpusResult result : mCorpusResults) {
106            corpora.add(result.getCorpus());
107        }
108        return corpora;
109    }
110
111    /**
112     * Sets the shortcut suggestions.
113     * Must be called on the UI thread, or before this object is seen by the UI thread.
114     *
115     * @param shortcuts The shortcuts.
116     */
117    public void setShortcuts(ShortcutCursor shortcuts) {
118        if (DBG) Log.d(TAG, "setShortcuts(" + shortcuts + ")");
119        if (mShortcuts != null) {
120            throw new IllegalStateException("Got duplicate shortcuts: old: " + mShortcuts
121                    + ", new: " + shortcuts);
122        }
123        if (shortcuts == null) return;
124        if (isClosed()) {
125            shortcuts.close();
126            return;
127        }
128        if (!mQuery.equals(shortcuts.getUserQuery())) {
129            throw new IllegalArgumentException("Got shortcuts for wrong query: "
130                    + mQuery + " != " + shortcuts.getUserQuery());
131        }
132        mShortcuts = shortcuts;
133        if (shortcuts != null) {
134            mShortcuts.registerDataSetObserver(mShortcutsObserver);
135        }
136        notifyDataSetChanged();
137    }
138
139    /**
140     * Checks whether all sources have reported.
141     * Must be called on the UI thread, or before this object is seen by the UI thread.
142     */
143    public boolean isDone() {
144        // TODO: Handle early completion because we have all the results we want.
145        return mCorpusResults.size() >= mExpectedCorpora.size();
146    }
147
148    /**
149     * Adds a list of corpus results. Must be called on the UI thread, or before this
150     * object is seen by the UI thread.
151     */
152    public void addCorpusResults(List<CorpusResult> corpusResults) {
153        if (isClosed()) {
154            for (CorpusResult corpusResult : corpusResults) {
155                corpusResult.close();
156            }
157            return;
158        }
159
160        for (CorpusResult corpusResult : corpusResults) {
161            if (DBG) {
162                Log.d(TAG, "addCorpusResult["+ hashCode() + "] corpus:" +
163                        corpusResult.getCorpus().getName() + " results:" + corpusResult.getCount());
164            }
165            if (!mQuery.equals(corpusResult.getUserQuery())) {
166              throw new IllegalArgumentException("Got result for wrong query: "
167                    + mQuery + " != " + corpusResult.getUserQuery());
168            }
169            mCorpusResults.add(corpusResult);
170            if (corpusResult.getCorpus().isWebCorpus()) {
171                mWebResult = corpusResult;
172            }
173        }
174        notifyDataSetChanged();
175    }
176
177    /**
178     * Registers an observer that will be notified when the reported results or
179     * the done status changes.
180     */
181    public void registerDataSetObserver(DataSetObserver observer) {
182        if (mClosed) {
183            throw new IllegalStateException("registerDataSetObserver() when closed");
184        }
185        mDataSetObservable.registerObserver(observer);
186    }
187
188
189    /**
190     * Unregisters an observer.
191     */
192    public void unregisterDataSetObserver(DataSetObserver observer) {
193        mDataSetObservable.unregisterObserver(observer);
194    }
195
196    /**
197     * Calls {@link DataSetObserver#onChanged()} on all observers.
198     */
199    protected void notifyDataSetChanged() {
200        if (DBG) Log.d(TAG, "notifyDataSetChanged()");
201        mDataSetObservable.notifyChanged();
202    }
203
204    /**
205     * Closes all the source results and unregisters all observers.
206     */
207    private void close() {
208        if (DBG) Log.d(TAG, "close() [" + hashCode() + "]");
209        if (mClosed) {
210            throw new IllegalStateException("Double close()");
211        }
212        mClosed = true;
213        mDataSetObservable.unregisterAll();
214        if (mShortcuts != null) {
215            mShortcuts.close();
216            mShortcuts = null;
217        }
218
219        for (CorpusResult result : mCorpusResults) {
220            result.close();
221        }
222        mCorpusResults.clear();
223    }
224
225    public boolean isClosed() {
226        return mClosed;
227    }
228
229    public ShortcutCursor getShortcuts() {
230        return mShortcuts;
231    }
232
233    private void refreshShortcuts(SuggestionCursor promoted) {
234        if (DBG) Log.d(TAG, "refreshShortcuts(" + promoted + ")");
235        for (int i = 0; i < promoted.getCount(); ++i) {
236            promoted.moveTo(i);
237            if (promoted.isSuggestionShortcut()) {
238                getShortcuts().refresh(promoted);
239            }
240        }
241    }
242
243    @Override
244    protected void finalize() {
245        if (!mClosed) {
246            Log.e(TAG, "LEAK! Finalized without being closed: Suggestions[" + getQuery() + "]");
247        }
248    }
249
250    public String getQuery() {
251        return mQuery;
252    }
253
254    public SuggestionCursor getPromoted(Promoter promoter, int maxPromoted) {
255        SuggestionCursor promoted = buildPromoted(promoter, maxPromoted);
256        refreshShortcuts(promoted);
257        return promoted;
258    }
259
260    protected SuggestionCursor buildPromoted(Promoter promoter, int maxPromoted) {
261        ListSuggestionCursor promoted = new ListSuggestionCursorNoDuplicates(mQuery);
262        if (promoter == null) {
263            return promoted;
264        }
265        promoter.pickPromoted(this, maxPromoted, promoted);
266        if (DBG) {
267            Log.d(TAG, "pickPromoted(" + getShortcuts() + "," + mCorpusResults + ","
268                    + maxPromoted + ") = " + promoted);
269        }
270        return promoted;
271    }
272
273    /**
274     * Gets the list of corpus results reported so far. Do not modify or hang on to
275     * the returned iterator.
276     */
277    public Iterable<CorpusResult> getCorpusResults() {
278        return mCorpusResults;
279    }
280
281    public CorpusResult getCorpusResult(Corpus corpus) {
282        for (CorpusResult result : mCorpusResults) {
283            if (result.getCorpus().equals(corpus)) {
284                return result;
285            }
286        }
287        return null;
288    }
289
290    public CorpusResult getWebResult() {
291        return mWebResult;
292    }
293
294    /**
295     * Gets the number of source results.
296     * Must be called on the UI thread, or before this object is seen by the UI thread.
297     */
298    public int getResultCount() {
299        if (isClosed()) {
300            throw new IllegalStateException("Called getSourceCount() when closed.");
301        }
302        return mCorpusResults == null ? 0 : mCorpusResults.size();
303    }
304
305    @Override
306    public String toString() {
307        return "Suggestions@" + hashCode() + "{expectedCorpora=" + mExpectedCorpora
308                + ",mCorpusResults.size()=" + mCorpusResults.size() + "}";
309    }
310
311    private class MyShortcutsObserver extends DataSetObserver {
312        @Override
313        public void onChanged() {
314            notifyDataSetChanged();
315        }
316    }
317
318}
319