Suggestions.java revision 870b826dfbc5ef7518826007f22343dc963d6d04
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.HashMap; 27import java.util.HashSet; 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 38 private final int mMaxPromoted; 39 40 private final String mQuery; 41 42 /** The number of sources that are expected to report. */ 43 private final int mExpectedCorpusCount; 44 45 /** The corpora that are promoted. */ 46 private final Set<Corpus> mPromotedCorpora; 47 48 /** 49 * The observers that want notifications of changes to the published suggestions. 50 * This object may be accessed on any thread. 51 */ 52 private final DataSetObservable mDataSetObservable = new DataSetObservable(); 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 /** 62 * All {@link SuggestionCursor} objects that have been published so far. 63 * This object may only be accessed on the UI thread. 64 * */ 65 private final HashMap<Corpus,CorpusResult> mResultsByCorpus; 66 67 private SuggestionCursor mShortcuts; 68 69 private MyShortcutsObserver mShortcutsObserver = new MyShortcutsObserver(); 70 71 /** True if {@link Suggestions#close} has been called. */ 72 private boolean mClosed = false; 73 74 private final Promoter mPromoter; 75 76 private ListSuggestionCursor mPromoted; 77 78 /** 79 * Creates a new empty Suggestions. 80 * 81 * @param expectedCorpusCount The number of sources that are expected to report. 82 */ 83 public Suggestions(Promoter promoter, int maxPromoted, 84 String query, int expectedCorpusCount, Set<Corpus> promotedCorpora) { 85 mPromoter = promoter; 86 mMaxPromoted = maxPromoted; 87 mQuery = query; 88 mExpectedCorpusCount = expectedCorpusCount; 89 mPromotedCorpora = promotedCorpora; 90 mCorpusResults = new ArrayList<CorpusResult>(mExpectedCorpusCount); 91 mResultsByCorpus = new HashMap<Corpus,CorpusResult>(mExpectedCorpusCount); 92 mPromoted = null; // will be set by updatePromoted() 93 } 94 95 @VisibleForTesting 96 public String getQuery() { 97 return mQuery; 98 } 99 100 /** 101 * Gets the number of sources that are expected to report. 102 */ 103 @VisibleForTesting 104 public int getExpectedSourceCount() { 105 return mExpectedCorpusCount; 106 } 107 108 /** 109 * Registers an observer that will be notified when the reported results or 110 * the done status changes. 111 */ 112 public void registerDataSetObserver(DataSetObserver observer) { 113 if (mClosed) { 114 throw new IllegalStateException("registerDataSetObserver() when closed"); 115 } 116 mDataSetObservable.registerObserver(observer); 117 } 118 119 /** 120 * Unregisters an observer. 121 */ 122 public void unregisterDataSetObserver(DataSetObserver observer) { 123 mDataSetObservable.unregisterObserver(observer); 124 } 125 126 public SuggestionCursor getPromoted() { 127 if (mPromoted == null) { 128 updatePromoted(); 129 } 130 return mPromoted; 131 } 132 133 /** 134 * Gets the set of corpora that have reported results to this suggestions set. 135 * 136 * @return A collection of corpora. 137 */ 138 public Set<Corpus> getIncludedCorpora() { 139 return new HashSet<Corpus>(mResultsByCorpus.keySet()); 140 } 141 142 /** 143 * Calls {@link DataSetObserver#onChanged()} on all observers. 144 */ 145 private void notifyDataSetChanged() { 146 if (DBG) Log.d(TAG, "notifyDataSetChanged()"); 147 mDataSetObservable.notifyChanged(); 148 } 149 150 /** 151 * Closes all the source results and unregisters all observers. 152 */ 153 public void close() { 154 if (DBG) Log.d(TAG, "close()"); 155 if (mClosed) { 156 throw new IllegalStateException("Double close()"); 157 } 158 mDataSetObservable.unregisterAll(); 159 mClosed = true; 160 if (mShortcuts != null) { 161 mShortcuts.close(); 162 mShortcuts = null; 163 } 164 for (CorpusResult result : mCorpusResults) { 165 result.close(); 166 } 167 mCorpusResults.clear(); 168 mResultsByCorpus.clear(); 169 } 170 171 public boolean isClosed() { 172 return mClosed; 173 } 174 175 @Override 176 protected void finalize() { 177 if (!mClosed) { 178 Log.e(TAG, "LEAK! Finalized without being closed: Suggestions[" + mQuery + "]"); 179 } 180 } 181 182 /** 183 * Checks whether all sources have reported. 184 * Must be called on the UI thread, or before this object is seen by the UI thread. 185 */ 186 public boolean isDone() { 187 // TODO: Handle early completion because we have all the results we want. 188 return mCorpusResults.size() >= mExpectedCorpusCount; 189 } 190 191 /** 192 * Sets the shortcut suggestions. 193 * Must be called on the UI thread, or before this object is seen by the UI thread. 194 * 195 * @param shortcuts The shortcuts. 196 */ 197 public void setShortcuts(SuggestionCursor shortcuts) { 198 if (DBG) Log.d(TAG, "setShortcuts(" + shortcuts + ")"); 199 mShortcuts = shortcuts; 200 if (shortcuts != null) { 201 mShortcuts.registerDataSetObserver(mShortcutsObserver); 202 } 203 } 204 205 /** 206 * Adds a corpus result. Must be called on the UI thread, or before this 207 * object is seen by the UI thread. 208 */ 209 public void addCorpusResult(CorpusResult corpusResult) { 210 if (mClosed) { 211 corpusResult.close(); 212 return; 213 } 214 if (!mQuery.equals(corpusResult.getUserQuery())) { 215 throw new IllegalArgumentException("Got result for wrong query: " 216 + mQuery + " != " + corpusResult.getUserQuery()); 217 } 218 mCorpusResults.add(corpusResult); 219 mResultsByCorpus.put(corpusResult.getCorpus(), corpusResult); 220 mPromoted = null; 221 notifyDataSetChanged(); 222 } 223 224 private void updatePromoted() { 225 mPromoted = new ListSuggestionCursorNoDuplicates(mQuery); 226 if (mPromoter == null) { 227 return; 228 } 229 mPromoter.pickPromoted(mShortcuts, mCorpusResults, mMaxPromoted, mPromoted, 230 mPromotedCorpora); 231 } 232 233 /** 234 * Gets a given corpus result. 235 * Must be called on the UI thread, or before this object is seen by the UI thread. 236 * 237 * @param corpus corpus 238 * @return The source result for the given source. {@code null} if the source has not 239 * yet returned. 240 */ 241 public CorpusResult getCorpusResult(Corpus corpus) { 242 if (mClosed) { 243 throw new IllegalStateException("getCorpusResult(" + corpus + ") when closed."); 244 } 245 return mResultsByCorpus.get(corpus); 246 } 247 248 /** 249 * Gets the number of source results. 250 * Must be called on the UI thread, or before this object is seen by the UI thread. 251 */ 252 public int getSourceCount() { 253 if (mClosed) { 254 throw new IllegalStateException("Called getSourceCount() when closed."); 255 } 256 return mCorpusResults == null ? 0 : mCorpusResults.size(); 257 } 258 259 private class MyShortcutsObserver extends DataSetObserver { 260 @Override 261 public void onChanged() { 262 mPromoted = null; 263 notifyDataSetChanged(); 264 } 265 } 266 267} 268