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