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