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