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.util.Log; 30import android.view.LayoutInflater; 31import android.view.View; 32import android.view.ViewGroup; 33import android.widget.BaseAdapter; 34import android.widget.TextView; 35 36import java.util.ArrayList; 37import java.util.Formatter; 38import java.util.Iterator; 39import java.util.LinkedList; 40import java.util.Locale; 41 42public class AgendaByDayAdapter extends BaseAdapter { 43 private static final int TYPE_DAY = 0; 44 private static final int TYPE_MEETING = 1; 45 static final int TYPE_LAST = 2; 46 47 private final Context mContext; 48 private final AgendaAdapter mAgendaAdapter; 49 private final LayoutInflater mInflater; 50 private ArrayList<RowInfo> mRowInfo; 51 private int mTodayJulianDay; 52 private Time mTmpTime; 53 private String mTimeZone; 54 // Note: Formatter is not thread safe. Fine for now as it is only used by the main thread. 55 private Formatter mFormatter; 56 private StringBuilder mStringBuilder; 57 58 static class ViewHolder { 59 TextView dayView; 60 TextView dateView; 61 int julianDay; 62 boolean grayed; 63 } 64 65 private Runnable mTZUpdater = new Runnable() { 66 @Override 67 public void run() { 68 mTimeZone = Utils.getTimeZone(mContext, this); 69 mTmpTime = new Time(mTimeZone); 70 notifyDataSetChanged(); 71 } 72 }; 73 74 public AgendaByDayAdapter(Context context) { 75 mContext = context; 76 mAgendaAdapter = new AgendaAdapter(context, R.layout.agenda_item); 77 mInflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 78 mStringBuilder = new StringBuilder(50); 79 mFormatter = new Formatter(mStringBuilder, Locale.getDefault()); 80 mTimeZone = Utils.getTimeZone(context, mTZUpdater); 81 mTmpTime = new Time(mTimeZone); 82 } 83 84 public long getInstanceId(int position) { 85 if (mRowInfo == null || position >= mRowInfo.size()) { 86 return -1; 87 } 88 return mRowInfo.get(position).mInstanceId; 89 } 90 91 // Returns the position of a header of a specific item 92 public int getHeaderPosition(int position) { 93 if (mRowInfo == null || position >= mRowInfo.size()) { 94 return -1; 95 } 96 97 for (int i = position; i >=0; i --) { 98 RowInfo row = mRowInfo.get(i); 99 if (row != null && row.mType == TYPE_DAY) 100 return i; 101 } 102 return -1; 103 } 104 105 // Returns the number of items in a section defined by a specific header location 106 public int getHeaderItemsCount(int position) { 107 if (mRowInfo == null) { 108 return -1; 109 } 110 int count = 0; 111 for (int i = position +1; i < mRowInfo.size(); i++) { 112 if (mRowInfo.get(i).mType != TYPE_MEETING) { 113 return count; 114 } 115 count ++; 116 } 117 return count; 118 } 119 120 public int getCount() { 121 if (mRowInfo != null) { 122 return mRowInfo.size(); 123 } 124 return mAgendaAdapter.getCount(); 125 } 126 127 public Object getItem(int position) { 128 if (mRowInfo != null) { 129 RowInfo row = mRowInfo.get(position); 130 if (row.mType == TYPE_DAY) { 131 return row; 132 } else { 133 return mAgendaAdapter.getItem(row.mPosition); 134 } 135 } 136 return mAgendaAdapter.getItem(position); 137 } 138 139 public long getItemId(int position) { 140 if (mRowInfo != null) { 141 RowInfo row = mRowInfo.get(position); 142 if (row.mType == TYPE_DAY) { 143 return -position; 144 } else { 145 return mAgendaAdapter.getItemId(row.mPosition); 146 } 147 } 148 return mAgendaAdapter.getItemId(position); 149 } 150 151 @Override 152 public int getViewTypeCount() { 153 return TYPE_LAST; 154 } 155 156 @Override 157 public int getItemViewType(int position) { 158 return mRowInfo != null && mRowInfo.size() > position ? 159 mRowInfo.get(position).mType : TYPE_DAY; 160 } 161 162 public boolean isDayHeaderView(int position) { 163 return (getItemViewType(position) == TYPE_DAY); 164 } 165 166 public View getView(int position, View convertView, ViewGroup parent) { 167 if ((mRowInfo == null) || (position > mRowInfo.size())) { 168 // If we have no row info, mAgendaAdapter returns the view. 169 return mAgendaAdapter.getView(position, convertView, parent); 170 } 171 172 RowInfo row = mRowInfo.get(position); 173 if (row.mType == TYPE_DAY) { 174 ViewHolder holder = null; 175 View agendaDayView = null; 176 if ((convertView != null) && (convertView.getTag() != null)) { 177 // Listview may get confused and pass in a different type of 178 // view since we keep shifting data around. Not a big problem. 179 Object tag = convertView.getTag(); 180 if (tag instanceof ViewHolder) { 181 agendaDayView = convertView; 182 holder = (ViewHolder) tag; 183 holder.julianDay = row.mDay; 184 } 185 } 186 187 if (holder == null) { 188 // Create a new AgendaView with a ViewHolder for fast access to 189 // views w/o calling findViewById() 190 holder = new ViewHolder(); 191 agendaDayView = mInflater.inflate(R.layout.agenda_day, parent, false); 192 holder.dayView = (TextView) agendaDayView.findViewById(R.id.day); 193 holder.dateView = (TextView) agendaDayView.findViewById(R.id.date); 194 holder.julianDay = row.mDay; 195 holder.grayed = false; 196 agendaDayView.setTag(holder); 197 } 198 199 // Re-use the member variable "mTime" which is set to the local 200 // time zone. 201 // It's difficult to find and update all these adapters when the 202 // home tz changes so check it here and update if needed. 203 String tz = Utils.getTimeZone(mContext, mTZUpdater); 204 if (!TextUtils.equals(tz, mTmpTime.timezone)) { 205 mTimeZone = tz; 206 mTmpTime = new Time(tz); 207 } 208 209 // Build the text for the day of the week. 210 // Should be yesterday/today/tomorrow (if applicable) + day of the week 211 212 Time date = mTmpTime; 213 long millis = date.setJulianDay(row.mDay); 214 int flags = DateUtils.FORMAT_SHOW_WEEKDAY; 215 mStringBuilder.setLength(0); 216 217 String dayViewText = Utils.getDayOfWeekString(row.mDay, mTodayJulianDay, millis, 218 mContext); 219 220 // Build text for the date 221 // Format should be month day 222 223 mStringBuilder.setLength(0); 224 flags = DateUtils.FORMAT_SHOW_DATE; 225 String dateViewText = DateUtils.formatDateRange(mContext, mFormatter, millis, millis, 226 flags, mTimeZone).toString(); 227 228 if (AgendaWindowAdapter.BASICLOG) { 229 dayViewText += " P:" + position; 230 dateViewText += " P:" + position; 231 } 232 holder.dayView.setText(dayViewText); 233 holder.dateView.setText(dateViewText); 234 235 // Set the background of the view, it is grayed for day that are in the past and today 236 if (row.mDay > mTodayJulianDay) { 237 agendaDayView.setBackgroundResource(R.drawable.agenda_item_bg_primary); 238 holder.grayed = false; 239 } else { 240 agendaDayView.setBackgroundResource(R.drawable.agenda_item_bg_secondary); 241 holder.grayed = true; 242 } 243 return agendaDayView; 244 } else if (row.mType == TYPE_MEETING) { 245 View itemView = mAgendaAdapter.getView(row.mPosition, convertView, parent); 246 AgendaAdapter.ViewHolder holder = ((AgendaAdapter.ViewHolder) itemView.getTag()); 247 TextView title = holder.title; 248 long eventStartTime = holder.startTimeMilli; 249 boolean allDay = holder.allDay; 250 if (AgendaWindowAdapter.BASICLOG) { 251 title.setText(title.getText() + " P:" + position); 252 } else { 253 title.setText(title.getText()); 254 } 255 256 // if event in the past or started already, un-bold the title and set the background 257 if ((!allDay && eventStartTime <= System.currentTimeMillis()) || 258 (allDay && row.mDay <= mTodayJulianDay)) { 259 itemView.setBackgroundResource(R.drawable.agenda_item_bg_secondary); 260 title.setTypeface(Typeface.DEFAULT); 261 holder.grayed = true; 262 } else { 263 itemView.setBackgroundResource(R.drawable.agenda_item_bg_primary); 264 title.setTypeface(Typeface.DEFAULT_BOLD); 265 holder.grayed = false; 266 } 267 holder.julianDay = row.mDay; 268 return itemView; 269 } else { 270 // Error 271 throw new IllegalStateException("Unknown event type:" + row.mType); 272 } 273 } 274 275 public void clearDayHeaderInfo() { 276 mRowInfo = null; 277 } 278 279 public void changeCursor(DayAdapterInfo info) { 280 calculateDays(info); 281 mAgendaAdapter.changeCursor(info.cursor); 282 } 283 284 public void calculateDays(DayAdapterInfo dayAdapterInfo) { 285 Cursor cursor = dayAdapterInfo.cursor; 286 ArrayList<RowInfo> rowInfo = new ArrayList<RowInfo>(); 287 int prevStartDay = -1; 288 289 Time tempTime = new Time(mTimeZone); 290 long now = System.currentTimeMillis(); 291 tempTime.set(now); 292 mTodayJulianDay = Time.getJulianDay(now, tempTime.gmtoff); 293 294 LinkedList<MultipleDayInfo> multipleDayList = new LinkedList<MultipleDayInfo>(); 295 for (int position = 0; cursor.moveToNext(); position++) { 296 int startDay = cursor.getInt(AgendaWindowAdapter.INDEX_START_DAY); 297 long id = cursor.getLong(AgendaWindowAdapter.INDEX_EVENT_ID); 298 long startTime = cursor.getLong(AgendaWindowAdapter.INDEX_BEGIN); 299 long endTime = cursor.getLong(AgendaWindowAdapter.INDEX_END); 300 long instanceId = cursor.getLong(AgendaWindowAdapter.INDEX_INSTANCE_ID); 301 boolean allDay = cursor.getInt(AgendaWindowAdapter.INDEX_ALL_DAY) != 0; 302 if (allDay) { 303 startTime = Utils.convertAlldayUtcToLocal(tempTime, startTime, mTimeZone); 304 endTime = Utils.convertAlldayUtcToLocal(tempTime, endTime, mTimeZone); 305 } 306 // Skip over the days outside of the adapter's range 307 startDay = Math.max(startDay, dayAdapterInfo.start); 308 // Make sure event's start time is not before the start of the day 309 // (setJulianDay sets the time to 12:00am) 310 long adapterStartTime = tempTime.setJulianDay(startDay); 311 startTime = Math.max(startTime, adapterStartTime); 312 313 if (startDay != prevStartDay) { 314 // Check if we skipped over any empty days 315 if (prevStartDay == -1) { 316 rowInfo.add(new RowInfo(TYPE_DAY, startDay)); 317 } else { 318 // If there are any multiple-day events that span the empty 319 // range of days, then create day headers and events for 320 // those multiple-day events. 321 boolean dayHeaderAdded = false; 322 for (int currentDay = prevStartDay + 1; currentDay <= startDay; currentDay++) { 323 dayHeaderAdded = false; 324 Iterator<MultipleDayInfo> iter = multipleDayList.iterator(); 325 while (iter.hasNext()) { 326 MultipleDayInfo info = iter.next(); 327 // If this event has ended then remove it from the 328 // list. 329 if (info.mEndDay < currentDay) { 330 iter.remove(); 331 continue; 332 } 333 334 // If this is the first event for the day, then 335 // insert a day header. 336 if (!dayHeaderAdded) { 337 rowInfo.add(new RowInfo(TYPE_DAY, currentDay)); 338 dayHeaderAdded = true; 339 } 340 long nextMidnight = Utils.getNextMidnight(tempTime, 341 info.mEventStartTimeMilli, mTimeZone); 342 343 long infoEndTime = (info.mEndDay == currentDay) ? 344 info.mEventEndTimeMilli : nextMidnight; 345 rowInfo.add(new RowInfo(TYPE_MEETING, currentDay, info.mPosition, 346 info.mEventId, info.mEventStartTimeMilli, 347 infoEndTime, info.mInstanceId, info.mAllDay)); 348 349 info.mEventStartTimeMilli = nextMidnight; 350 } 351 } 352 353 // If the day header was not added for the start day, then 354 // add it now. 355 if (!dayHeaderAdded) { 356 rowInfo.add(new RowInfo(TYPE_DAY, startDay)); 357 } 358 } 359 prevStartDay = startDay; 360 } 361 362 // Add in the event for this cursor position 363 rowInfo.add(new RowInfo(TYPE_MEETING, startDay, position, id, startTime, endTime, 364 instanceId, allDay)); 365 366 // If this event spans multiple days, then add it to the multipleDay 367 // list. 368 int endDay = cursor.getInt(AgendaWindowAdapter.INDEX_END_DAY); 369 370 // Skip over the days outside of the adapter's range 371 endDay = Math.min(endDay, dayAdapterInfo.end); 372 if (endDay > startDay) { 373 multipleDayList.add(new MultipleDayInfo(position, endDay, id, 374 Utils.getNextMidnight(tempTime, startTime, mTimeZone), 375 endTime, instanceId, allDay)); 376 } 377 } 378 379 // There are no more cursor events but we might still have multiple-day 380 // events left. So create day headers and events for those. 381 if (prevStartDay > 0) { 382 for (int currentDay = prevStartDay + 1; currentDay <= dayAdapterInfo.end; 383 currentDay++) { 384 boolean dayHeaderAdded = false; 385 Iterator<MultipleDayInfo> iter = multipleDayList.iterator(); 386 while (iter.hasNext()) { 387 MultipleDayInfo info = iter.next(); 388 // If this event has ended then remove it from the 389 // list. 390 if (info.mEndDay < currentDay) { 391 iter.remove(); 392 continue; 393 } 394 395 // If this is the first event for the day, then 396 // insert a day header. 397 if (!dayHeaderAdded) { 398 rowInfo.add(new RowInfo(TYPE_DAY, currentDay)); 399 dayHeaderAdded = true; 400 } 401 long nextMidnight = Utils.getNextMidnight(tempTime, info.mEventStartTimeMilli, 402 mTimeZone); 403 long infoEndTime = 404 (info.mEndDay == currentDay) ? info.mEventEndTimeMilli : nextMidnight; 405 rowInfo.add(new RowInfo(TYPE_MEETING, currentDay, info.mPosition, 406 info.mEventId, info.mEventStartTimeMilli, infoEndTime, 407 info.mInstanceId, info.mAllDay)); 408 409 info.mEventStartTimeMilli = nextMidnight; 410 } 411 } 412 } 413 mRowInfo = rowInfo; 414 } 415 416 private static class RowInfo { 417 // mType is either a day header (TYPE_DAY) or an event (TYPE_MEETING) 418 final int mType; 419 420 final int mDay; // Julian day 421 final int mPosition; // cursor position (not used for TYPE_DAY) 422 // This is used to mark a day header as the first day with events that is "today" 423 // or later. This flag is used by the adapter to create a view with a visual separator 424 // between the past and the present/future 425 boolean mFirstDayAfterYesterday; 426 final long mEventId; 427 final long mEventStartTimeMilli; 428 final long mEventEndTimeMilli; 429 final long mInstanceId; 430 final boolean mAllDay; 431 432 RowInfo(int type, int julianDay, int position, long id, long startTime, long endTime, 433 long instanceId, boolean allDay) { 434 mType = type; 435 mDay = julianDay; 436 mPosition = position; 437 mEventId = id; 438 mEventStartTimeMilli = startTime; 439 mEventEndTimeMilli = endTime; 440 mFirstDayAfterYesterday = false; 441 mInstanceId = instanceId; 442 mAllDay = allDay; 443 } 444 445 RowInfo(int type, int julianDay) { 446 mType = type; 447 mDay = julianDay; 448 mPosition = 0; 449 mEventId = 0; 450 mEventStartTimeMilli = 0; 451 mEventEndTimeMilli = 0; 452 mFirstDayAfterYesterday = false; 453 mInstanceId = -1; 454 mAllDay = false; 455 } 456 } 457 458 private static class MultipleDayInfo { 459 final int mPosition; 460 final int mEndDay; 461 final long mEventId; 462 long mEventStartTimeMilli; 463 long mEventEndTimeMilli; 464 final long mInstanceId; 465 final boolean mAllDay; 466 467 MultipleDayInfo(int position, int endDay, long id, long startTime, long endTime, 468 long instanceId, boolean allDay) { 469 mPosition = position; 470 mEndDay = endDay; 471 mEventId = id; 472 mEventStartTimeMilli = startTime; 473 mEventEndTimeMilli = endTime; 474 mInstanceId = instanceId; 475 mAllDay = allDay; 476 } 477 } 478 479 /** 480 * Finds the position in the cursor of the event that best matches the time and Id. 481 * It will try to find the event that has the specified id and start time, if such event 482 * doesn't exist, it will return the event with a matching id that is closest to the start time. 483 * If the id doesn't exist, it will return the event with start time closest to the specified 484 * time. 485 * @param time - start of event in milliseconds (or any arbitrary time if event id is unknown) 486 * @param id - Event id (-1 if unknown). 487 * @return Position of event (if found) or position of nearest event according to the time. 488 * Zero if no event found 489 */ 490 public int findEventPositionNearestTime(Time time, long id) { 491 if (mRowInfo == null) { 492 return 0; 493 } 494 long millis = time.toMillis(false /* use isDst */); 495 long minDistance = Integer.MAX_VALUE; // some big number 496 long IdFoundMinDistance = Integer.MAX_VALUE; // some big number 497 int minIndex = 0; 498 int idFoundMinIndex = 0; 499 int eventInTimeIndex = -1; 500 int allDayEventInTimeIndex = -1; 501 int allDayEventDay = 0; 502 int minDay = 0; 503 boolean idFound = false; 504 int len = mRowInfo.size(); 505 506 // Loop through the events and find the best match 507 // 1. Event id and start time matches requested id and time 508 // 2. Event id matches and closest time 509 // 3. No event id match , time matches a all day event (midnight) 510 // 4. No event id match , time is between event start and end 511 // 5. No event id match , all day event 512 // 6. The closest event to the requested time 513 514 for (int index = 0; index < len; index++) { 515 RowInfo row = mRowInfo.get(index); 516 if (row.mType == TYPE_DAY) { 517 continue; 518 } 519 520 // Found exact match - done 521 if (row.mEventId == id) { 522 if (row.mEventStartTimeMilli == millis) { 523 return index; 524 } 525 526 // Not an exact match, Save event index if it is the closest to time so far 527 long distance = Math.abs(millis - row.mEventStartTimeMilli); 528 if (distance < minDistance) { 529 IdFoundMinDistance = distance; 530 idFoundMinIndex = index; 531 } 532 idFound = true; 533 } 534 if (!idFound) { 535 // Found an event that contains the requested time 536 if (millis >= row.mEventStartTimeMilli && millis <= row.mEventEndTimeMilli) { 537 if (row.mAllDay) { 538 if (millis == row.mEventStartTimeMilli) { 539 return index; 540 } 541 allDayEventInTimeIndex = index; 542 allDayEventDay = row.mDay; 543 } else { 544 eventInTimeIndex = index; 545 } 546 } else { 547 // Save event index if it is the closest to time so far 548 long distance = Math.abs(millis - row.mEventStartTimeMilli); 549 if (distance < minDistance) { 550 minDistance = distance; 551 minIndex = index; 552 minDay = row.mDay; 553 } 554 } 555 } 556 } 557 // We didn't find an exact match so take the best matching event 558 if (idFound) { 559 return idFoundMinIndex; 560 } 561 if (eventInTimeIndex != -1) { 562 return eventInTimeIndex; 563 } else if (allDayEventInTimeIndex != -1 && minDay != allDayEventDay) { 564 return allDayEventInTimeIndex; 565 } 566 return minIndex; 567 } 568 569 570 /** 571 * Returns a flag indicating if this position is the first day after "yesterday" that has 572 * events in it. 573 * 574 * @return a flag indicating if this is the "first day after yesterday" 575 */ 576 public boolean isFirstDayAfterYesterday(int position) { 577 int headerPos = getHeaderPosition(position); 578 RowInfo row = mRowInfo.get(headerPos); 579 if (row != null) { 580 return row.mFirstDayAfterYesterday; 581 } 582 return false; 583 } 584 585 /** 586 * Finds the Julian day containing the event at the given position. 587 * 588 * @param position the list position of an event 589 * @return the Julian day containing that event 590 */ 591 public int findJulianDayFromPosition(int position) { 592 if (mRowInfo == null || position < 0) { 593 return 0; 594 } 595 596 int len = mRowInfo.size(); 597 if (position >= len) return 0; // no row info at this position 598 599 for (int index = position; index >= 0; index--) { 600 RowInfo row = mRowInfo.get(index); 601 if (row.mType == TYPE_DAY) { 602 return row.mDay; 603 } 604 } 605 return 0; 606 } 607 608 /** 609 * Marks the current row as the first day that has events after "yesterday". 610 * Used to mark the separation between the past and the present/future 611 * 612 * @param position in the adapter 613 */ 614 public void setAsFirstDayAfterYesterday(int position) { 615 if (mRowInfo == null || position < 0 || position > mRowInfo.size()) { 616 return; 617 } 618 RowInfo row = mRowInfo.get(position); 619 row.mFirstDayAfterYesterday = true; 620 } 621 622 /** 623 * Converts a list position to a cursor position. The list contains 624 * day headers as well as events. The cursor contains only events. 625 * 626 * @param listPos the list position of an event 627 * @return the corresponding cursor position of that event 628 * if the position point to day header , it will give the position of the next event 629 * negated. 630 */ 631 public int getCursorPosition(int listPos) { 632 if (mRowInfo != null && listPos >= 0) { 633 RowInfo row = mRowInfo.get(listPos); 634 if (row.mType == TYPE_MEETING) { 635 return row.mPosition; 636 } else { 637 int nextPos = listPos + 1; 638 if (nextPos < mRowInfo.size()) { 639 nextPos = getCursorPosition(nextPos); 640 if (nextPos >= 0) { 641 return -nextPos; 642 } 643 } 644 } 645 } 646 return Integer.MIN_VALUE; 647 } 648 649 @Override 650 public boolean areAllItemsEnabled() { 651 return false; 652 } 653 654 @Override 655 public boolean isEnabled(int position) { 656 if (mRowInfo != null && position < mRowInfo.size()) { 657 RowInfo row = mRowInfo.get(position); 658 return row.mType == TYPE_MEETING; 659 } 660 return true; 661 } 662} 663