1/*
2 * Copyright (C) 2010 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.Arrays;
27import java.util.HashMap;
28import java.util.HashSet;
29import java.util.List;
30import java.util.Set;
31
32/**
33 * Collects all corpus results for a single query.
34 */
35public class Suggestions {
36    private static final boolean DBG = false;
37    private static final String TAG = "QSB.Suggestions";
38
39    /** True if {@link Suggestions#close} has been called. */
40    private boolean mClosed = false;
41    protected final String mQuery;
42
43    private ShortcutCursor mShortcuts;
44
45    private final MyShortcutsObserver mShortcutsObserver = new MyShortcutsObserver();
46
47    /**
48     * The observers that want notifications of changes to the published suggestions.
49     * This object may be accessed on any thread.
50     */
51    private final DataSetObservable mDataSetObservable = new DataSetObservable();
52
53    /** The sources that are expected to report. */
54    private final List<Corpus> mExpectedCorpora;
55    private final HashMap<String, Integer> mCorpusPositions;
56
57    /**
58     * All {@link SuggestionCursor} objects that have been published so far,
59     * in the same order as {@link #mExpectedCorpora}. There may be {@code null} items
60     * in the array, if not all corpora have published yet.
61     * This object may only be accessed on the UI thread.
62     * */
63    private final CorpusResult[] mCorpusResults;
64
65    private CorpusResult mWebResult;
66
67    private int mRefCount = 0;
68
69    private boolean mDone = false;
70
71    public Suggestions(String query, List<Corpus> expectedCorpora) {
72        mQuery = query;
73        mExpectedCorpora = expectedCorpora;
74        mCorpusResults = new CorpusResult[mExpectedCorpora.size()];
75        // create a map of corpus name -> position in mExpectedCorpora for sorting later
76        // (we want to keep the ordering of corpora in mCorpusResults).
77        mCorpusPositions = new HashMap<String, Integer>();
78        for (int i = 0; i < mExpectedCorpora.size(); ++i) {
79            mCorpusPositions.put(mExpectedCorpora.get(i).getName(), i);
80        }
81        if (DBG) {
82            Log.d(TAG, "new Suggestions [" + hashCode() + "] query \"" + query
83                    + "\" expected corpora: " + mExpectedCorpora);
84        }
85    }
86
87    public void acquire() {
88        mRefCount++;
89    }
90
91    public void release() {
92        mRefCount--;
93        if (mRefCount <= 0) {
94            close();
95        }
96    }
97
98    public List<Corpus> getExpectedCorpora() {
99        return mExpectedCorpora;
100    }
101
102    /**
103     * Gets the number of corpora that are expected to report.
104     */
105    @VisibleForTesting
106    public int getExpectedResultCount() {
107        return mExpectedCorpora.size();
108    }
109
110    public boolean expectsCorpus(Corpus corpus) {
111        for (Corpus expectedCorpus : mExpectedCorpora) {
112            if (expectedCorpus.equals(corpus)) return true;
113        }
114        return false;
115    }
116
117    /**
118     * Gets the set of corpora that have reported results to this suggestions set.
119     *
120     * @return A collection of corpora.
121     */
122    public Set<Corpus> getIncludedCorpora() {
123        HashSet<Corpus> corpora = new HashSet<Corpus>();
124        for (CorpusResult result : mCorpusResults) {
125            if (result != null) {
126                corpora.add(result.getCorpus());
127            }
128        }
129        return corpora;
130    }
131
132    /**
133     * Sets the shortcut suggestions.
134     * Must be called on the UI thread, or before this object is seen by the UI thread.
135     *
136     * @param shortcuts The shortcuts.
137     */
138    public void setShortcuts(ShortcutCursor shortcuts) {
139        if (DBG) Log.d(TAG, "setShortcuts(" + shortcuts + ")");
140        if (mShortcuts != null) {
141            throw new IllegalStateException("Got duplicate shortcuts: old: " + mShortcuts
142                    + ", new: " + shortcuts);
143        }
144        if (shortcuts == null) return;
145        if (isClosed()) {
146            shortcuts.close();
147            return;
148        }
149        if (!mQuery.equals(shortcuts.getUserQuery())) {
150            throw new IllegalArgumentException("Got shortcuts for wrong query: "
151                    + mQuery + " != " + shortcuts.getUserQuery());
152        }
153        mShortcuts = shortcuts;
154        if (shortcuts != null) {
155            mShortcuts.registerDataSetObserver(mShortcutsObserver);
156        }
157        notifyDataSetChanged();
158    }
159
160    /**
161     * Marks the suggestions set as complete, regardless of whether all corpora have
162     * returned.
163     */
164    public void done() {
165        mDone = true;
166    }
167
168    /**
169     * Checks whether all sources have reported.
170     * Must be called on the UI thread, or before this object is seen by the UI thread.
171     */
172    public boolean isDone() {
173        // TODO: Handle early completion because we have all the results we want.
174        return mDone || countCorpusResults() >= mExpectedCorpora.size();
175    }
176
177    private int countCorpusResults() {
178        int count = 0;
179        for (int i = 0; i < mCorpusResults.length; ++i) {
180            if (mCorpusResults[i] != null) {
181                count++;
182            }
183        }
184        return count;
185    }
186
187    /**
188     * Adds a list of corpus results. Must be called on the UI thread, or before this
189     * object is seen by the UI thread.
190     */
191    public void addCorpusResults(List<CorpusResult> corpusResults) {
192        if (isClosed()) {
193            for (CorpusResult corpusResult : corpusResults) {
194                corpusResult.close();
195            }
196            return;
197        }
198
199        for (CorpusResult corpusResult : corpusResults) {
200            if (DBG) {
201                Log.d(TAG, "addCorpusResult["+ hashCode() + "] corpus:" +
202                        corpusResult.getCorpus().getName() + " results:" + corpusResult.getCount());
203            }
204            if (!mQuery.equals(corpusResult.getUserQuery())) {
205              throw new IllegalArgumentException("Got result for wrong query: "
206                    + mQuery + " != " + corpusResult.getUserQuery());
207            }
208            Integer pos = mCorpusPositions.get(corpusResult.getCorpus().getName());
209            if (pos == null) {
210                Log.w(TAG, "Got unexpected CorpusResult from corpus " +
211                        corpusResult.getCorpus().getName());
212                corpusResult.close();
213            } else {
214                mCorpusResults[pos] = corpusResult;
215                if (corpusResult.getCorpus().isWebCorpus()) {
216                    mWebResult = corpusResult;
217                }
218            }
219        }
220        notifyDataSetChanged();
221    }
222
223    /**
224     * Registers an observer that will be notified when the reported results or
225     * the done status changes.
226     */
227    public void registerDataSetObserver(DataSetObserver observer) {
228        if (mClosed) {
229            throw new IllegalStateException("registerDataSetObserver() when closed");
230        }
231        mDataSetObservable.registerObserver(observer);
232    }
233
234
235    /**
236     * Unregisters an observer.
237     */
238    public void unregisterDataSetObserver(DataSetObserver observer) {
239        mDataSetObservable.unregisterObserver(observer);
240    }
241
242    /**
243     * Calls {@link DataSetObserver#onChanged()} on all observers.
244     */
245    protected void notifyDataSetChanged() {
246        if (DBG) Log.d(TAG, "notifyDataSetChanged()");
247        mDataSetObservable.notifyChanged();
248    }
249
250    /**
251     * Closes all the source results and unregisters all observers.
252     */
253    private void close() {
254        if (DBG) Log.d(TAG, "close() [" + hashCode() + "]");
255        if (mClosed) {
256            throw new IllegalStateException("Double close()");
257        }
258        mClosed = true;
259        mDataSetObservable.unregisterAll();
260        if (mShortcuts != null) {
261            mShortcuts.close();
262            mShortcuts = null;
263        }
264
265        for (CorpusResult result : mCorpusResults) {
266            if (result != null) {
267                result.close();
268            }
269        }
270        Arrays.fill(mCorpusResults, null);
271    }
272
273    public boolean isClosed() {
274        return mClosed;
275    }
276
277    public ShortcutCursor getShortcuts() {
278        return mShortcuts;
279    }
280
281    private void refreshShortcuts(SuggestionCursor promoted) {
282        if (DBG) Log.d(TAG, "refreshShortcuts(" + promoted + ")");
283        for (int i = 0; i < promoted.getCount(); ++i) {
284            promoted.moveTo(i);
285            if (promoted.isSuggestionShortcut()) {
286                getShortcuts().refresh(promoted);
287            }
288        }
289    }
290
291    @Override
292    protected void finalize() {
293        if (!mClosed) {
294            Log.e(TAG, "LEAK! Finalized without being closed: Suggestions[" + getQuery() + "]");
295        }
296    }
297
298    public String getQuery() {
299        return mQuery;
300    }
301
302    public SuggestionCursor getPromoted(Promoter promoter, int maxPromoted) {
303        SuggestionCursor promoted = buildPromoted(promoter, maxPromoted);
304        refreshShortcuts(promoted);
305        return promoted;
306    }
307
308    protected SuggestionCursor buildPromoted(Promoter promoter, int maxPromoted) {
309        ListSuggestionCursor promoted = new ListSuggestionCursorNoDuplicates(mQuery);
310        if (promoter == null) {
311            return promoted;
312        }
313        promoter.pickPromoted(this, maxPromoted, promoted);
314        if (DBG) {
315            Log.d(TAG, "pickPromoted(" + getShortcuts() + "," + mCorpusResults + ","
316                    + maxPromoted + ") = " + promoted);
317        }
318        return promoted;
319    }
320
321    /**
322     * Gets the list of corpus results reported so far. Do not modify or hang on to
323     * the returned iterator.
324     */
325    public Iterable<CorpusResult> getCorpusResults() {
326        ArrayList<CorpusResult> results = new ArrayList<CorpusResult>(mCorpusResults.length);
327        for (int i = 0; i < mCorpusResults.length; ++i) {
328            if (mCorpusResults[i] != null) {
329                results.add(mCorpusResults[i]);
330            }
331        }
332        return results;
333    }
334
335    public CorpusResult getCorpusResult(Corpus corpus) {
336        for (CorpusResult result : mCorpusResults) {
337            if (result != null && corpus.equals(result.getCorpus())) {
338                return result;
339            }
340        }
341        return null;
342    }
343
344    public CorpusResult getWebResult() {
345        return mWebResult;
346    }
347
348    /**
349     * Gets the number of source results.
350     * Must be called on the UI thread, or before this object is seen by the UI thread.
351     */
352    public int getResultCount() {
353        if (isClosed()) {
354            throw new IllegalStateException("Called getSourceCount() when closed.");
355        }
356        return countCorpusResults();
357    }
358
359    @Override
360    public String toString() {
361        return "Suggestions@" + hashCode() + "{expectedCorpora=" + mExpectedCorpora
362                + ",countCorpusResults()=" + countCorpusResults() + "}";
363    }
364
365    private class MyShortcutsObserver extends DataSetObserver {
366        @Override
367        public void onChanged() {
368            notifyDataSetChanged();
369        }
370    }
371
372}
373