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.CorpusResult;
19import com.android.quicksearchbox.ListSuggestionCursor;
20import com.android.quicksearchbox.R;
21import com.android.quicksearchbox.Suggestion;
22import com.android.quicksearchbox.SuggestionCursor;
23import com.android.quicksearchbox.SuggestionPosition;
24import com.android.quicksearchbox.SuggestionUtils;
25import com.android.quicksearchbox.Suggestions;
26
27import android.content.Context;
28import android.util.Log;
29import android.view.LayoutInflater;
30import android.view.View;
31import android.view.ViewGroup;
32import android.widget.BaseExpandableListAdapter;
33import android.widget.ExpandableListAdapter;
34
35import java.util.ArrayList;
36import java.util.HashSet;
37
38/**
39 * Adapter for suggestions list where suggestions are clustered by corpus.
40 */
41public class ClusteredSuggestionsAdapter extends SuggestionsAdapterBase<ExpandableListAdapter> {
42
43    private static final String TAG = "QSB.ClusteredSuggestionsAdapter";
44
45    private final static int GROUP_SHIFT = 32;
46    private final static long CHILD_MASK = 0xffffffff;
47
48    private final Adapter mAdapter;
49    private final Context mContext;
50    private final LayoutInflater mInflater;
51
52    public ClusteredSuggestionsAdapter(SuggestionViewFactory viewFactory, Context context) {
53        super(viewFactory);
54        mAdapter = new Adapter();
55        mContext = context;
56        mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
57    }
58
59    @Override
60    public boolean isEmpty() {
61        return mAdapter.getGroupCount() == 0;
62    }
63
64    @Override
65    public boolean willPublishNonPromotedSuggestions() {
66        return true;
67    }
68
69    @Override
70    public SuggestionPosition getSuggestion(long suggestionId) {
71        return mAdapter.getChildById(suggestionId);
72    }
73
74    @Override
75    public ExpandableListAdapter getListAdapter() {
76        return mAdapter;
77    }
78
79    @Override
80    protected void notifyDataSetChanged() {
81        mAdapter.buildCorpusGroups();
82        mAdapter.notifyDataSetChanged();
83    }
84
85    @Override
86    protected void notifyDataSetInvalidated() {
87        mAdapter.buildCorpusGroups();
88        mAdapter.notifyDataSetInvalidated();
89    }
90
91    private class Adapter extends BaseExpandableListAdapter {
92
93        private ArrayList<SuggestionCursor> mCorpusGroups;
94
95        public void buildCorpusGroups() {
96            Suggestions suggestions = getSuggestions();
97            SuggestionCursor promoted = getCurrentPromotedSuggestions();
98            HashSet<String> promotedSuggestions = new HashSet<String>();
99            if (promoted != null && promoted.getCount() > 0) {
100                promoted.moveTo(0);
101                do {
102                    promotedSuggestions.add(SuggestionUtils.getSuggestionKey(promoted));
103                } while (promoted.moveToNext());
104            }
105            if (suggestions == null) {
106                mCorpusGroups = null;
107            } else {
108                if (mCorpusGroups == null) {
109                    mCorpusGroups = new ArrayList<SuggestionCursor>();
110                } else {
111                    mCorpusGroups.clear();
112                }
113                for (CorpusResult result : suggestions.getCorpusResults()) {
114                    ListSuggestionCursor corpusSuggestions = new ListSuggestionCursor(
115                            result.getUserQuery());
116                    for (int i = 0; i < result.getCount(); ++i) {
117                        result.moveTo(i);
118                        if (!result.isWebSearchSuggestion()) {
119                            if (!promotedSuggestions.contains(
120                                    SuggestionUtils.getSuggestionKey(result))) {
121                                corpusSuggestions.add(new SuggestionPosition(result, i));
122                            }
123                        }
124                    }
125                    if (corpusSuggestions.getCount() > 0) {
126                        mCorpusGroups.add(corpusSuggestions);
127                    }
128                }
129            }
130        }
131
132        @Override
133        public long getCombinedChildId(long groupId, long childId) {
134            // add one to the child ID to ensure that the group elements do not have the same ID
135            // as the first child within the group.
136            return (groupId << GROUP_SHIFT) | ((childId + 1) & CHILD_MASK);
137        }
138
139        @Override
140        public long getCombinedGroupId(long groupId) {
141            return groupId << GROUP_SHIFT;
142        }
143
144        public int getChildPosition(long childId) {
145            return (int) (childId & CHILD_MASK) - 1;
146        }
147
148        public int getGroupPosition(long childId) {
149            return (int) ((childId >> GROUP_SHIFT) & CHILD_MASK);
150        }
151
152        @Override
153        public Suggestion getChild(int groupPosition, int childPosition) {
154            SuggestionCursor c = getGroup(groupPosition);
155            if (c != null) {
156                c.moveTo(childPosition);
157                return new SuggestionPosition(c, childPosition);
158            }
159            return null;
160        }
161
162        public SuggestionPosition getChildById(long childId) {
163            SuggestionCursor groupCursor = getGroup(getGroupPosition(childId));
164            if (groupCursor != null) {
165                return new SuggestionPosition(groupCursor, getChildPosition(childId));
166            } else {
167                Log.w(TAG, "Invalid childId " + Long.toHexString(childId) + " (invalid group)");
168                return null;
169            }
170        }
171
172        @Override
173        public long getChildId(int groupPosition, int childPosition) {
174            return childPosition;
175        }
176
177        @Override
178        public View getChildView(int groupPosition, int childPosition, boolean isLastChild,
179                View convertView, ViewGroup parent) {
180            SuggestionCursor cursor = getGroup(groupPosition);
181            if (cursor == null) return null;
182            return getView(cursor, childPosition, getCombinedChildId(groupPosition, childPosition),
183                    convertView, parent);
184        }
185
186        @Override
187        public int getChildrenCount(int groupPosition) {
188            SuggestionCursor group = getGroup(groupPosition);
189            return group == null ? 0 : group.getCount();
190        }
191
192        @Override
193        public SuggestionCursor getGroup(int groupPosition) {
194            if (groupPosition < promotedGroupCount()) {
195                return getCurrentPromotedSuggestions();
196            } else {
197                int pos = groupPosition - promotedGroupCount();
198                if ((pos < 0 ) || (pos >= mCorpusGroups.size())) return null;
199                return mCorpusGroups.get(pos);
200            }
201        }
202
203        private int promotedCount() {
204            SuggestionCursor promoted = getCurrentPromotedSuggestions();
205            return (promoted == null ? 0 : promoted.getCount());
206        }
207
208        private int promotedGroupCount() {
209            return (promotedCount() == 0) ? 0 : 1;
210        }
211
212        private int corpusGroupCount() {
213            return mCorpusGroups == null ? 0 : mCorpusGroups.size();
214        }
215
216        @Override
217        public int getGroupCount() {
218            return promotedGroupCount() + corpusGroupCount();
219        }
220
221        @Override
222        public long getGroupId(int groupPosition) {
223            return groupPosition;
224        }
225
226        @Override
227        public View getGroupView(
228                int groupPosition, boolean isExpanded, View convertView, ViewGroup parent) {
229            if (convertView == null) {
230                convertView = mInflater.inflate(R.layout.suggestion_group, parent, false);
231            }
232            if (groupPosition == 0) {
233                // don't show the group separator for the first group, to avoid seeing an empty
234                // gap at the top of the list.
235                convertView.getLayoutParams().height = 0;
236            } else {
237                convertView.getLayoutParams().height = mContext.getResources().
238                        getDimensionPixelSize(R.dimen.suggestion_group_spacing);
239            }
240            // since we've fiddled with the layout params:
241            convertView.requestLayout();
242            return convertView;
243        }
244
245        @Override
246        public boolean hasStableIds() {
247            return false;
248        }
249
250        @Override
251        public boolean isChildSelectable(int groupPosition, int childPosition) {
252            return true;
253        }
254
255        @Override
256        public int getChildType(int groupPosition, int childPosition) {
257            return getSuggestionViewType(getGroup(groupPosition), childPosition);
258        }
259
260        @Override
261        public int getChildTypeCount() {
262            return getSuggestionViewTypeCount();
263        }
264    }
265
266}
267