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