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