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 17 package com.android.browser; 18 19 import android.content.Context; 20 import android.database.ContentObserver; 21 import android.database.Cursor; 22 import android.database.DataSetObserver; 23 import android.os.Handler; 24 import android.provider.BaseColumns; 25 import android.view.LayoutInflater; 26 import android.view.View; 27 import android.view.ViewGroup; 28 import android.webkit.DateSorter; 29 import android.widget.ExpandableListAdapter; 30 import android.widget.ExpandableListView; 31 import android.widget.TextView; 32 33 import java.util.Vector; 34 35 /** 36 * ExpandableListAdapter which separates data into categories based on date. 37 * Used for History and Downloads. 38 */ 39 public class DateSortedExpandableListAdapter implements ExpandableListAdapter { 40 // Array for each of our bins. Each entry represents how many items are 41 // in that bin. 42 private int mItemMap[]; 43 // This is our GroupCount. We will have at most DateSorter.DAY_COUNT 44 // bins, less if the user has no items in one or more bins. 45 private int mNumberOfBins; 46 private Vector<DataSetObserver> mObservers; 47 private Cursor mCursor; 48 private DateSorter mDateSorter; 49 private int mDateIndex; 50 private int mIdIndex; 51 private Context mContext; 52 53 private class ChangeObserver extends ContentObserver { 54 public ChangeObserver() { 55 super(new Handler()); 56 } 57 58 @Override 59 public boolean deliverSelfNotifications() { 60 return true; 61 } 62 63 @Override 64 public void onChange(boolean selfChange) { 65 refreshData(); 66 } 67 } 68 69 public DateSortedExpandableListAdapter(Context context, Cursor cursor, 70 int dateIndex) { 71 mContext = context; 72 mDateSorter = new DateSorter(context); 73 mObservers = new Vector<DataSetObserver>(); 74 mCursor = cursor; 75 mIdIndex = cursor.getColumnIndexOrThrow(BaseColumns._ID); 76 cursor.registerContentObserver(new ChangeObserver()); 77 mDateIndex = dateIndex; 78 buildMap(); 79 } 80 81 /** 82 * Set up the bins for determining which items belong to which groups. 83 */ 84 private void buildMap() { 85 // The cursor is sorted by date 86 // The ItemMap will store the number of items in each bin. 87 int array[] = new int[DateSorter.DAY_COUNT]; 88 // Zero out the array. 89 for (int j = 0; j < DateSorter.DAY_COUNT; j++) { 90 array[j] = 0; 91 } 92 mNumberOfBins = 0; 93 int dateIndex = -1; 94 if (mCursor.moveToFirst() && mCursor.getCount() > 0) { 95 while (!mCursor.isAfterLast()) { 96 long date = getLong(mDateIndex); 97 int index = mDateSorter.getIndex(date); 98 if (index > dateIndex) { 99 mNumberOfBins++; 100 if (index == DateSorter.DAY_COUNT - 1) { 101 // We are already in the last bin, so it will 102 // include all the remaining items 103 array[index] = mCursor.getCount() 104 - mCursor.getPosition(); 105 break; 106 } 107 dateIndex = index; 108 } 109 array[dateIndex]++; 110 mCursor.moveToNext(); 111 } 112 } 113 mItemMap = array; 114 } 115 116 /** 117 * Get the byte array at cursorIndex from the Cursor. Assumes the Cursor 118 * has already been moved to the correct position. Along with 119 * {@link #getInt} and {@link #getString}, these are provided so the client 120 * does not need to access the Cursor directly 121 * @param cursorIndex Index to query the Cursor. 122 * @return corresponding byte array from the Cursor. 123 */ 124 /* package */ byte[] getBlob(int cursorIndex) { 125 return mCursor.getBlob(cursorIndex); 126 } 127 128 /* package */ Context getContext() { 129 return mContext; 130 } 131 132 /** 133 * Get the integer at cursorIndex from the Cursor. Assumes the Cursor has 134 * already been moved to the correct position. Along with 135 * {@link #getBlob} and {@link #getString}, these are provided so the client 136 * does not need to access the Cursor directly 137 * @param cursorIndex Index to query the Cursor. 138 * @return corresponding integer from the Cursor. 139 */ 140 /* package */ int getInt(int cursorIndex) { 141 return mCursor.getInt(cursorIndex); 142 } 143 144 /** 145 * Get the long at cursorIndex from the Cursor. Assumes the Cursor has 146 * already been moved to the correct position. 147 */ 148 /* package */ long getLong(int cursorIndex) { 149 return mCursor.getLong(cursorIndex); 150 } 151 152 /** 153 * Get the String at cursorIndex from the Cursor. Assumes the Cursor has 154 * already been moved to the correct position. Along with 155 * {@link #getInt} and {@link #getInt}, these are provided so the client 156 * does not need to access the Cursor directly 157 * @param cursorIndex Index to query the Cursor. 158 * @return corresponding String from the Cursor. 159 */ 160 /* package */ String getString(int cursorIndex) { 161 return mCursor.getString(cursorIndex); 162 } 163 164 /** 165 * Determine which group an item belongs to. 166 * @param childId ID of the child view in question. 167 * @return int Group position of the containing group. 168 /* package */ int groupFromChildId(long childId) { 169 int group = -1; 170 for (mCursor.moveToFirst(); !mCursor.isAfterLast(); 171 mCursor.moveToNext()) { 172 if (getLong(mIdIndex) == childId) { 173 int bin = mDateSorter.getIndex(getLong(mDateIndex)); 174 // bin is the same as the group if the number of bins is the 175 // same as DateSorter 176 if (mDateSorter.DAY_COUNT == mNumberOfBins) return bin; 177 // There are some empty bins. Find the corresponding group. 178 group = 0; 179 for (int i = 0; i < bin; i++) { 180 if (mItemMap[i] != 0) group++; 181 } 182 break; 183 } 184 } 185 return group; 186 } 187 188 /** 189 * Translates from a group position in the ExpandableList to a bin. This is 190 * necessary because some groups have no history items, so we do not include 191 * those in the ExpandableList. 192 * @param groupPosition Position in the ExpandableList's set of groups 193 * @return The corresponding bin that holds that group. 194 */ 195 private int groupPositionToBin(int groupPosition) { 196 if (groupPosition < 0 || groupPosition >= DateSorter.DAY_COUNT) { 197 throw new AssertionError("group position out of range"); 198 } 199 if (DateSorter.DAY_COUNT == mNumberOfBins || 0 == mNumberOfBins) { 200 // In the first case, we have exactly the same number of bins 201 // as our maximum possible, so there is no need to do a 202 // conversion 203 // The second statement is in case this method gets called when 204 // the array is empty, in which case the provided groupPosition 205 // will do fine. 206 return groupPosition; 207 } 208 int arrayPosition = -1; 209 while (groupPosition > -1) { 210 arrayPosition++; 211 if (mItemMap[arrayPosition] != 0) { 212 groupPosition--; 213 } 214 } 215 return arrayPosition; 216 } 217 218 /** 219 * Move the cursor to the position indicated. 220 * @param packedPosition Position in packed position representation. 221 * @return True on success, false otherwise. 222 */ 223 boolean moveCursorToPackedChildPosition(long packedPosition) { 224 if (ExpandableListView.getPackedPositionType(packedPosition) != 225 ExpandableListView.PACKED_POSITION_TYPE_CHILD) { 226 return false; 227 } 228 int groupPosition = ExpandableListView.getPackedPositionGroup( 229 packedPosition); 230 int childPosition = ExpandableListView.getPackedPositionChild( 231 packedPosition); 232 return moveCursorToChildPosition(groupPosition, childPosition); 233 } 234 235 /** 236 * Move the cursor the the position indicated. 237 * @param groupPosition Index of the group containing the desired item. 238 * @param childPosition Index of the item within the specified group. 239 * @return boolean False if the cursor is closed, so the Cursor was not 240 * moved. True on success. 241 */ 242 /* package */ boolean moveCursorToChildPosition(int groupPosition, 243 int childPosition) { 244 if (mCursor.isClosed()) return false; 245 groupPosition = groupPositionToBin(groupPosition); 246 int index = childPosition; 247 for (int i = 0; i < groupPosition; i++) { 248 index += mItemMap[i]; 249 } 250 return mCursor.moveToPosition(index); 251 } 252 253 /* package */ void refreshData() { 254 if (mCursor.isClosed()) { 255 return; 256 } 257 mCursor.requery(); 258 buildMap(); 259 for (DataSetObserver o : mObservers) { 260 o.onChanged(); 261 } 262 } 263 264 public View getGroupView(int groupPosition, boolean isExpanded, 265 View convertView, ViewGroup parent) { 266 TextView item; 267 if (null == convertView || !(convertView instanceof TextView)) { 268 LayoutInflater factory = LayoutInflater.from(mContext); 269 item = (TextView) factory.inflate(R.layout.history_header, null); 270 } else { 271 item = (TextView) convertView; 272 } 273 String label = mDateSorter.getLabel(groupPositionToBin(groupPosition)); 274 item.setText(label); 275 return item; 276 } 277 278 public View getChildView(int groupPosition, int childPosition, 279 boolean isLastChild, View convertView, ViewGroup parent) { 280 return null; 281 } 282 283 public boolean areAllItemsEnabled() { 284 return true; 285 } 286 287 public boolean isChildSelectable(int groupPosition, int childPosition) { 288 return true; 289 } 290 291 public int getGroupCount() { 292 return mNumberOfBins; 293 } 294 295 public int getChildrenCount(int groupPosition) { 296 return mItemMap[groupPositionToBin(groupPosition)]; 297 } 298 299 public Object getGroup(int groupPosition) { 300 return null; 301 } 302 303 public Object getChild(int groupPosition, int childPosition) { 304 return null; 305 } 306 307 public long getGroupId(int groupPosition) { 308 return groupPosition; 309 } 310 311 public long getChildId(int groupPosition, int childPosition) { 312 if (moveCursorToChildPosition(groupPosition, childPosition)) { 313 return getLong(mIdIndex); 314 } 315 return 0; 316 } 317 318 public boolean hasStableIds() { 319 return true; 320 } 321 322 public void registerDataSetObserver(DataSetObserver observer) { 323 mObservers.add(observer); 324 } 325 326 public void unregisterDataSetObserver(DataSetObserver observer) { 327 mObservers.remove(observer); 328 } 329 330 public void onGroupExpanded(int groupPosition) { 331 } 332 333 public void onGroupCollapsed(int groupPosition) { 334 } 335 336 public long getCombinedChildId(long groupId, long childId) { 337 return childId; 338 } 339 340 public long getCombinedGroupId(long groupId) { 341 return groupId; 342 } 343 344 public boolean isEmpty() { 345 return mCursor.isClosed() || mCursor.getCount() == 0; 346 } 347 } 348