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.Cursor; 21 import android.database.DataSetObserver; 22 import android.view.LayoutInflater; 23 import android.view.View; 24 import android.view.ViewGroup; 25 import android.webkit.DateSorter; 26 import android.widget.BaseExpandableListAdapter; 27 import android.widget.ExpandableListView; 28 import android.widget.TextView; 29 30 /** 31 * ExpandableListAdapter which separates data into categories based on date. 32 * Used for History and Downloads. 33 */ 34 public class DateSortedExpandableListAdapter extends BaseExpandableListAdapter { 35 // Array for each of our bins. Each entry represents how many items are 36 // in that bin. 37 private int mItemMap[]; 38 // This is our GroupCount. We will have at most DateSorter.DAY_COUNT 39 // bins, less if the user has no items in one or more bins. 40 private int mNumberOfBins; 41 private Cursor mCursor; 42 private DateSorter mDateSorter; 43 private int mDateIndex; 44 private int mIdIndex; 45 private Context mContext; 46 47 boolean mDataValid; 48 49 DataSetObserver mDataSetObserver = new DataSetObserver() { 50 @Override 51 public void onChanged() { 52 mDataValid = true; 53 notifyDataSetChanged(); 54 } 55 56 @Override 57 public void onInvalidated() { 58 mDataValid = false; 59 notifyDataSetInvalidated(); 60 } 61 }; 62 63 public DateSortedExpandableListAdapter(Context context, int dateIndex) { 64 mContext = context; 65 mDateSorter = new DateSorter(context); 66 mDateIndex = dateIndex; 67 mDataValid = false; 68 mIdIndex = -1; 69 } 70 71 /** 72 * Set up the bins for determining which items belong to which groups. 73 */ 74 private void buildMap() { 75 // The cursor is sorted by date 76 // The ItemMap will store the number of items in each bin. 77 int array[] = new int[DateSorter.DAY_COUNT]; 78 // Zero out the array. 79 for (int j = 0; j < DateSorter.DAY_COUNT; j++) { 80 array[j] = 0; 81 } 82 mNumberOfBins = 0; 83 int dateIndex = -1; 84 if (mCursor.moveToFirst() && mCursor.getCount() > 0) { 85 while (!mCursor.isAfterLast()) { 86 long date = getLong(mDateIndex); 87 int index = mDateSorter.getIndex(date); 88 if (index > dateIndex) { 89 mNumberOfBins++; 90 if (index == DateSorter.DAY_COUNT - 1) { 91 // We are already in the last bin, so it will 92 // include all the remaining items 93 array[index] = mCursor.getCount() 94 - mCursor.getPosition(); 95 break; 96 } 97 dateIndex = index; 98 } 99 array[dateIndex]++; 100 mCursor.moveToNext(); 101 } 102 } 103 mItemMap = array; 104 } 105 106 /** 107 * Get the byte array at cursorIndex from the Cursor. Assumes the Cursor 108 * has already been moved to the correct position. Along with 109 * {@link #getInt} and {@link #getString}, these are provided so the client 110 * does not need to access the Cursor directly 111 * @param cursorIndex Index to query the Cursor. 112 * @return corresponding byte array from the Cursor. 113 */ 114 /* package */ byte[] getBlob(int cursorIndex) { 115 if (!mDataValid) return null; 116 return mCursor.getBlob(cursorIndex); 117 } 118 119 /* package */ Context getContext() { 120 return mContext; 121 } 122 123 /** 124 * Get the integer at cursorIndex from the Cursor. Assumes the Cursor has 125 * already been moved to the correct position. Along with 126 * {@link #getBlob} and {@link #getString}, these are provided so the client 127 * does not need to access the Cursor directly 128 * @param cursorIndex Index to query the Cursor. 129 * @return corresponding integer from the Cursor. 130 */ 131 /* package */ int getInt(int cursorIndex) { 132 if (!mDataValid) return 0; 133 return mCursor.getInt(cursorIndex); 134 } 135 136 /** 137 * Get the long at cursorIndex from the Cursor. Assumes the Cursor has 138 * already been moved to the correct position. 139 */ 140 /* package */ long getLong(int cursorIndex) { 141 if (!mDataValid) return 0; 142 return mCursor.getLong(cursorIndex); 143 } 144 145 /** 146 * Get the String at cursorIndex from the Cursor. Assumes the Cursor has 147 * already been moved to the correct position. Along with 148 * {@link #getInt} and {@link #getInt}, these are provided so the client 149 * does not need to access the Cursor directly 150 * @param cursorIndex Index to query the Cursor. 151 * @return corresponding String from the Cursor. 152 */ 153 /* package */ String getString(int cursorIndex) { 154 if (!mDataValid) return null; 155 return mCursor.getString(cursorIndex); 156 } 157 158 /** 159 * Determine which group an item belongs to. 160 * @param childId ID of the child view in question. 161 * @return int Group position of the containing group. 162 /* package */ int groupFromChildId(long childId) { 163 if (!mDataValid) return -1; 164 int group = -1; 165 for (mCursor.moveToFirst(); !mCursor.isAfterLast(); 166 mCursor.moveToNext()) { 167 if (getLong(mIdIndex) == childId) { 168 int bin = mDateSorter.getIndex(getLong(mDateIndex)); 169 // bin is the same as the group if the number of bins is the 170 // same as DateSorter 171 if (DateSorter.DAY_COUNT == mNumberOfBins) { 172 return bin; 173 } 174 // There are some empty bins. Find the corresponding group. 175 group = 0; 176 for (int i = 0; i < bin; i++) { 177 if (mItemMap[i] != 0) { 178 group++; 179 } 180 } 181 break; 182 } 183 } 184 return group; 185 } 186 187 /** 188 * Translates from a group position in the ExpandableList to a bin. This is 189 * necessary because some groups have no history items, so we do not include 190 * those in the ExpandableList. 191 * @param groupPosition Position in the ExpandableList's set of groups 192 * @return The corresponding bin that holds that group. 193 */ 194 private int groupPositionToBin(int groupPosition) { 195 if (!mDataValid) return -1; 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 (!mDataValid || mCursor.isClosed()) { 245 return false; 246 } 247 groupPosition = groupPositionToBin(groupPosition); 248 int index = childPosition; 249 for (int i = 0; i < groupPosition; i++) { 250 index += mItemMap[i]; 251 } 252 return mCursor.moveToPosition(index); 253 } 254 255 public void changeCursor(Cursor cursor) { 256 if (cursor == mCursor) { 257 return; 258 } 259 if (mCursor != null) { 260 mCursor.unregisterDataSetObserver(mDataSetObserver); 261 mCursor.close(); 262 } 263 mCursor = cursor; 264 if (cursor != null) { 265 cursor.registerDataSetObserver(mDataSetObserver); 266 mIdIndex = cursor.getColumnIndexOrThrow("_id"); 267 mDataValid = true; 268 buildMap(); 269 // notify the observers about the new cursor 270 notifyDataSetChanged(); 271 } else { 272 mIdIndex = -1; 273 mDataValid = false; 274 // notify the observers about the lack of a data set 275 notifyDataSetInvalidated(); 276 } 277 } 278 279 @Override 280 public View getGroupView(int groupPosition, boolean isExpanded, 281 View convertView, ViewGroup parent) { 282 if (!mDataValid) throw new IllegalStateException("Data is not valid"); 283 TextView item; 284 if (null == convertView || !(convertView instanceof TextView)) { 285 LayoutInflater factory = LayoutInflater.from(mContext); 286 item = (TextView) factory.inflate(R.layout.history_header, null); 287 } else { 288 item = (TextView) convertView; 289 } 290 String label = mDateSorter.getLabel(groupPositionToBin(groupPosition)); 291 item.setText(label); 292 return item; 293 } 294 295 @Override 296 public View getChildView(int groupPosition, int childPosition, 297 boolean isLastChild, View convertView, ViewGroup parent) { 298 if (!mDataValid) throw new IllegalStateException("Data is not valid"); 299 return null; 300 } 301 302 @Override 303 public boolean areAllItemsEnabled() { 304 return true; 305 } 306 307 @Override 308 public boolean isChildSelectable(int groupPosition, int childPosition) { 309 return true; 310 } 311 312 @Override 313 public int getGroupCount() { 314 if (!mDataValid) return 0; 315 return mNumberOfBins; 316 } 317 318 @Override 319 public int getChildrenCount(int groupPosition) { 320 if (!mDataValid) return 0; 321 return mItemMap[groupPositionToBin(groupPosition)]; 322 } 323 324 @Override 325 public Object getGroup(int groupPosition) { 326 return null; 327 } 328 329 @Override 330 public Object getChild(int groupPosition, int childPosition) { 331 return null; 332 } 333 334 @Override 335 public long getGroupId(int groupPosition) { 336 if (!mDataValid) return 0; 337 return groupPosition; 338 } 339 340 @Override 341 public long getChildId(int groupPosition, int childPosition) { 342 if (!mDataValid) return 0; 343 if (moveCursorToChildPosition(groupPosition, childPosition)) { 344 return getLong(mIdIndex); 345 } 346 return 0; 347 } 348 349 @Override 350 public boolean hasStableIds() { 351 return true; 352 } 353 354 @Override 355 public void onGroupExpanded(int groupPosition) { 356 } 357 358 @Override 359 public void onGroupCollapsed(int groupPosition) { 360 } 361 362 @Override 363 public long getCombinedChildId(long groupId, long childId) { 364 if (!mDataValid) return 0; 365 return childId; 366 } 367 368 @Override 369 public long getCombinedGroupId(long groupId) { 370 if (!mDataValid) return 0; 371 return groupId; 372 } 373 374 @Override 375 public boolean isEmpty() { 376 return !mDataValid || mCursor == null || mCursor.isClosed() || mCursor.getCount() == 0; 377 } 378 } 379