• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
17 package com.android.quicksearchbox;
18 
19 import com.google.common.annotations.VisibleForTesting;
20 
21 import android.database.DataSetObservable;
22 import android.database.DataSetObserver;
23 import android.util.Log;
24 
25 import java.util.ArrayList;
26 import java.util.HashSet;
27 import java.util.List;
28 import java.util.Set;
29 
30 /**
31  * Contains all {@link SuggestionCursor} objects that have been reported.
32  */
33 public 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 ShortcutCursor 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      */
Suggestions(Promoter promoter, int maxPromoted, String query, List<Corpus> expectedCorpora)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
getQuery()95     public String getQuery() {
96         return mQuery;
97     }
98 
getExpectedCorpora()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
getExpectedResultCount()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      */
registerDataSetObserver(DataSetObserver observer)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      */
unregisterDataSetObserver(DataSetObserver observer)125     public void unregisterDataSetObserver(DataSetObserver observer) {
126         mDataSetObservable.unregisterObserver(observer);
127     }
128 
getPromoted()129     public SuggestionCursor getPromoted() {
130         if (mPromoted == null) {
131             updatePromoted();
132         }
133         if (DBG) Log.d(TAG, "getPromoted() = " + mPromoted);
134         return mPromoted;
135     }
136 
137     /**
138      * Gets the set of corpora that have reported results to this suggestions set.
139      *
140      * @return A collection of corpora.
141      */
getIncludedCorpora()142     public Set<Corpus> getIncludedCorpora() {
143         HashSet<Corpus> corpora = new HashSet<Corpus>();
144         for (CorpusResult result : mCorpusResults) {
145             corpora.add(result.getCorpus());
146         }
147         return corpora;
148     }
149 
150     /**
151      * Calls {@link DataSetObserver#onChanged()} on all observers.
152      */
notifyDataSetChanged()153     private void notifyDataSetChanged() {
154         if (DBG) Log.d(TAG, "notifyDataSetChanged()");
155         mDataSetObservable.notifyChanged();
156     }
157 
158     /**
159      * Closes all the source results and unregisters all observers.
160      */
close()161     public void close() {
162         if (DBG) Log.d(TAG, "close() [" + mId + "]");
163         if (mClosed) {
164             throw new IllegalStateException("Double close()");
165         }
166         mDataSetObservable.unregisterAll();
167         mClosed = true;
168         if (mShortcuts != null) {
169             mShortcuts.close();
170             mShortcuts = null;
171         }
172         for (CorpusResult result : mCorpusResults) {
173             result.close();
174         }
175         mCorpusResults.clear();
176     }
177 
isClosed()178     public boolean isClosed() {
179         return mClosed;
180     }
181 
182     @Override
finalize()183     protected void finalize() {
184         if (!mClosed) {
185             Log.e(TAG, "LEAK! Finalized without being closed: Suggestions[" + mQuery + "]");
186         }
187     }
188 
189     /**
190      * Checks whether all sources have reported.
191      * Must be called on the UI thread, or before this object is seen by the UI thread.
192      */
isDone()193     public boolean isDone() {
194         // TODO: Handle early completion because we have all the results we want.
195         return mCorpusResults.size() >= mExpectedCorpora.size();
196     }
197 
198     /**
199      * Sets the shortcut suggestions.
200      * Must be called on the UI thread, or before this object is seen by the UI thread.
201      *
202      * @param shortcuts The shortcuts.
203      */
setShortcuts(ShortcutCursor shortcuts)204     public void setShortcuts(ShortcutCursor shortcuts) {
205         if (DBG) Log.d(TAG, "setShortcuts(" + shortcuts + ")");
206         mShortcuts = shortcuts;
207         if (shortcuts != null) {
208             mShortcuts.registerDataSetObserver(mShortcutsObserver);
209         }
210     }
211 
212     /**
213      * Adds a list of corpus results. Must be called on the UI thread, or before this
214      * object is seen by the UI thread.
215      */
addCorpusResults(List<CorpusResult> corpusResults)216     public void addCorpusResults(List<CorpusResult> corpusResults) {
217         if (mClosed) {
218             for (CorpusResult corpusResult : corpusResults) {
219                 corpusResult.close();
220             }
221             return;
222         }
223 
224         for (CorpusResult corpusResult : corpusResults) {
225             if (DBG) {
226                 Log.d(TAG, "addCorpusResult["+ mId + "] corpus:" +
227                         corpusResult.getCorpus().getName() + " results:" + corpusResult.getCount());
228             }
229             if (!mQuery.equals(corpusResult.getUserQuery())) {
230               throw new IllegalArgumentException("Got result for wrong query: "
231                     + mQuery + " != " + corpusResult.getUserQuery());
232             }
233             mCorpusResults.add(corpusResult);
234         }
235         mPromoted = null;
236         notifyDataSetChanged();
237     }
238 
updatePromoted()239     private void updatePromoted() {
240         if (mSingleCorpusFilter == null) {
241             ListSuggestionCursor promoted = new ListSuggestionCursorNoDuplicates(mQuery);
242             mPromoted = promoted;
243             if (mPromoter == null) {
244                 return;
245             }
246             mPromoter.pickPromoted(mShortcuts, mCorpusResults, mMaxPromoted, promoted);
247             if (DBG) {
248                 Log.d(TAG, "pickPromoted(" + mShortcuts + "," + mCorpusResults + ","
249                         + mMaxPromoted + ") = " + mPromoted);
250             }
251             refreshShortcuts();
252         } else {
253             mPromoted = getCorpusResult(mSingleCorpusFilter);
254             if (mPromoted == null) {
255                 mPromoted = new ListSuggestionCursor(mQuery);
256             }
257         }
258     }
259 
refreshShortcuts()260     private void refreshShortcuts() {
261         if (DBG) Log.d(TAG, "refreshShortcuts(" + mPromoted + ")");
262         for (int i = 0; i < mPromoted.getCount(); ++i) {
263             mPromoted.moveTo(i);
264             if (mPromoted.isSuggestionShortcut()) {
265                 mShortcuts.refresh(mPromoted);
266             }
267         }
268     }
269 
270 
getCorpusResult(Corpus corpus)271     private CorpusResult getCorpusResult(Corpus corpus) {
272         for (CorpusResult result : mCorpusResults) {
273             if (result.getCorpus().equals(mSingleCorpusFilter)) {
274                 return result;
275             }
276         }
277         return null;
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      */
getResultCount()284     public int getResultCount() {
285         if (mClosed) {
286             throw new IllegalStateException("Called getSourceCount() when closed.");
287         }
288         return mCorpusResults == null ? 0 : mCorpusResults.size();
289     }
290 
filterByCorpus(Corpus singleCorpus)291     public void filterByCorpus(Corpus singleCorpus) {
292         if (mSingleCorpusFilter == singleCorpus) {
293             return;
294         }
295         mSingleCorpusFilter = singleCorpus;
296         if ((mExpectedCorpora.size() == 1) && (mExpectedCorpora.get(0) == singleCorpus)) {
297             return;
298         }
299         updatePromoted();
300         notifyDataSetChanged();
301     }
302 
303     @Override
toString()304     public String toString() {
305         return "Suggestions{expectedCorpora=" + mExpectedCorpora
306                 + ",mCorpusResults.size()=" + mCorpusResults.size() + "}";
307     }
308 
309     private class MyShortcutsObserver extends DataSetObserver {
310         @Override
onChanged()311         public void onChanged() {
312             mPromoted = null;
313             notifyDataSetChanged();
314         }
315     }
316 
317 }
318