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 */
16package com.android.quicksearchbox.ui;
17
18import com.android.quicksearchbox.Promoter;
19import com.android.quicksearchbox.Suggestion;
20import com.android.quicksearchbox.SuggestionCursor;
21import com.android.quicksearchbox.SuggestionPosition;
22import com.android.quicksearchbox.Suggestions;
23
24import android.database.DataSetObserver;
25import android.util.Log;
26import android.view.View;
27import android.view.View.OnFocusChangeListener;
28import android.view.ViewGroup;
29
30import java.util.HashMap;
31
32/**
33 * Base class for suggestions adapters. The templated class A is the list adapter class.
34 */
35public abstract class SuggestionsAdapterBase<A> implements SuggestionsAdapter<A> {
36
37    private static final boolean DBG = false;
38    private static final String TAG = "QSB.SuggestionsAdapter";
39
40    private DataSetObserver mDataSetObserver;
41
42    private Promoter mPromoter;
43
44    private int mMaxPromoted;
45
46    private SuggestionCursor mPromotedSuggestions;
47    private final HashMap<String, Integer> mViewTypeMap;
48    private final SuggestionViewFactory mViewFactory;
49
50    private Suggestions mSuggestions;
51
52    private SuggestionClickListener mSuggestionClickListener;
53    private OnFocusChangeListener mOnFocusChangeListener;
54
55    private boolean mClosed = false;
56
57    protected SuggestionsAdapterBase(SuggestionViewFactory viewFactory) {
58        mViewFactory = viewFactory;
59        mViewTypeMap = new HashMap<String, Integer>();
60        for (String viewType : mViewFactory.getSuggestionViewTypes()) {
61            if (!mViewTypeMap.containsKey(viewType)) {
62                mViewTypeMap.put(viewType, mViewTypeMap.size());
63            }
64        }
65    }
66
67    public abstract boolean isEmpty();
68
69    /**
70     * Indicates if this adapter will publish suggestions other than those in the promoted list.
71     */
72    public abstract boolean willPublishNonPromotedSuggestions();
73
74    public void setMaxPromoted(int maxPromoted) {
75        if (DBG) Log.d(TAG, "setMaxPromoted " + maxPromoted);
76        mMaxPromoted = maxPromoted;
77        onSuggestionsChanged();
78    }
79
80    public boolean isClosed() {
81        return mClosed;
82    }
83
84    public void close() {
85        setSuggestions(null);
86        mClosed = true;
87    }
88
89    public void setPromoter(Promoter promoter) {
90        mPromoter = promoter;
91        onSuggestionsChanged();
92    }
93
94    public void setSuggestionClickListener(SuggestionClickListener listener) {
95        mSuggestionClickListener = listener;
96    }
97
98    public void setOnFocusChangeListener(OnFocusChangeListener l) {
99        mOnFocusChangeListener = l;
100    }
101
102    public void setSuggestions(Suggestions suggestions) {
103        if (mSuggestions == suggestions) {
104            return;
105        }
106        if (mClosed) {
107            if (suggestions != null) {
108                suggestions.release();
109            }
110            return;
111        }
112        if (mDataSetObserver == null) {
113            mDataSetObserver = new MySuggestionsObserver();
114        }
115        // TODO: delay the change if there are no suggestions for the currently visible tab.
116        if (mSuggestions != null) {
117            mSuggestions.unregisterDataSetObserver(mDataSetObserver);
118            mSuggestions.release();
119        }
120        mSuggestions = suggestions;
121        if (mSuggestions != null) {
122            mSuggestions.registerDataSetObserver(mDataSetObserver);
123        }
124        onSuggestionsChanged();
125    }
126
127    public Suggestions getSuggestions() {
128        return mSuggestions;
129    }
130
131    public abstract SuggestionPosition getSuggestion(long suggestionId);
132
133    protected int getPromotedCount() {
134        return mPromotedSuggestions == null ? 0 : mPromotedSuggestions.getCount();
135    }
136
137    protected SuggestionPosition getPromotedSuggestion(int position) {
138        if (mPromotedSuggestions == null) return null;
139        return new SuggestionPosition(mPromotedSuggestions, position);
140    }
141
142    protected int getViewTypeCount() {
143        return mViewTypeMap.size();
144    }
145
146    private String suggestionViewType(Suggestion suggestion) {
147        String viewType = mViewFactory.getViewType(suggestion);
148        if (!mViewTypeMap.containsKey(viewType)) {
149            throw new IllegalStateException("Unknown viewType " + viewType);
150        }
151        return viewType;
152    }
153
154    protected int getSuggestionViewType(SuggestionCursor cursor, int position) {
155        if (cursor == null) {
156            return 0;
157        }
158        cursor.moveTo(position);
159        return mViewTypeMap.get(suggestionViewType(cursor));
160    }
161
162    protected int getSuggestionViewTypeCount() {
163        return mViewTypeMap.size();
164    }
165
166    protected View getView(SuggestionCursor suggestions, int position, long suggestionId,
167            View convertView, ViewGroup parent) {
168        suggestions.moveTo(position);
169        View v = mViewFactory.getView(suggestions, suggestions.getUserQuery(), convertView, parent);
170        if (v instanceof SuggestionView) {
171            ((SuggestionView) v).bindAdapter(this, suggestionId);
172        } else {
173            SuggestionViewClickListener l = new SuggestionViewClickListener(suggestionId);
174            v.setOnClickListener(l);
175        }
176
177        if (mOnFocusChangeListener != null) {
178            v.setOnFocusChangeListener(mOnFocusChangeListener);
179        }
180        return v;
181    }
182
183    protected void onSuggestionsChanged() {
184        if (DBG) Log.d(TAG, "onSuggestionsChanged(" + mSuggestions + ")");
185        SuggestionCursor cursor = getPromoted(mSuggestions);
186        changePromoted(cursor);
187    }
188
189    /**
190     * Gets the cursor containing the currently shown suggestions. The caller should not hold
191     * on to or modify the returned cursor.
192     */
193    public SuggestionCursor getCurrentPromotedSuggestions() {
194        return mPromotedSuggestions;
195    }
196
197    /**
198     * Gets the cursor for the given source.
199     */
200    protected SuggestionCursor getPromoted(Suggestions suggestions) {
201        if (suggestions == null) return null;
202        return suggestions.getPromoted(mPromoter, mMaxPromoted);
203    }
204
205    /**
206     * Replace the cursor.
207     *
208     * This does not close the old cursor. Instead, all the cursors are closed in
209     * {@link #setSuggestions(Suggestions)}.
210     */
211    private void changePromoted(SuggestionCursor newCursor) {
212        if (DBG) {
213            Log.d(TAG, "changeCursor(" + newCursor + ") count=" +
214                    (newCursor == null ? 0 : newCursor.getCount()));
215        }
216        if (newCursor == mPromotedSuggestions) {
217            if (newCursor != null) {
218                // Shortcuts may have changed without the cursor changing.
219                notifyDataSetChanged();
220            }
221            return;
222        }
223        mPromotedSuggestions = newCursor;
224        if (mPromotedSuggestions != null) {
225            notifyDataSetChanged();
226        } else {
227            notifyDataSetInvalidated();
228        }
229    }
230
231    public void onSuggestionClicked(long suggestionId) {
232        if (mClosed) {
233            Log.w(TAG, "onSuggestionClicked after close");
234        } else if (mSuggestionClickListener != null) {
235            mSuggestionClickListener.onSuggestionClicked(this, suggestionId);
236        }
237    }
238
239    public void onSuggestionQuickContactClicked(long suggestionId) {
240        if (mClosed) {
241            Log.w(TAG, "onSuggestionQuickContactClicked after close");
242        } else if (mSuggestionClickListener != null) {
243            mSuggestionClickListener.onSuggestionQuickContactClicked(this, suggestionId);
244        }
245    }
246
247    public void onSuggestionRemoveFromHistoryClicked(long suggestionId) {
248        if (mClosed) {
249            Log.w(TAG, "onSuggestionRemoveFromHistoryClicked after close");
250        } else if (mSuggestionClickListener != null) {
251            mSuggestionClickListener.onSuggestionRemoveFromHistoryClicked(this, suggestionId);
252        }
253    }
254
255    public void onSuggestionQueryRefineClicked(long suggestionId) {
256        if (mClosed) {
257            Log.w(TAG, "onSuggestionQueryRefineClicked after close");
258        } else if (mSuggestionClickListener != null) {
259            mSuggestionClickListener.onSuggestionQueryRefineClicked(this, suggestionId);
260        }
261    }
262
263    public abstract A getListAdapter();
264
265    protected abstract void notifyDataSetInvalidated();
266
267    protected abstract void notifyDataSetChanged();
268
269    private class MySuggestionsObserver extends DataSetObserver {
270        @Override
271        public void onChanged() {
272            onSuggestionsChanged();
273        }
274    }
275
276    private class SuggestionViewClickListener implements View.OnClickListener {
277        private final long mSuggestionId;
278        public SuggestionViewClickListener(long suggestionId) {
279            mSuggestionId = suggestionId;
280        }
281        public void onClick(View v) {
282            onSuggestionClicked(mSuggestionId);
283        }
284    }
285
286}
287