Suggestions.java revision 82bcf002f2f736afcd6cb548c19aa4e8ac12b421
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 android.database.DataSetObservable;
20import android.database.DataSetObserver;
21import android.util.Log;
22
23import java.util.ArrayList;
24import java.util.HashMap;
25import java.util.Set;
26
27/**
28 * Contains all {@link SuggestionCursor} objects that have been reported.
29 */
30public class Suggestions {
31
32    private static final boolean DBG = false;
33    private static final String TAG = "QSB.Suggestions";
34
35    private final int mMaxPromoted;
36
37    private final String mQuery;
38
39    /** The number of sources that are expected to report. */
40    private final int mExpectedCorpusCount;
41
42    /** The corpora that are promoted. */
43    private final Set<Corpus> mPromotedCorpora;
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    /**
52     * All {@link SuggestionCursor} objects that have been published so far,
53     * in the order that they were published.
54     * This object may only be accessed on the UI thread.
55     * */
56    private final ArrayList<CorpusResult> mCorpusResults;
57
58    /**
59     * All {@link SuggestionCursor} objects that have been published so far.
60     * This object may only be accessed on the UI thread.
61     * */
62    private final HashMap<Corpus,CorpusResult> mResultsByCorpus;
63
64    private SuggestionCursor mShortcuts;
65
66    private MyShortcutsObserver mShortcutsObserver = new MyShortcutsObserver();
67
68    /** True if {@link Suggestions#close} has been called. */
69    private boolean mClosed = false;
70
71    private final Promoter mPromoter;
72
73    private ListSuggestionCursor mPromoted;
74
75    /**
76     * Creates a new empty Suggestions.
77     *
78     * @param expectedCorpusCount The number of sources that are expected to report.
79     */
80    public Suggestions(Promoter promoter, int maxPromoted,
81            String query, int expectedCorpusCount, Set<Corpus> promotedCorpora) {
82        mPromoter = promoter;
83        mMaxPromoted = maxPromoted;
84        mQuery = query;
85        mExpectedCorpusCount = expectedCorpusCount;
86        mPromotedCorpora = promotedCorpora;
87        mCorpusResults = new ArrayList<CorpusResult>(mExpectedCorpusCount);
88        mResultsByCorpus = new HashMap<Corpus,CorpusResult>(mExpectedCorpusCount);
89        mPromoted = null;  // will be set by updatePromoted()
90    }
91
92    public String getQuery() {
93        return mQuery;
94    }
95
96    /**
97     * Gets the number of sources that are expected to report.
98     */
99    public int getExpectedSourceCount() {
100        return mExpectedCorpusCount;
101    }
102
103    /**
104     * Registers an observer that will be notified when the reported results or
105     * the done status changes.
106     */
107    public void registerDataSetObserver(DataSetObserver observer) {
108        if (mClosed) {
109            throw new IllegalStateException("registerDataSetObserver() when closed");
110        }
111        mDataSetObservable.registerObserver(observer);
112    }
113
114    /**
115     * Unregisters an observer.
116     */
117    public void unregisterDataSetObserver(DataSetObserver observer) {
118        mDataSetObservable.unregisterObserver(observer);
119    }
120
121    public SuggestionCursor getPromoted() {
122        if (mPromoted == null) {
123            updatePromoted();
124        }
125        return mPromoted;
126    }
127
128    /**
129     * Calls {@link DataSetObserver#onChanged()} on all observers.
130     */
131    private void notifyDataSetChanged() {
132        if (DBG) Log.d(TAG, "notifyDataSetChanged()");
133        mDataSetObservable.notifyChanged();
134    }
135
136    /**
137     * Closes all the source results and unregisters all observers.
138     */
139    public void close() {
140        if (DBG) Log.d(TAG, "close()");
141        if (mClosed) {
142            throw new IllegalStateException("Double close()");
143        }
144        mDataSetObservable.unregisterAll();
145        mClosed = true;
146        if (mShortcuts != null) {
147            mShortcuts.close();
148            mShortcuts = null;
149        }
150        for (CorpusResult result : mCorpusResults) {
151            result.close();
152        }
153        mCorpusResults.clear();
154        mResultsByCorpus.clear();
155    }
156
157    public boolean isClosed() {
158        return mClosed;
159    }
160
161    @Override
162    protected void finalize() {
163        if (!mClosed) {
164            Log.e(TAG, "LEAK! Finalized without being closed: Suggestions[" + mQuery + "]");
165        }
166    }
167
168    /**
169     * Checks whether all sources have reported.
170     * Must be called on the UI thread, or before this object is seen by the UI thread.
171     */
172    public boolean isDone() {
173        // TODO: Handle early completion because we have all the results we want.
174        return mCorpusResults.size() >= mExpectedCorpusCount;
175    }
176
177    /**
178     * Sets the shortcut suggestions.
179     * Must be called on the UI thread, or before this object is seen by the UI thread.
180     *
181     * @param shortcuts The shortcuts.
182     */
183    public void setShortcuts(SuggestionCursor shortcuts) {
184        if (DBG) Log.d(TAG, "setShortcuts(" + shortcuts + ")");
185        mShortcuts = shortcuts;
186        if (shortcuts != null) {
187            mShortcuts.registerDataSetObserver(mShortcutsObserver);
188        }
189    }
190
191    /**
192     * Adds a corpus result. Must be called on the UI thread, or before this
193     * object is seen by the UI thread.
194     */
195    public void addCorpusResult(CorpusResult corpusResult) {
196        if (mClosed) {
197            corpusResult.close();
198            return;
199        }
200        if (!mQuery.equals(corpusResult.getUserQuery())) {
201          throw new IllegalArgumentException("Got result for wrong query: "
202                + mQuery + " != " + corpusResult.getUserQuery());
203        }
204        mCorpusResults.add(corpusResult);
205        mResultsByCorpus.put(corpusResult.getCorpus(), corpusResult);
206        mPromoted = null;
207        notifyDataSetChanged();
208    }
209
210    private void updatePromoted() {
211        mPromoted = new ListSuggestionCursorNoDuplicates(mQuery);
212        if (mPromoter == null) {
213            return;
214        }
215        mPromoter.pickPromoted(mShortcuts, mCorpusResults, mMaxPromoted, mPromoted,
216                mPromotedCorpora);
217    }
218
219    /**
220     * Gets a given corpus result.
221     * Must be called on the UI thread, or before this object is seen by the UI thread.
222     *
223     * @param corpus corpus
224     * @return The source result for the given source. {@code null} if the source has not
225     *         yet returned.
226     */
227    public CorpusResult getCorpusResult(Corpus corpus) {
228        if (mClosed) {
229            throw new IllegalStateException("getCorpusResult(" + corpus + ") when closed.");
230        }
231        return mResultsByCorpus.get(corpus);
232    }
233
234    /**
235     * Gets the number of source results.
236     * Must be called on the UI thread, or before this object is seen by the UI thread.
237     */
238    public int getSourceCount() {
239        if (mClosed) {
240            throw new IllegalStateException("Called getSourceCount() when closed.");
241        }
242        return mCorpusResults == null ? 0 : mCorpusResults.size();
243    }
244
245    private class MyShortcutsObserver extends DataSetObserver {
246        @Override
247        public void onChanged() {
248            mPromoted = null;
249            notifyDataSetChanged();
250        }
251    }
252
253}
254