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