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 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 87 public void acquire() { 88 mRefCount++; 89 } 90 91 public void release() { 92 mRefCount--; 93 if (mRefCount <= 0) { 94 close(); 95 } 96 } 97 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 106 public int getExpectedResultCount() { 107 return mExpectedCorpora.size(); 108 } 109 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 */ 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 */ 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 */ 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 */ 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 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 */ 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 */ 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 */ 238 public void unregisterDataSetObserver(DataSetObserver observer) { 239 mDataSetObservable.unregisterObserver(observer); 240 } 241 242 /** 243 * Calls {@link DataSetObserver#onChanged()} on all observers. 244 */ 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 */ 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 273 public boolean isClosed() { 274 return mClosed; 275 } 276 277 public ShortcutCursor getShortcuts() { 278 return mShortcuts; 279 } 280 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 292 protected void finalize() { 293 if (!mClosed) { 294 Log.e(TAG, "LEAK! Finalized without being closed: Suggestions[" + getQuery() + "]"); 295 } 296 } 297 298 public String getQuery() { 299 return mQuery; 300 } 301 302 public SuggestionCursor getPromoted(Promoter promoter, int maxPromoted) { 303 SuggestionCursor promoted = buildPromoted(promoter, maxPromoted); 304 refreshShortcuts(promoted); 305 return promoted; 306 } 307 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 */ 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 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 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 */ 352 public int getResultCount() { 353 if (isClosed()) { 354 throw new IllegalStateException("Called getSourceCount() when closed."); 355 } 356 return countCorpusResults(); 357 } 358 359 @Override 360 public String toString() { 361 return "Suggestions@" + hashCode() + "{expectedCorpora=" + mExpectedCorpora 362 + ",countCorpusResults()=" + countCorpusResults() + "}"; 363 } 364 365 private class MyShortcutsObserver extends DataSetObserver { 366 @Override 367 public void onChanged() { 368 notifyDataSetChanged(); 369 } 370 } 371 372 } 373