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