• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2010 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.Arrays;
27 import java.util.HashMap;
28 import java.util.HashSet;
29 import java.util.List;
30 import java.util.Set;
31 
32 /**
33  * Collects all corpus results for a single query.
34  */
35 public class Suggestions {
36     private static final boolean DBG = false;
37     private static final String TAG = "QSB.Suggestions";
38 
39     /** True if {@link Suggestions#close} has been called. */
40     private boolean mClosed = false;
41     protected final String mQuery;
42 
43     private ShortcutCursor mShortcuts;
44 
45     private final MyShortcutsObserver mShortcutsObserver = new MyShortcutsObserver();
46 
47     /**
48      * The observers that want notifications of changes to the published suggestions.
49      * This object may be accessed on any thread.
50      */
51     private final DataSetObservable mDataSetObservable = new DataSetObservable();
52 
53     /** The sources that are expected to report. */
54     private final List<Corpus> mExpectedCorpora;
55     private final HashMap<String, Integer> mCorpusPositions;
56 
57     /**
58      * All {@link SuggestionCursor} objects that have been published so far,
59      * in the same order as {@link #mExpectedCorpora}. There may be {@code null} items
60      * in the array, if not all corpora have published yet.
61      * This object may only be accessed on the UI thread.
62      * */
63     private final CorpusResult[] mCorpusResults;
64 
65     private CorpusResult mWebResult;
66 
67     private int mRefCount = 0;
68 
69     private boolean mDone = false;
70 
Suggestions(String query, List<Corpus> expectedCorpora)71     public Suggestions(String query, List<Corpus> expectedCorpora) {
72         mQuery = query;
73         mExpectedCorpora = expectedCorpora;
74         mCorpusResults = new CorpusResult[mExpectedCorpora.size()];
75         // create a map of corpus name -> position in mExpectedCorpora for sorting later
76         // (we want to keep the ordering of corpora in mCorpusResults).
77         mCorpusPositions = new HashMap<String, Integer>();
78         for (int i = 0; i < mExpectedCorpora.size(); ++i) {
79             mCorpusPositions.put(mExpectedCorpora.get(i).getName(), i);
80         }
81         if (DBG) {
82             Log.d(TAG, "new Suggestions [" + hashCode() + "] query \"" + query
83                     + "\" expected corpora: " + mExpectedCorpora);
84         }
85     }
86 
acquire()87     public void acquire() {
88         mRefCount++;
89     }
90 
release()91     public void release() {
92         mRefCount--;
93         if (mRefCount <= 0) {
94             close();
95         }
96     }
97 
getExpectedCorpora()98     public List<Corpus> getExpectedCorpora() {
99         return mExpectedCorpora;
100     }
101 
102     /**
103      * Gets the number of corpora that are expected to report.
104      */
105     @VisibleForTesting
getExpectedResultCount()106     public int getExpectedResultCount() {
107         return mExpectedCorpora.size();
108     }
109 
expectsCorpus(Corpus corpus)110     public boolean expectsCorpus(Corpus corpus) {
111         for (Corpus expectedCorpus : mExpectedCorpora) {
112             if (expectedCorpus.equals(corpus)) return true;
113         }
114         return false;
115     }
116 
117     /**
118      * Gets the set of corpora that have reported results to this suggestions set.
119      *
120      * @return A collection of corpora.
121      */
getIncludedCorpora()122     public Set<Corpus> getIncludedCorpora() {
123         HashSet<Corpus> corpora = new HashSet<Corpus>();
124         for (CorpusResult result : mCorpusResults) {
125             if (result != null) {
126                 corpora.add(result.getCorpus());
127             }
128         }
129         return corpora;
130     }
131 
132     /**
133      * Sets the shortcut suggestions.
134      * Must be called on the UI thread, or before this object is seen by the UI thread.
135      *
136      * @param shortcuts The shortcuts.
137      */
setShortcuts(ShortcutCursor shortcuts)138     public void setShortcuts(ShortcutCursor shortcuts) {
139         if (DBG) Log.d(TAG, "setShortcuts(" + shortcuts + ")");
140         if (mShortcuts != null) {
141             throw new IllegalStateException("Got duplicate shortcuts: old: " + mShortcuts
142                     + ", new: " + shortcuts);
143         }
144         if (shortcuts == null) return;
145         if (isClosed()) {
146             shortcuts.close();
147             return;
148         }
149         if (!mQuery.equals(shortcuts.getUserQuery())) {
150             throw new IllegalArgumentException("Got shortcuts for wrong query: "
151                     + mQuery + " != " + shortcuts.getUserQuery());
152         }
153         mShortcuts = shortcuts;
154         if (shortcuts != null) {
155             mShortcuts.registerDataSetObserver(mShortcutsObserver);
156         }
157         notifyDataSetChanged();
158     }
159 
160     /**
161      * Marks the suggestions set as complete, regardless of whether all corpora have
162      * returned.
163      */
done()164     public void done() {
165         mDone = true;
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      */
isDone()172     public boolean isDone() {
173         // TODO: Handle early completion because we have all the results we want.
174         return mDone || countCorpusResults() >= mExpectedCorpora.size();
175     }
176 
countCorpusResults()177     private int countCorpusResults() {
178         int count = 0;
179         for (int i = 0; i < mCorpusResults.length; ++i) {
180             if (mCorpusResults[i] != null) {
181                 count++;
182             }
183         }
184         return count;
185     }
186 
187     /**
188      * Adds a list of corpus results. Must be called on the UI thread, or before this
189      * object is seen by the UI thread.
190      */
addCorpusResults(List<CorpusResult> corpusResults)191     public void addCorpusResults(List<CorpusResult> corpusResults) {
192         if (isClosed()) {
193             for (CorpusResult corpusResult : corpusResults) {
194                 corpusResult.close();
195             }
196             return;
197         }
198 
199         for (CorpusResult corpusResult : corpusResults) {
200             if (DBG) {
201                 Log.d(TAG, "addCorpusResult["+ hashCode() + "] corpus:" +
202                         corpusResult.getCorpus().getName() + " results:" + corpusResult.getCount());
203             }
204             if (!mQuery.equals(corpusResult.getUserQuery())) {
205               throw new IllegalArgumentException("Got result for wrong query: "
206                     + mQuery + " != " + corpusResult.getUserQuery());
207             }
208             Integer pos = mCorpusPositions.get(corpusResult.getCorpus().getName());
209             if (pos == null) {
210                 Log.w(TAG, "Got unexpected CorpusResult from corpus " +
211                         corpusResult.getCorpus().getName());
212                 corpusResult.close();
213             } else {
214                 mCorpusResults[pos] = corpusResult;
215                 if (corpusResult.getCorpus().isWebCorpus()) {
216                     mWebResult = corpusResult;
217                 }
218             }
219         }
220         notifyDataSetChanged();
221     }
222 
223     /**
224      * Registers an observer that will be notified when the reported results or
225      * the done status changes.
226      */
registerDataSetObserver(DataSetObserver observer)227     public void registerDataSetObserver(DataSetObserver observer) {
228         if (mClosed) {
229             throw new IllegalStateException("registerDataSetObserver() when closed");
230         }
231         mDataSetObservable.registerObserver(observer);
232     }
233 
234 
235     /**
236      * Unregisters an observer.
237      */
unregisterDataSetObserver(DataSetObserver observer)238     public void unregisterDataSetObserver(DataSetObserver observer) {
239         mDataSetObservable.unregisterObserver(observer);
240     }
241 
242     /**
243      * Calls {@link DataSetObserver#onChanged()} on all observers.
244      */
notifyDataSetChanged()245     protected void notifyDataSetChanged() {
246         if (DBG) Log.d(TAG, "notifyDataSetChanged()");
247         mDataSetObservable.notifyChanged();
248     }
249 
250     /**
251      * Closes all the source results and unregisters all observers.
252      */
close()253     private void close() {
254         if (DBG) Log.d(TAG, "close() [" + hashCode() + "]");
255         if (mClosed) {
256             throw new IllegalStateException("Double close()");
257         }
258         mClosed = true;
259         mDataSetObservable.unregisterAll();
260         if (mShortcuts != null) {
261             mShortcuts.close();
262             mShortcuts = null;
263         }
264 
265         for (CorpusResult result : mCorpusResults) {
266             if (result != null) {
267                 result.close();
268             }
269         }
270         Arrays.fill(mCorpusResults, null);
271     }
272 
isClosed()273     public boolean isClosed() {
274         return mClosed;
275     }
276 
getShortcuts()277     public ShortcutCursor getShortcuts() {
278         return mShortcuts;
279     }
280 
refreshShortcuts(SuggestionCursor promoted)281     private void refreshShortcuts(SuggestionCursor promoted) {
282         if (DBG) Log.d(TAG, "refreshShortcuts(" + promoted + ")");
283         for (int i = 0; i < promoted.getCount(); ++i) {
284             promoted.moveTo(i);
285             if (promoted.isSuggestionShortcut()) {
286                 getShortcuts().refresh(promoted);
287             }
288         }
289     }
290 
291     @Override
finalize()292     protected void finalize() {
293         if (!mClosed) {
294             Log.e(TAG, "LEAK! Finalized without being closed: Suggestions[" + getQuery() + "]");
295         }
296     }
297 
getQuery()298     public String getQuery() {
299         return mQuery;
300     }
301 
getPromoted(Promoter promoter, int maxPromoted)302     public SuggestionCursor getPromoted(Promoter promoter, int maxPromoted) {
303         SuggestionCursor promoted = buildPromoted(promoter, maxPromoted);
304         refreshShortcuts(promoted);
305         return promoted;
306     }
307 
buildPromoted(Promoter promoter, int maxPromoted)308     protected SuggestionCursor buildPromoted(Promoter promoter, int maxPromoted) {
309         ListSuggestionCursor promoted = new ListSuggestionCursorNoDuplicates(mQuery);
310         if (promoter == null) {
311             return promoted;
312         }
313         promoter.pickPromoted(this, maxPromoted, promoted);
314         if (DBG) {
315             Log.d(TAG, "pickPromoted(" + getShortcuts() + "," + mCorpusResults + ","
316                     + maxPromoted + ") = " + promoted);
317         }
318         return promoted;
319     }
320 
321     /**
322      * Gets the list of corpus results reported so far. Do not modify or hang on to
323      * the returned iterator.
324      */
getCorpusResults()325     public Iterable<CorpusResult> getCorpusResults() {
326         ArrayList<CorpusResult> results = new ArrayList<CorpusResult>(mCorpusResults.length);
327         for (int i = 0; i < mCorpusResults.length; ++i) {
328             if (mCorpusResults[i] != null) {
329                 results.add(mCorpusResults[i]);
330             }
331         }
332         return results;
333     }
334 
getCorpusResult(Corpus corpus)335     public CorpusResult getCorpusResult(Corpus corpus) {
336         for (CorpusResult result : mCorpusResults) {
337             if (result != null && corpus.equals(result.getCorpus())) {
338                 return result;
339             }
340         }
341         return null;
342     }
343 
getWebResult()344     public CorpusResult getWebResult() {
345         return mWebResult;
346     }
347 
348     /**
349      * Gets the number of source results.
350      * Must be called on the UI thread, or before this object is seen by the UI thread.
351      */
getResultCount()352     public int getResultCount() {
353         if (isClosed()) {
354             throw new IllegalStateException("Called getSourceCount() when closed.");
355         }
356         return countCorpusResults();
357     }
358 
359     @Override
toString()360     public String toString() {
361         return "Suggestions@" + hashCode() + "{expectedCorpora=" + mExpectedCorpora
362                 + ",countCorpusResults()=" + countCorpusResults() + "}";
363     }
364 
365     private class MyShortcutsObserver extends DataSetObserver {
366         @Override
onChanged()367         public void onChanged() {
368             notifyDataSetChanged();
369         }
370     }
371 
372 }
373