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