Suggestions.java revision 3e44ff1f2a204db3f479698cf0b3eab3d451dec2
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 java.util.ArrayList; 20 21import android.database.DataSetObservable; 22import android.database.DataSetObserver; 23import android.os.Handler; 24import android.util.Log; 25 26/** 27 * Contains all non-empty {@link SuggestionCursor} objects that have been reported so far. 28 * 29 */ 30public class Suggestions { 31 32 private static final boolean DBG = true; 33 private static final String TAG = "QSB.Suggestions"; 34 35 private final Handler mUiThread; 36 37 private final int mMaxPromoted; 38 39 private final long mPublishDelay; 40 41 private final String mQuery; 42 43 /** The number of sources that are expected to report. */ 44 private final int mExpectedSourceCount; 45 46 /** 47 * The observers that want notifications of changes to the published suggestions. 48 * This object may be accessed on any thread. 49 */ 50 private final DataSetObservable mDataSetObservable = new DataSetObservable(); 51 52 /** 53 * All non-empty {@link SuggestionCursor} objects that have been published so far. 54 * This object may only be accessed on the UI thread. 55 * */ 56 private final ArrayList<SuggestionCursor> mSourceResults; 57 58 /** 59 * All {@link SuggestionCursor} objects that have been reported but not yet published. 60 * This object may be accessed on any thread. 61 * */ 62 private final ArrayList<SuggestionCursor> mUnpublishedSourceResults; 63 64 /** 65 * The number of sources that have reported so far. This may be greater 66 * that the size of {@link #mSourceResults}, since this count also includes 67 * sources that failed or reported zero results. 68 */ 69 private int mPublishedSourceCount = 0; 70 71 private SuggestionCursor mShortcuts; 72 73 /** True if {@link Suggestions#close} has been called. */ 74 private boolean mClosed = false; 75 76 private final Runnable mPublishRunnable = new Runnable() { 77 public void run() { 78 publishSourceResults(); 79 } 80 }; 81 82 private final Promoter mPromoter; 83 84 private ListSuggestionCursor mPromoted; 85 86 /** 87 * Creates a new empty Suggestions. 88 * 89 * @param expectedSourceCount The number of sources that are expected to report. 90 */ 91 public Suggestions(Handler uiThread, Promoter promoter, int maxPromoted, long publishDelay, 92 String query, int expectedSourceCount) { 93 mUiThread = uiThread; 94 mPromoter = promoter; 95 mMaxPromoted = maxPromoted; 96 mPublishDelay = publishDelay; 97 mQuery = query; 98 mExpectedSourceCount = expectedSourceCount; 99 mSourceResults = new ArrayList<SuggestionCursor>(mExpectedSourceCount); 100 mUnpublishedSourceResults = new ArrayList<SuggestionCursor>(); 101 mPromoted = null; // will be set by updatePromoted() 102 } 103 104 public String getQuery() { 105 return mQuery; 106 } 107 108 /** 109 * Gets the number of sources that are expected to report. 110 */ 111 public int getExpectedSourceCount() { 112 return mExpectedSourceCount; 113 } 114 115 /** 116 * Gets the number of sources whose results have been published. This may be higher than 117 * {@link #getSourceCount()}, since empty results are not included in 118 * {@link #getSourceCount()}. 119 */ 120 public int getPublishedSourceCount() { 121 return mPublishedSourceCount; 122 } 123 124 /** 125 * Gets the current progress of the suggestions, in the inclusive range 0-100. 126 */ 127 public int getProgress() { 128 if (mExpectedSourceCount == 0) return 100; 129 return 100 * mPublishedSourceCount / mExpectedSourceCount; 130 } 131 132 /** 133 * Registers an observer that will be notified when the reported results or 134 * the done status changes. 135 */ 136 public void registerDataSetObserver(DataSetObserver observer) { 137 if (mClosed) { 138 throw new IllegalStateException("registerDataSetObserver() when closed"); 139 } 140 mDataSetObservable.registerObserver(observer); 141 } 142 143 /** 144 * Unregisters an observer. 145 */ 146 public void unregisterDataSetObserver(DataSetObserver observer) { 147 mDataSetObservable.unregisterObserver(observer); 148 } 149 150 public SuggestionCursor getPromoted() { 151 return mPromoted; 152 } 153 154 /** 155 * Calls {@link DataSetObserver#onChanged()} on all observers. 156 */ 157 private void notifyDataSetChanged() { 158 if (DBG) Log.d(TAG, "notifyDataSetChanged()"); 159 mDataSetObservable.notifyChanged(); 160 } 161 162 /** 163 * Closes all the source results and unregisters all observers. 164 */ 165 public void close() { 166 if (DBG) Log.d(TAG, "close()"); 167 if (mClosed) { 168 Log.w(TAG, "Double close()"); 169 return; 170 } 171 mDataSetObservable.unregisterAll(); 172 mClosed = true; 173 cancelPublishCalls(); 174 if (mShortcuts != null) { 175 mShortcuts.close(); 176 } 177 for (SuggestionCursor result : mSourceResults) { 178 result.close(); 179 } 180 mSourceResults.clear(); 181 for (SuggestionCursor result : mUnpublishedSourceResults) { 182 result.close(); 183 } 184 mUnpublishedSourceResults.clear(); 185 } 186 187 @Override 188 protected void finalize() { 189 if (!mClosed) { 190 Log.e(TAG, "Leak! Finalized without being closed."); 191 close(); 192 } 193 } 194 195 /** 196 * Checks whether all sources have reported. 197 * Must be called on the UI thread, or before this object is seen by the UI thread. 198 */ 199 public boolean isDone() { 200 return mPublishedSourceCount >= mExpectedSourceCount; 201 } 202 203 public SuggestionCursor getShortcuts() { 204 return mShortcuts; 205 } 206 207 public void setShortcuts(SuggestionCursor shortcuts) { 208 if (DBG) Log.d(TAG, "setShortcuts(" + shortcuts + ")"); 209 mShortcuts = shortcuts; 210 updatePromoted(); 211 } 212 213 /** 214 * Adds a source result, possibly with some delay. 215 * Must be called on the UI thread, or before this object is seen by the UI thread. 216 * 217 * @param sourceResult The source result. 218 */ 219 public void addSourceResult(SuggestionCursor sourceResult) { 220 if (mClosed) { 221 sourceResult.close(); 222 return; 223 } 224 if (!mQuery.equals(sourceResult.getUserQuery())) { 225 throw new IllegalArgumentException("Got result for wrong query: " 226 + mQuery + " != " + sourceResult.getUserQuery()); 227 } 228 mUnpublishedSourceResults.add(sourceResult); 229 int newReportedCount = mPublishedSourceCount + mUnpublishedSourceResults.size(); 230 if (newReportedCount >= mExpectedSourceCount) { 231 // This is the last result, publish immediately. 232 if (DBG) Log.d(TAG, "Publishing immediately: " + sourceResult); 233 // Since we are already on the UI thread, we could call mPublishRunnable 234 // directly. Doing it through the handler for uniformity. 235 mUiThread.post(mPublishRunnable); 236 } else if (sourceResult.getCount() > 0) { 237 // We are waiting for more results, but this result was non-empty, schedule 238 // a delayed publish. 239 if (DBG) Log.d(TAG, "Publish delayed by " + mPublishDelay + "ms: " + sourceResult); 240 mUiThread.postDelayed(mPublishRunnable, mPublishDelay); 241 } else { 242 // We are waiting for more results, and this result was empty, don't publish now. 243 if (DBG) Log.d(TAG, "Not publishing empty results: " + sourceResult); 244 } 245 } 246 247 /** 248 * Cancels all pending calls to {@link #publishSourceResults()}. 249 */ 250 protected void cancelPublishCalls() { 251 mUiThread.removeCallbacks(mPublishRunnable); 252 } 253 254 /** 255 * Publishes the reported but unpublished source results, and notifies the observers if needed. 256 * Must be called on the UI thread, or before this object is seen by the UI thread. 257 */ 258 protected void publishSourceResults() { 259 if (DBG) Log.d(TAG, "publishSourceResults()"); 260 // Remove any duplicate publish calls 261 cancelPublishCalls(); 262 boolean changed = false; 263 int unpublishedCount = mUnpublishedSourceResults.size(); 264 for (int i = 0; i < unpublishedCount; i++) { 265 boolean thisChanged = publishSourceResult(mUnpublishedSourceResults.get(i)); 266 changed |= thisChanged; 267 } 268 mUnpublishedSourceResults.clear(); 269 if (changed) { 270 updatePromoted(); 271 notifyDataSetChanged(); 272 } 273 } 274 275 private void updatePromoted() { 276 mPromoted = new ListSuggestionCursorNoDuplicates(mQuery); 277 if (mPromoter == null) { 278 return; 279 } 280 mPromoter.pickPromoted(mShortcuts, mSourceResults, mMaxPromoted, mPromoted); 281 } 282 283 /** 284 * Publishes the result from a source. Does not notify the observers. 285 * Must be called on the UI thread, or before this object is seen by the UI thread. 286 * If called after the UI has seen this object, {@link #notifyDataSetChanged()} 287 * must be called if this method returns {@code true}. 288 * 289 * @param sourceResult 290 * @return {@code true} if any suggestions were added or the state changed to done. 291 */ 292 private boolean publishSourceResult(SuggestionCursor sourceResult) { 293 if (mClosed) { 294 throw new IllegalStateException("publishSourceResult(" + sourceResult 295 + ") after close()"); 296 } 297 if (DBG) Log.d(TAG, "publishSourceResult(" + sourceResult + ")"); 298 mPublishedSourceCount++; 299 final int count = sourceResult.getCount(); 300 boolean added = false; 301 if (count > 0) { 302 added = true; 303 mSourceResults.add(sourceResult); 304 } else { 305 sourceResult.close(); 306 } 307 return (added || isDone()); 308 } 309 310 /** 311 * Gets a given source result. 312 * Must be called on the UI thread, or before this object is seen by the UI thread. 313 * 314 * @param sourcePos 315 * @return The source result at the given position. 316 * @throws IndexOutOfBoundsException If {@code sourcePos < 0} or 317 * {@code sourcePos >= getSourceCount()}. 318 */ 319 public SuggestionCursor getSourceResult(int sourcePos) { 320 if (mClosed) { 321 throw new IllegalStateException("Called getSourceResult(" + sourcePos 322 + ") when closed."); 323 } 324 return mSourceResults.get(sourcePos); 325 } 326 327 /** 328 * Gets the number of source results. 329 * Must be called on the UI thread, or before this object is seen by the UI thread. 330 */ 331 public int getSourceCount() { 332 if (mClosed) { 333 throw new IllegalStateException("Called getSourceCount() when closed."); 334 } 335 return mSourceResults == null ? 0 : mSourceResults.size(); 336 } 337 338} 339