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