AgendaByDayAdapter.java revision 31412a0fea756e0da0bcbdf3cdffe4efae21cdbe
1/* 2 * Copyright (C) 2008 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.calendar.agenda; 18 19import com.android.calendar.R; 20import com.android.calendar.Utils; 21import com.android.calendar.agenda.AgendaWindowAdapter.DayAdapterInfo; 22 23import android.content.Context; 24import android.database.Cursor; 25import android.graphics.Typeface; 26import android.text.TextUtils; 27import android.text.format.DateUtils; 28import android.text.format.Time; 29import android.view.LayoutInflater; 30import android.view.View; 31import android.view.ViewGroup; 32import android.widget.BaseAdapter; 33import android.widget.TextView; 34 35import java.util.ArrayList; 36import java.util.Formatter; 37import java.util.Iterator; 38import java.util.LinkedList; 39import java.util.Locale; 40 41public class AgendaByDayAdapter extends BaseAdapter { 42 private static final int TYPE_DAY = 0; 43 private static final int TYPE_MEETING = 1; 44 static final int TYPE_LAST = 2; 45 46 private final Context mContext; 47 private final AgendaAdapter mAgendaAdapter; 48 private final LayoutInflater mInflater; 49 private ArrayList<RowInfo> mRowInfo; 50 private int mTodayJulianDay; 51 private Time mTmpTime; 52 private String mTimeZone; 53 // Note: Formatter is not thread safe. Fine for now as it is only used by the main thread. 54 private Formatter mFormatter; 55 private StringBuilder mStringBuilder; 56 57 static class ViewHolder { 58 TextView dayView; 59 TextView dateView; 60 int julianDay; 61 boolean grayed; 62 } 63 64 private Runnable mTZUpdater = new Runnable() { 65 @Override 66 public void run() { 67 mTimeZone = Utils.getTimeZone(mContext, this); 68 mTmpTime = new Time(mTimeZone); 69 notifyDataSetChanged(); 70 } 71 }; 72 73 public AgendaByDayAdapter(Context context) { 74 mContext = context; 75 mAgendaAdapter = new AgendaAdapter(context, R.layout.agenda_item); 76 mInflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 77 mStringBuilder = new StringBuilder(50); 78 mFormatter = new Formatter(mStringBuilder, Locale.getDefault()); 79 mTimeZone = Utils.getTimeZone(context, mTZUpdater); 80 mTmpTime = new Time(mTimeZone); 81 } 82 83 84 // Returns the position of a header of a specific item 85 public int getHeaderPosition(int position) { 86 if (mRowInfo == null || position >= mRowInfo.size()) { 87 return -1; 88 } 89 90 for (int i = position; i >=0; i --) { 91 RowInfo row = mRowInfo.get(i); 92 if (row != null && row.mType == TYPE_DAY) 93 return i; 94 } 95 return -1; 96 } 97 98 // Returns the number of items in a section defined by a specific header location 99 public int getHeaderItemsCount(int position) { 100 if (mRowInfo == null) { 101 return -1; 102 } 103 int count = 0; 104 for (int i = position +1; i < mRowInfo.size(); i++) { 105 if (mRowInfo.get(i).mType != TYPE_MEETING) { 106 return count; 107 } 108 count ++; 109 } 110 return count; 111 } 112 113 public int getCount() { 114 if (mRowInfo != null) { 115 return mRowInfo.size(); 116 } 117 return mAgendaAdapter.getCount(); 118 } 119 120 public Object getItem(int position) { 121 if (mRowInfo != null) { 122 RowInfo row = mRowInfo.get(position); 123 if (row.mType == TYPE_DAY) { 124 return row; 125 } else { 126 return mAgendaAdapter.getItem(row.mPosition); 127 } 128 } 129 return mAgendaAdapter.getItem(position); 130 } 131 132 public long getItemId(int position) { 133 if (mRowInfo != null) { 134 RowInfo row = mRowInfo.get(position); 135 if (row.mType == TYPE_DAY) { 136 return -position; 137 } else { 138 return mAgendaAdapter.getItemId(row.mPosition); 139 } 140 } 141 return mAgendaAdapter.getItemId(position); 142 } 143 144 @Override 145 public int getViewTypeCount() { 146 return TYPE_LAST; 147 } 148 149 @Override 150 public int getItemViewType(int position) { 151 return mRowInfo != null && mRowInfo.size() > position ? 152 mRowInfo.get(position).mType : TYPE_DAY; 153 } 154 155 public boolean isDayHeaderView(int position) { 156 return (getItemViewType(position) == TYPE_DAY); 157 } 158 159 public View getView(int position, View convertView, ViewGroup parent) { 160 if ((mRowInfo == null) || (position > mRowInfo.size())) { 161 // If we have no row info, mAgendaAdapter returns the view. 162 return mAgendaAdapter.getView(position, convertView, parent); 163 } 164 165 RowInfo row = mRowInfo.get(position); 166 if (row.mType == TYPE_DAY) { 167 ViewHolder holder = null; 168 View agendaDayView = null; 169 if ((convertView != null) && (convertView.getTag() != null)) { 170 // Listview may get confused and pass in a different type of 171 // view since we keep shifting data around. Not a big problem. 172 Object tag = convertView.getTag(); 173 if (tag instanceof ViewHolder) { 174 agendaDayView = convertView; 175 holder = (ViewHolder) tag; 176 holder.julianDay = row.mDay; 177 } 178 } 179 180 if (holder == null) { 181 // Create a new AgendaView with a ViewHolder for fast access to 182 // views w/o calling findViewById() 183 holder = new ViewHolder(); 184 agendaDayView = mInflater.inflate(R.layout.agenda_day, parent, false); 185 holder.dayView = (TextView) agendaDayView.findViewById(R.id.day); 186 holder.dateView = (TextView) agendaDayView.findViewById(R.id.date); 187 holder.julianDay = row.mDay; 188 holder.grayed = false; 189 agendaDayView.setTag(holder); 190 } 191 192 // Re-use the member variable "mTime" which is set to the local 193 // time zone. 194 // It's difficult to find and update all these adapters when the 195 // home tz changes so check it here and update if needed. 196 String tz = Utils.getTimeZone(mContext, mTZUpdater); 197 if (!TextUtils.equals(tz, mTmpTime.timezone)) { 198 mTimeZone = tz; 199 mTmpTime = new Time(tz); 200 } 201 202 // Build the text for the day of the week. 203 // Should be yesterday/today/tomorrow (if applicable) + day of the week 204 205 Time date = mTmpTime; 206 long millis = date.setJulianDay(row.mDay); 207 int flags = DateUtils.FORMAT_SHOW_WEEKDAY; 208 mStringBuilder.setLength(0); 209 210 String dayViewText = Utils.getDayOfWeekString(row.mDay, mTodayJulianDay, millis, 211 mContext); 212 213 // Build text for the date 214 // Format should be month day 215 216 mStringBuilder.setLength(0); 217 flags = DateUtils.FORMAT_SHOW_DATE; 218 String dateViewText = DateUtils.formatDateRange(mContext, mFormatter, millis, millis, 219 flags, mTimeZone).toString(); 220 221 if (AgendaWindowAdapter.BASICLOG) { 222 dayViewText += " P:" + position; 223 dateViewText += " P:" + position; 224 } 225 holder.dayView.setText(dayViewText); 226 holder.dateView.setText(dateViewText); 227 228 // Set the background of the view, it is grayed for day that are in the past and today 229 if (row.mDay > mTodayJulianDay) { 230 agendaDayView.setBackgroundResource(R.drawable.agenda_item_bg_primary); 231 holder.grayed = false; 232 } else { 233 agendaDayView.setBackgroundResource(R.drawable.agenda_item_bg_secondary); 234 holder.grayed = true; 235 } 236 return agendaDayView; 237 } else if (row.mType == TYPE_MEETING) { 238 View itemView = mAgendaAdapter.getView(row.mPosition, convertView, parent); 239 AgendaAdapter.ViewHolder holder = ((AgendaAdapter.ViewHolder) itemView.getTag()); 240 TextView title = holder.title; 241 long eventStartTime = holder.startTimeMilli; 242 boolean allDay = holder.allDay; 243 if (AgendaWindowAdapter.BASICLOG) { 244 title.setText(title.getText() + " P:" + position); 245 } else { 246 title.setText(title.getText()); 247 } 248 249 // if event in the past or started already, un-bold the title and set the background 250 if ((!allDay && eventStartTime <= System.currentTimeMillis()) || 251 (allDay && row.mDay <= mTodayJulianDay)) { 252 itemView.setBackgroundResource(R.drawable.agenda_item_bg_secondary); 253 title.setTypeface(Typeface.DEFAULT); 254 holder.grayed = true; 255 } else { 256 itemView.setBackgroundResource(R.drawable.agenda_item_bg_primary); 257 title.setTypeface(Typeface.DEFAULT_BOLD); 258 holder.grayed = false; 259 } 260 holder.julianDay = row.mDay; 261 return itemView; 262 } else { 263 // Error 264 throw new IllegalStateException("Unknown event type:" + row.mType); 265 } 266 } 267 268 public void clearDayHeaderInfo() { 269 mRowInfo = null; 270 } 271 272 public void changeCursor(DayAdapterInfo info) { 273 calculateDays(info); 274 mAgendaAdapter.changeCursor(info.cursor); 275 } 276 277 public void calculateDays(DayAdapterInfo dayAdapterInfo) { 278 Cursor cursor = dayAdapterInfo.cursor; 279 ArrayList<RowInfo> rowInfo = new ArrayList<RowInfo>(); 280 int prevStartDay = -1; 281 Time time = new Time(mTimeZone); 282 long now = System.currentTimeMillis(); 283 time.set(now); 284 mTodayJulianDay = Time.getJulianDay(now, time.gmtoff); 285 LinkedList<MultipleDayInfo> multipleDayList = new LinkedList<MultipleDayInfo>(); 286 for (int position = 0; cursor.moveToNext(); position++) { 287 int startDay = cursor.getInt(AgendaWindowAdapter.INDEX_START_DAY); 288 289 // Skip over the days outside of the adapter's range 290 startDay = Math.max(startDay, dayAdapterInfo.start); 291 292 if (startDay != prevStartDay) { 293 // Check if we skipped over any empty days 294 if (prevStartDay == -1) { 295 rowInfo.add(new RowInfo(TYPE_DAY, startDay, 0)); 296 } else { 297 // If there are any multiple-day events that span the empty 298 // range of days, then create day headers and events for 299 // those multiple-day events. 300 boolean dayHeaderAdded = false; 301 for (int currentDay = prevStartDay + 1; currentDay <= startDay; currentDay++) { 302 dayHeaderAdded = false; 303 Iterator<MultipleDayInfo> iter = multipleDayList.iterator(); 304 while (iter.hasNext()) { 305 MultipleDayInfo info = iter.next(); 306 // If this event has ended then remove it from the 307 // list. 308 if (info.mEndDay < currentDay) { 309 iter.remove(); 310 continue; 311 } 312 313 // If this is the first event for the day, then 314 // insert a day header. 315 if (!dayHeaderAdded) { 316 rowInfo.add(new RowInfo(TYPE_DAY, currentDay, 0)); 317 dayHeaderAdded = true; 318 } 319 rowInfo.add(new RowInfo(TYPE_MEETING, currentDay, info.mPosition)); 320 } 321 } 322 323 // If the day header was not added for the start day, then 324 // add it now. 325 if (!dayHeaderAdded) { 326 rowInfo.add(new RowInfo(TYPE_DAY, startDay, 0)); 327 } 328 } 329 prevStartDay = startDay; 330 } 331 332 // Add in the event for this cursor position 333 rowInfo.add(new RowInfo(TYPE_MEETING, startDay, position)); 334 335 // If this event spans multiple days, then add it to the multipleDay 336 // list. 337 int endDay = cursor.getInt(AgendaWindowAdapter.INDEX_END_DAY); 338 339 // Skip over the days outside of the adapter's range 340 endDay = Math.min(endDay, dayAdapterInfo.end); 341 if (endDay > startDay) { 342 multipleDayList.add(new MultipleDayInfo(position, endDay)); 343 } 344 } 345 346 // There are no more cursor events but we might still have multiple-day 347 // events left. So create day headers and events for those. 348 if (prevStartDay > 0) { 349 for (int currentDay = prevStartDay + 1; currentDay <= dayAdapterInfo.end; 350 currentDay++) { 351 boolean dayHeaderAdded = false; 352 Iterator<MultipleDayInfo> iter = multipleDayList.iterator(); 353 while (iter.hasNext()) { 354 MultipleDayInfo info = iter.next(); 355 // If this event has ended then remove it from the 356 // list. 357 if (info.mEndDay < currentDay) { 358 iter.remove(); 359 continue; 360 } 361 362 // If this is the first event for the day, then 363 // insert a day header. 364 if (!dayHeaderAdded) { 365 rowInfo.add(new RowInfo(TYPE_DAY, currentDay, 0)); 366 dayHeaderAdded = true; 367 } 368 rowInfo.add(new RowInfo(TYPE_MEETING, currentDay, info.mPosition)); 369 } 370 } 371 } 372 mRowInfo = rowInfo; 373 } 374 375 private static class RowInfo { 376 // mType is either a day header (TYPE_DAY) or an event (TYPE_MEETING) 377 final int mType; 378 379 final int mDay; // Julian day 380 final int mPosition; // cursor position (not used for TYPE_DAY) 381 // This is used to mark a day header as the first day with events that is "today" 382 // or later. This flag is used by the adapter to create a view with a visual separator 383 // between the past and the present/future 384 boolean mFirstDayAfterYesterday; 385 386 RowInfo(int type, int julianDay, int position) { 387 mType = type; 388 mDay = julianDay; 389 mPosition = position; 390 mFirstDayAfterYesterday = false; 391 } 392 } 393 394 private static class MultipleDayInfo { 395 final int mPosition; 396 final int mEndDay; 397 398 MultipleDayInfo(int position, int endDay) { 399 mPosition = position; 400 mEndDay = endDay; 401 } 402 } 403 404 /** 405 * Searches for the day that matches the given Time object and returns the 406 * list position of that day. If there are no events for that day, then it 407 * finds the nearest day (before or after) that has events and returns the 408 * list position for that day. 409 * 410 * @param time the date to search for 411 * @return the cursor position of the first event for that date, or zero 412 * if no match was found 413 */ 414 public int findDayPositionNearestTime(Time time) { 415 if (mRowInfo == null) { 416 return 0; 417 } 418 long millis = time.toMillis(false /* use isDst */); 419 int julianDay = Time.getJulianDay(millis, time.gmtoff); 420 int minDistance = 1000; // some big number 421 int minIndex = 0; 422 int len = mRowInfo.size(); 423 for (int index = 0; index < len; index++) { 424 RowInfo row = mRowInfo.get(index); 425 if (row.mType == TYPE_DAY) { 426 int distance = Math.abs(julianDay - row.mDay); 427 if (distance == 0) { 428 return index; 429 } 430 if (distance < minDistance) { 431 minDistance = distance; 432 minIndex = index; 433 } 434 } 435 } 436 437 // We didn't find an exact match so take the nearest day that had 438 // events. 439 return minIndex; 440 } 441 442 /** 443 * Returns a flag indicating if this position is the first day after "yesterday" that has 444 * events in it. 445 * 446 * @return a flag indicating if this is the "first day after yesterday" 447 */ 448 public boolean isFirstDayAfterYesterday(int position) { 449 int headerPos = getHeaderPosition(position); 450 RowInfo row = mRowInfo.get(headerPos); 451 if (row != null) { 452 return row.mFirstDayAfterYesterday; 453 } 454 return false; 455 } 456 457 /** 458 * Finds the Julian day containing the event at the given position. 459 * 460 * @param position the list position of an event 461 * @return the Julian day containing that event 462 */ 463 public int findJulianDayFromPosition(int position) { 464 if (mRowInfo == null || position < 0) { 465 return 0; 466 } 467 468 int len = mRowInfo.size(); 469 if (position >= len) return 0; // no row info at this position 470 471 for (int index = position; index >= 0; index--) { 472 RowInfo row = mRowInfo.get(index); 473 if (row.mType == TYPE_DAY) { 474 return row.mDay; 475 } 476 } 477 return 0; 478 } 479 480 /** 481 * Marks the current row as the first day that has events after "yesterday". 482 * Used to mark the separation between the past and the present/future 483 * 484 * @param position in the adapter 485 */ 486 public void setAsFirstDayAfterYesterday(int position) { 487 if (mRowInfo == null || position < 0 || position > mRowInfo.size()) { 488 return; 489 } 490 RowInfo row = mRowInfo.get(position); 491 row.mFirstDayAfterYesterday = true; 492 } 493 494 /** 495 * Converts a list position to a cursor position. The list contains 496 * day headers as well as events. The cursor contains only events. 497 * 498 * @param listPos the list position of an event 499 * @return the corresponding cursor position of that event 500 */ 501 public int getCursorPosition(int listPos) { 502 if (mRowInfo != null && listPos >= 0) { 503 RowInfo row = mRowInfo.get(listPos); 504 if (row.mType == TYPE_MEETING) { 505 return row.mPosition; 506 } else { 507 int nextPos = listPos + 1; 508 if (nextPos < mRowInfo.size()) { 509 nextPos = getCursorPosition(nextPos); 510 if (nextPos >= 0) { 511 return -nextPos; 512 } 513 } 514 } 515 } 516 return Integer.MIN_VALUE; 517 } 518 519 @Override 520 public boolean areAllItemsEnabled() { 521 return false; 522 } 523 524 @Override 525 public boolean isEnabled(int position) { 526 if (mRowInfo != null && position < mRowInfo.size()) { 527 RowInfo row = mRowInfo.get(position); 528 return row.mType == TYPE_MEETING; 529 } 530 return true; 531 } 532} 533