Suggestions.java revision dfc1772caf35942837d83331d787eb10734c37cb
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    public boolean expectsCorpus(Corpus corpus) {
99        for (Corpus expectedCorpus : mExpectedCorpora) {
100            if (expectedCorpus.equals(corpus)) return true;
101        }
102        return false;
103    }
104
105    /**
106     * Gets the set of corpora that have reported results to this suggestions set.
107     *
108     * @return A collection of corpora.
109     */
110    public Set<Corpus> getIncludedCorpora() {
111        HashSet<Corpus> corpora = new HashSet<Corpus>();
112        for (CorpusResult result : mCorpusResults) {
113            corpora.add(result.getCorpus());
114        }
115        return corpora;
116    }
117
118    /**
119     * Sets the shortcut suggestions.
120     * Must be called on the UI thread, or before this object is seen by the UI thread.
121     *
122     * @param shortcuts The shortcuts.
123     */
124    public void setShortcuts(ShortcutCursor shortcuts) {
125        if (DBG) Log.d(TAG, "setShortcuts(" + shortcuts + ")");
126        if (mShortcuts != null) {
127            throw new IllegalStateException("Got duplicate shortcuts: old: " + mShortcuts
128                    + ", new: " + shortcuts);
129        }
130        if (shortcuts == null) return;
131        if (isClosed()) {
132            shortcuts.close();
133            return;
134        }
135        if (!mQuery.equals(shortcuts.getUserQuery())) {
136            throw new IllegalArgumentException("Got shortcuts for wrong query: "
137                    + mQuery + " != " + shortcuts.getUserQuery());
138        }
139        mShortcuts = shortcuts;
140        if (shortcuts != null) {
141            mShortcuts.registerDataSetObserver(mShortcutsObserver);
142        }
143        notifyDataSetChanged();
144    }
145
146    /**
147     * Checks whether all sources have reported.
148     * Must be called on the UI thread, or before this object is seen by the UI thread.
149     */
150    public boolean isDone() {
151        // TODO: Handle early completion because we have all the results we want.
152        return mCorpusResults.size() >= mExpectedCorpora.size();
153    }
154
155    /**
156     * Adds a list of corpus results. Must be called on the UI thread, or before this
157     * object is seen by the UI thread.
158     */
159    public void addCorpusResults(List<CorpusResult> corpusResults) {
160        if (isClosed()) {
161            for (CorpusResult corpusResult : corpusResults) {
162                corpusResult.close();
163            }
164            return;
165        }
166
167        for (CorpusResult corpusResult : corpusResults) {
168            if (DBG) {
169                Log.d(TAG, "addCorpusResult["+ hashCode() + "] corpus:" +
170                        corpusResult.getCorpus().getName() + " results:" + corpusResult.getCount());
171            }
172            if (!mQuery.equals(corpusResult.getUserQuery())) {
173              throw new IllegalArgumentException("Got result for wrong query: "
174                    + mQuery + " != " + corpusResult.getUserQuery());
175            }
176            mCorpusResults.add(corpusResult);
177            if (corpusResult.getCorpus().isWebCorpus()) {
178                mWebResult = corpusResult;
179            }
180        }
181        notifyDataSetChanged();
182    }
183
184    /**
185     * Registers an observer that will be notified when the reported results or
186     * the done status changes.
187     */
188    public void registerDataSetObserver(DataSetObserver observer) {
189        if (mClosed) {
190            throw new IllegalStateException("registerDataSetObserver() when closed");
191        }
192        mDataSetObservable.registerObserver(observer);
193    }
194
195
196    /**
197     * Unregisters an observer.
198     */
199    public void unregisterDataSetObserver(DataSetObserver observer) {
200        mDataSetObservable.unregisterObserver(observer);
201    }
202
203    /**
204     * Calls {@link DataSetObserver#onChanged()} on all observers.
205     */
206    protected void notifyDataSetChanged() {
207        if (DBG) Log.d(TAG, "notifyDataSetChanged()");
208        mDataSetObservable.notifyChanged();
209    }
210
211    /**
212     * Closes all the source results and unregisters all observers.
213     */
214    private void close() {
215        if (DBG) Log.d(TAG, "close() [" + hashCode() + "]");
216        if (mClosed) {
217            throw new IllegalStateException("Double close()");
218        }
219        mClosed = true;
220        mDataSetObservable.unregisterAll();
221        if (mShortcuts != null) {
222            mShortcuts.close();
223            mShortcuts = null;
224        }
225
226        for (CorpusResult result : mCorpusResults) {
227            result.close();
228        }
229        mCorpusResults.clear();
230    }
231
232    public boolean isClosed() {
233        return mClosed;
234    }
235
236    public ShortcutCursor getShortcuts() {
237        return mShortcuts;
238    }
239
240    private void refreshShortcuts(SuggestionCursor promoted) {
241        if (DBG) Log.d(TAG, "refreshShortcuts(" + promoted + ")");
242        for (int i = 0; i < promoted.getCount(); ++i) {
243            promoted.moveTo(i);
244            if (promoted.isSuggestionShortcut()) {
245                getShortcuts().refresh(promoted);
246            }
247        }
248    }
249
250    @Override
251    protected void finalize() {
252        if (!mClosed) {
253            Log.e(TAG, "LEAK! Finalized without being closed: Suggestions[" + getQuery() + "]");
254        }
255    }
256
257    public String getQuery() {
258        return mQuery;
259    }
260
261    public SuggestionCursor getPromoted(Promoter promoter, int maxPromoted) {
262        SuggestionCursor promoted = buildPromoted(promoter, maxPromoted);
263        refreshShortcuts(promoted);
264        return promoted;
265    }
266
267    protected SuggestionCursor buildPromoted(Promoter promoter, int maxPromoted) {
268        ListSuggestionCursor promoted = new ListSuggestionCursorNoDuplicates(mQuery);
269        if (promoter == null) {
270            return promoted;
271        }
272        promoter.pickPromoted(this, maxPromoted, promoted);
273        if (DBG) {
274            Log.d(TAG, "pickPromoted(" + getShortcuts() + "," + mCorpusResults + ","
275                    + maxPromoted + ") = " + promoted);
276        }
277        return promoted;
278    }
279
280    /**
281     * Gets the list of corpus results reported so far. Do not modify or hang on to
282     * the returned iterator.
283     */
284    public Iterable<CorpusResult> getCorpusResults() {
285        return mCorpusResults;
286    }
287
288    public CorpusResult getCorpusResult(Corpus corpus) {
289        for (CorpusResult result : mCorpusResults) {
290            if (result.getCorpus().equals(corpus)) {
291                return result;
292            }
293        }
294        return null;
295    }
296
297    public CorpusResult getWebResult() {
298        return mWebResult;
299    }
300
301    /**
302     * Gets the number of source results.
303     * Must be called on the UI thread, or before this object is seen by the UI thread.
304     */
305    public int getResultCount() {
306        if (isClosed()) {
307            throw new IllegalStateException("Called getSourceCount() when closed.");
308        }
309        return mCorpusResults == null ? 0 : mCorpusResults.size();
310    }
311
312    @Override
313    public String toString() {
314        return "Suggestions@" + hashCode() + "{expectedCorpora=" + mExpectedCorpora
315                + ",mCorpusResults.size()=" + mCorpusResults.size() + "}";
316    }
317
318    private class MyShortcutsObserver extends DataSetObserver {
319        @Override
320        public void onChanged() {
321            notifyDataSetChanged();
322        }
323    }
324
325}
326