SuggestionsAdapter.java revision 49f8d487114bdbe5e0cdd6da923e1e92e5ce1bbf
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.ui;
18
19import com.android.quicksearchbox.Corpora;
20import com.android.quicksearchbox.Corpus;
21import com.android.quicksearchbox.Promoter;
22import com.android.quicksearchbox.Source;
23import com.android.quicksearchbox.SuggestionCursor;
24import com.android.quicksearchbox.SuggestionPosition;
25import com.android.quicksearchbox.Suggestions;
26
27import android.database.DataSetObserver;
28import android.util.Log;
29import android.view.View;
30import android.view.View.OnFocusChangeListener;
31import android.view.ViewGroup;
32import android.widget.BaseAdapter;
33
34import java.util.HashMap;
35
36/**
37 * Uses a {@link Suggestions} object to back a {@link SuggestionsView}.
38 */
39public class SuggestionsAdapter extends BaseAdapter {
40
41    private static final boolean DBG = false;
42    private static final String TAG = "QSB.SuggestionsAdapter";
43
44    private final Corpora mCorpora;
45
46    private DataSetObserver mDataSetObserver;
47
48    private Promoter mPromoter;
49
50    private int mMaxPromoted;
51
52    private SuggestionCursor mCursor;
53    private final HashMap<String, Integer> mViewTypeMap;
54    private final SuggestionViewFactory mFallback;
55
56    private Suggestions mSuggestions;
57
58    private SuggestionClickListener mSuggestionClickListener;
59    private OnFocusChangeListener mOnFocusChangeListener;
60
61    private SuggestionsAdapterChangeListener mListener;
62
63    private final CorporaObserver mCorporaObserver = new CorporaObserver();
64
65    private boolean mClosed = false;
66
67    private boolean mIcon1Enabled = true;
68
69    public SuggestionsAdapter(SuggestionViewFactory fallbackFactory, Corpora corpora) {
70        mCorpora = corpora;
71        mFallback = fallbackFactory;
72        mViewTypeMap = new HashMap<String, Integer>();
73        mCorpora.registerDataSetObserver(mCorporaObserver);
74        buildViewTypeMap();
75    }
76
77    public void setSuggestionAdapterChangeListener(SuggestionsAdapterChangeListener l) {
78        mListener = l;
79    }
80
81    private boolean addViewTypes(SuggestionViewFactory f) {
82        boolean changed = false;
83        for (String viewType : f.getSuggestionViewTypes()) {
84            if (!mViewTypeMap.containsKey(viewType)) {
85                mViewTypeMap.put(viewType, mViewTypeMap.size());
86                changed = true;
87            }
88        }
89        return changed;
90    }
91
92    private boolean buildViewTypeMap() {
93        boolean changed = addViewTypes(mFallback);
94        for (Corpus c : mCorpora.getEnabledCorpora()) {
95            for (Source s : c.getSources()) {
96                SuggestionViewFactory f = s.getSuggestionViewFactory();
97                changed |= addViewTypes(f);
98            }
99        }
100        return changed;
101    }
102
103    public void setMaxPromoted(int maxPromoted) {
104        if (DBG) Log.d(TAG, "setMaxPromoted " + maxPromoted);
105        mMaxPromoted = maxPromoted;
106        onSuggestionsChanged();
107    }
108
109    public void setIcon1Enabled(boolean enabled) {
110        mIcon1Enabled = enabled;
111    }
112
113    public boolean isClosed() {
114        return mClosed;
115    }
116
117    public void close() {
118        setSuggestions(null);
119        mCorpora.unregisterDataSetObserver(mCorporaObserver);
120        mClosed = true;
121    }
122
123    public void setPromoter(Promoter promoter) {
124        mPromoter = promoter;
125        onSuggestionsChanged();
126    }
127
128    public void setSuggestionClickListener(SuggestionClickListener listener) {
129        mSuggestionClickListener = listener;
130    }
131
132    public void setOnFocusChangeListener(OnFocusChangeListener l) {
133        mOnFocusChangeListener = l;
134    }
135
136    public void setSuggestions(Suggestions suggestions) {
137        if (mSuggestions == suggestions) {
138            return;
139        }
140        if (mClosed) {
141            if (suggestions != null) {
142                suggestions.release();
143            }
144            return;
145        }
146        if (mDataSetObserver == null) {
147            mDataSetObserver = new MySuggestionsObserver();
148        }
149        // TODO: delay the change if there are no suggestions for the currently visible tab.
150        if (mSuggestions != null) {
151            mSuggestions.unregisterDataSetObserver(mDataSetObserver);
152            mSuggestions.release();
153        }
154        mSuggestions = suggestions;
155        if (mSuggestions != null) {
156            mSuggestions.registerDataSetObserver(mDataSetObserver);
157        }
158        onSuggestionsChanged();
159    }
160
161    public Suggestions getSuggestions() {
162        return mSuggestions;
163    }
164
165    public int getCount() {
166        return mCursor == null ? 0 : mCursor.getCount();
167    }
168
169    public SuggestionPosition getItem(int position) {
170        if (mCursor == null) return null;
171        return new SuggestionPosition(mCursor, position);
172    }
173
174    public long getItemId(int position) {
175        return position;
176    }
177
178    @Override
179    public int getViewTypeCount() {
180        return mViewTypeMap.size();
181    }
182
183    private String currentSuggestionViewType() {
184        String viewType = mCursor.getSuggestionSource().getSuggestionViewFactory()
185                .getViewType(mCursor);
186        if (!mViewTypeMap.containsKey(viewType)) {
187            throw new IllegalStateException("Unknown viewType " + viewType);
188        }
189        return viewType;
190    }
191
192    @Override
193    public int getItemViewType(int position) {
194        if (mCursor == null) {
195            return 0;
196        }
197        mCursor.moveTo(position);
198        return mViewTypeMap.get(currentSuggestionViewType());
199    }
200
201    // Implements Adapter#getView()
202    public View getView(int position, View convertView, ViewGroup parent) {
203        if (mCursor == null) {
204            throw new IllegalStateException("getView() called with null cursor");
205        }
206        mCursor.moveTo(position);
207        SuggestionViewFactory factory = mCursor.getSuggestionSource().getSuggestionViewFactory();
208        View v = factory.getView(mCursor, mCursor.getUserQuery(), convertView, parent);
209        if (v == null) {
210            v = mFallback.getView(mCursor, mCursor.getUserQuery(), convertView, parent);
211        }
212        if (v instanceof SuggestionView) {
213            ((SuggestionView) v).setIcon1Enabled(mIcon1Enabled);
214            ((SuggestionView) v).bindAdapter(this, position);
215        } else {
216            SuggestionViewClickListener l = new SuggestionViewClickListener(position);
217            v.setOnClickListener(l);
218        }
219
220        if (mOnFocusChangeListener != null) {
221            v.setOnFocusChangeListener(mOnFocusChangeListener);
222        }
223        return v;
224    }
225
226    protected void onSuggestionsChanged() {
227        if (DBG) Log.d(TAG, "onSuggestionsChanged(" + mSuggestions + ")");
228        SuggestionCursor cursor = getPromoted(mSuggestions);
229        changeCursor(cursor);
230    }
231
232    /**
233     * Gets the cursor containing the currently shown suggestions. The caller should not hold
234     * on to or modify the returned cursor.
235     */
236    public SuggestionCursor getCurrentSuggestions() {
237        return mCursor;
238    }
239
240    /**
241     * Gets the cursor for the given source.
242     */
243    protected SuggestionCursor getPromoted(Suggestions suggestions) {
244        if (suggestions == null) return null;
245        return suggestions.getPromoted(mPromoter, mMaxPromoted);
246    }
247
248    /**
249     * Replace the cursor.
250     *
251     * This does not close the old cursor. Instead, all the cursors are closed in
252     * {@link #setSuggestions(Suggestions)}.
253     */
254    private void changeCursor(SuggestionCursor newCursor) {
255        if (DBG) {
256            Log.d(TAG, "changeCursor(" + newCursor + ") count=" +
257                    (newCursor == null ? 0 : newCursor.getCount()));
258        }
259        if (newCursor == mCursor) {
260            if (newCursor != null) {
261                // Shortcuts may have changed without the cursor changing.
262                notifyDataSetChanged();
263            }
264            return;
265        }
266        mCursor = newCursor;
267        if (mCursor != null) {
268            notifyDataSetChanged();
269        } else {
270            notifyDataSetInvalidated();
271        }
272    }
273
274    public void onSuggestionClicked(int position) {
275        if (mSuggestionClickListener != null) {
276            mSuggestionClickListener.onSuggestionClicked(this, position);
277        }
278    }
279
280    public void onSuggestionQuickContactClicked(int position) {
281        if (mSuggestionClickListener != null) {
282            mSuggestionClickListener.onSuggestionQuickContactClicked(this, position);
283        }
284    }
285
286    public void onSuggestionRemoveFromHistoryClicked(int position) {
287        if (mSuggestionClickListener != null) {
288            mSuggestionClickListener.onSuggestionRemoveFromHistoryClicked(this, position);
289        }
290    }
291
292    public void onSuggestionQueryRefineClicked(int position) {
293        if (mSuggestionClickListener != null) {
294            mSuggestionClickListener.onSuggestionQueryRefineClicked(this, position);
295        }
296    }
297
298    private class MySuggestionsObserver extends DataSetObserver {
299        @Override
300        public void onChanged() {
301            onSuggestionsChanged();
302        }
303    }
304
305    private class CorporaObserver extends DataSetObserver {
306        @Override
307        public void onChanged() {
308            if (buildViewTypeMap()) {
309                if (mListener != null) {
310                    mListener.onSuggestionAdapterChanged();
311                }
312            }
313        }
314    }
315
316    private class SuggestionViewClickListener implements View.OnClickListener {
317        private final int mPosition;
318        public SuggestionViewClickListener(int position) {
319            mPosition = position;
320        }
321        public void onClick(View v) {
322            onSuggestionClicked(mPosition);
323        }
324    }
325
326    /**
327     * Callback interface used to notify the view when the adapter has changed (i.e. the number and
328     * type of views returned has changed).
329     */
330    public interface SuggestionsAdapterChangeListener {
331        void onSuggestionAdapterChanged();
332    }
333
334}
335