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