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 package com.android.quicksearchbox.ui; 17 18 import com.android.quicksearchbox.CorpusResult; 19 import com.android.quicksearchbox.ListSuggestionCursor; 20 import com.android.quicksearchbox.R; 21 import com.android.quicksearchbox.Suggestion; 22 import com.android.quicksearchbox.SuggestionCursor; 23 import com.android.quicksearchbox.SuggestionPosition; 24 import com.android.quicksearchbox.SuggestionUtils; 25 import com.android.quicksearchbox.Suggestions; 26 27 import android.content.Context; 28 import android.util.Log; 29 import android.view.LayoutInflater; 30 import android.view.View; 31 import android.view.ViewGroup; 32 import android.widget.BaseExpandableListAdapter; 33 import android.widget.ExpandableListAdapter; 34 35 import java.util.ArrayList; 36 import java.util.HashSet; 37 38 /** 39 * Adapter for suggestions list where suggestions are clustered by corpus. 40 */ 41 public 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