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