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