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.Corpus;
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.ViewGroup;
28import android.view.View.OnFocusChangeListener;
29import android.widget.BaseAdapter;
30
31/**
32 * Uses a {@link Suggestions} object to back a {@link SuggestionsView}.
33 */
34public class SuggestionsAdapter extends BaseAdapter {
35
36    private static final boolean DBG = false;
37    private static final String TAG = "QSB.SuggestionsAdapter";
38
39    private DataSetObserver mDataSetObserver;
40
41    private final SuggestionViewFactory mViewFactory;
42
43    private SuggestionCursor mCursor;
44
45    private Corpus mCorpus = null;
46
47    private Suggestions mSuggestions;
48
49    private SuggestionClickListener mSuggestionClickListener;
50    private OnFocusChangeListener mOnFocusChangeListener;
51
52    private boolean mClosed = false;
53
54    public SuggestionsAdapter(SuggestionViewFactory viewFactory) {
55        mViewFactory = viewFactory;
56    }
57
58    public boolean isClosed() {
59        return mClosed;
60    }
61
62    public void close() {
63        setSuggestions(null);
64        mCorpus = null;
65        mClosed = true;
66    }
67
68    public void setSuggestionClickListener(SuggestionClickListener listener) {
69        mSuggestionClickListener = listener;
70    }
71
72    public void setOnFocusChangeListener(OnFocusChangeListener l) {
73        mOnFocusChangeListener = l;
74    }
75
76    public void setSuggestions(Suggestions suggestions) {
77        if (mSuggestions == suggestions) {
78            return;
79        }
80        if (mClosed) {
81            if (suggestions != null) {
82                suggestions.close();
83            }
84            return;
85        }
86        if (mDataSetObserver == null) {
87            mDataSetObserver = new MySuggestionsObserver();
88        }
89        // TODO: delay the change if there are no suggestions for the currently visible tab.
90        if (mSuggestions != null) {
91            mSuggestions.unregisterDataSetObserver(mDataSetObserver);
92            mSuggestions.close();
93        }
94        mSuggestions = suggestions;
95        if (mSuggestions != null) {
96            mSuggestions.registerDataSetObserver(mDataSetObserver);
97        }
98        onSuggestionsChanged();
99    }
100
101    public Suggestions getSuggestions() {
102        return mSuggestions;
103    }
104
105    /**
106     * Gets the source whose results are displayed.
107     */
108    public Corpus getCorpus() {
109        return mCorpus;
110    }
111
112    /**
113     * Sets the source whose results are displayed.
114     */
115    public void setCorpus(Corpus corpus) {
116        if (mSuggestions != null) {
117            if ((mCorpus == null) && (corpus != null)) {
118                // we've just switched from the 'All' corpus to a specific corpus
119                // we can filter the existing results immediately.
120                if (DBG) Log.d(TAG, "setCorpus(" + corpus.getName() + ") Filter suggestions");
121                mSuggestions.filterByCorpus(corpus);
122            } else if (corpus != null) {
123                // Note, when switching from a specific corpus to 'All' we do not change the
124                // suggestions, since they're still relevant for 'All'. Hence 'corpus != null'
125                if (DBG) Log.d(TAG, "setCorpus(" + corpus.getName() + ") Clear suggestions");
126                mSuggestions.unregisterDataSetObserver(mDataSetObserver);
127                mSuggestions.close();
128                mSuggestions = null;
129            }
130        }
131        mCorpus = corpus;
132        onSuggestionsChanged();
133    }
134
135    public int getCount() {
136        return mCursor == null ? 0 : mCursor.getCount();
137    }
138
139    public SuggestionPosition getItem(int position) {
140        if (mCursor == null) return null;
141        return new SuggestionPosition(mCursor, position);
142    }
143
144    public long getItemId(int position) {
145        return position;
146    }
147
148    @Override
149    public int getViewTypeCount() {
150        return mViewFactory.getSuggestionViewTypeCount();
151    }
152
153    @Override
154    public int getItemViewType(int position) {
155        if (mCursor == null) {
156            return 0;
157        }
158        mCursor.moveTo(position);
159        return mViewFactory.getSuggestionViewType(mCursor);
160    }
161
162    public View getView(int position, View convertView, ViewGroup parent) {
163        if (mCursor == null) {
164            throw new IllegalStateException("getView() called with null cursor");
165        }
166        mCursor.moveTo(position);
167        int viewType = mViewFactory.getSuggestionViewType(mCursor);
168        SuggestionView view = mViewFactory.getSuggestionView(viewType, convertView, parent);
169        view.bindAsSuggestion(mCursor, mSuggestionClickListener);
170        View v = (View) view;
171        if (mOnFocusChangeListener != null) {
172            v.setOnFocusChangeListener(mOnFocusChangeListener);
173        }
174        return v;
175    }
176
177    protected void onSuggestionsChanged() {
178        if (DBG) Log.d(TAG, "onSuggestionsChanged(" + mSuggestions + ")");
179        SuggestionCursor cursor = getCorpusCursor(mSuggestions, mCorpus);
180        changeCursor(cursor);
181    }
182
183    /**
184     * Gets the cursor containing the currently shown suggestions. The caller should not hold
185     * on to or modify the returned cursor.
186     */
187    public SuggestionCursor getCurrentSuggestions() {
188        return mCursor;
189    }
190
191    /**
192     * Gets the cursor for the given source.
193     */
194    protected SuggestionCursor getCorpusCursor(Suggestions suggestions, Corpus corpus) {
195        if (suggestions == null) return null;
196        return suggestions.getPromoted();
197    }
198
199    /**
200     * Replace the cursor.
201     *
202     * This does not close the old cursor. Instead, all the cursors are closed in
203     * {@link #setSuggestions(Suggestions)}.
204     */
205    private void changeCursor(SuggestionCursor newCursor) {
206        if (DBG) Log.d(TAG, "changeCursor(" + newCursor + ")");
207        if (newCursor == mCursor) {
208            // Shortcuts may have changed without the cursor changing.
209            notifyDataSetChanged();
210            return;
211        }
212        mCursor = newCursor;
213        if (mCursor != null) {
214            // TODO: Register observers here to watch for
215            // changes in the cursor, e.g. shortcut refreshes?
216            notifyDataSetChanged();
217        } else {
218            notifyDataSetInvalidated();
219        }
220    }
221
222    public void onIcon2Clicked(int position) {
223        if (mSuggestionClickListener != null) {
224            mSuggestionClickListener.onSuggestionQueryRefineClicked(position);
225        }
226    }
227
228    private class MySuggestionsObserver extends DataSetObserver {
229        @Override
230        public void onChanged() {
231            onSuggestionsChanged();
232        }
233    }
234
235}
236