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