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