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 com.android.calendar.R; 20 import com.android.calendar.Utils; 21 import com.android.calendar.agenda.AgendaWindowAdapter.DayAdapterInfo; 22 23 import android.content.Context; 24 import android.database.Cursor; 25 import android.graphics.Typeface; 26 import android.text.TextUtils; 27 import android.text.format.DateUtils; 28 import android.text.format.Time; 29 import android.util.Log; 30 import android.view.LayoutInflater; 31 import android.view.View; 32 import android.view.ViewGroup; 33 import android.widget.BaseAdapter; 34 import android.widget.TextView; 35 36 import java.util.ArrayList; 37 import java.util.Formatter; 38 import java.util.Iterator; 39 import java.util.LinkedList; 40 import java.util.Locale; 41 42 public 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 final Formatter mFormatter; 56 private final StringBuilder mStringBuilder; 57 58 static class ViewHolder { 59 TextView dayView; 60 TextView dateView; 61 int julianDay; 62 boolean grayed; 63 } 64 65 private final 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 // The holder in the view stores information from the cursor, but the cursor has no 249 // notion of multi-day event and the start time of each instance of a multi-day event 250 // is the same. RowInfo has the correct info , so take it from there. 251 holder.startTimeMilli = row.mEventStartTimeMilli; 252 boolean allDay = holder.allDay; 253 if (AgendaWindowAdapter.BASICLOG) { 254 title.setText(title.getText() + " P:" + position); 255 } else { 256 title.setText(title.getText()); 257 } 258 259 // if event in the past or started already, un-bold the title and set the background 260 if ((!allDay && row.mEventStartTimeMilli <= System.currentTimeMillis()) || 261 (allDay && row.mDay <= mTodayJulianDay)) { 262 itemView.setBackgroundResource(R.drawable.agenda_item_bg_secondary); 263 title.setTypeface(Typeface.DEFAULT); 264 holder.grayed = true; 265 } else { 266 itemView.setBackgroundResource(R.drawable.agenda_item_bg_primary); 267 title.setTypeface(Typeface.DEFAULT_BOLD); 268 holder.grayed = false; 269 } 270 holder.julianDay = row.mDay; 271 return itemView; 272 } else { 273 // Error 274 throw new IllegalStateException("Unknown event type:" + row.mType); 275 } 276 } 277 278 public void clearDayHeaderInfo() { 279 mRowInfo = null; 280 } 281 282 public void changeCursor(DayAdapterInfo info) { 283 calculateDays(info); 284 mAgendaAdapter.changeCursor(info.cursor); 285 } 286 287 public void calculateDays(DayAdapterInfo dayAdapterInfo) { 288 Cursor cursor = dayAdapterInfo.cursor; 289 ArrayList<RowInfo> rowInfo = new ArrayList<RowInfo>(); 290 int prevStartDay = -1; 291 292 Time tempTime = new Time(mTimeZone); 293 long now = System.currentTimeMillis(); 294 tempTime.set(now); 295 mTodayJulianDay = Time.getJulianDay(now, tempTime.gmtoff); 296 297 LinkedList<MultipleDayInfo> multipleDayList = new LinkedList<MultipleDayInfo>(); 298 for (int position = 0; cursor.moveToNext(); position++) { 299 int startDay = cursor.getInt(AgendaWindowAdapter.INDEX_START_DAY); 300 long id = cursor.getLong(AgendaWindowAdapter.INDEX_EVENT_ID); 301 long startTime = cursor.getLong(AgendaWindowAdapter.INDEX_BEGIN); 302 long endTime = cursor.getLong(AgendaWindowAdapter.INDEX_END); 303 long instanceId = cursor.getLong(AgendaWindowAdapter.INDEX_INSTANCE_ID); 304 boolean allDay = cursor.getInt(AgendaWindowAdapter.INDEX_ALL_DAY) != 0; 305 if (allDay) { 306 startTime = Utils.convertAlldayUtcToLocal(tempTime, startTime, mTimeZone); 307 endTime = Utils.convertAlldayUtcToLocal(tempTime, endTime, mTimeZone); 308 } 309 // Skip over the days outside of the adapter's range 310 startDay = Math.max(startDay, dayAdapterInfo.start); 311 // Make sure event's start time is not before the start of the day 312 // (setJulianDay sets the time to 12:00am) 313 long adapterStartTime = tempTime.setJulianDay(startDay); 314 startTime = Math.max(startTime, adapterStartTime); 315 316 if (startDay != prevStartDay) { 317 // Check if we skipped over any empty days 318 if (prevStartDay == -1) { 319 rowInfo.add(new RowInfo(TYPE_DAY, startDay)); 320 } else { 321 // If there are any multiple-day events that span the empty 322 // range of days, then create day headers and events for 323 // those multiple-day events. 324 boolean dayHeaderAdded = false; 325 for (int currentDay = prevStartDay + 1; currentDay <= startDay; currentDay++) { 326 dayHeaderAdded = false; 327 Iterator<MultipleDayInfo> iter = multipleDayList.iterator(); 328 while (iter.hasNext()) { 329 MultipleDayInfo info = iter.next(); 330 // If this event has ended then remove it from the 331 // list. 332 if (info.mEndDay < currentDay) { 333 iter.remove(); 334 continue; 335 } 336 337 // If this is the first event for the day, then 338 // insert a day header. 339 if (!dayHeaderAdded) { 340 rowInfo.add(new RowInfo(TYPE_DAY, currentDay)); 341 dayHeaderAdded = true; 342 } 343 long nextMidnight = Utils.getNextMidnight(tempTime, 344 info.mEventStartTimeMilli, mTimeZone); 345 346 long infoEndTime = (info.mEndDay == currentDay) ? 347 info.mEventEndTimeMilli : nextMidnight; 348 rowInfo.add(new RowInfo(TYPE_MEETING, currentDay, info.mPosition, 349 info.mEventId, info.mEventStartTimeMilli, 350 infoEndTime, info.mInstanceId, info.mAllDay)); 351 352 info.mEventStartTimeMilli = nextMidnight; 353 } 354 } 355 356 // If the day header was not added for the start day, then 357 // add it now. 358 if (!dayHeaderAdded) { 359 rowInfo.add(new RowInfo(TYPE_DAY, startDay)); 360 } 361 } 362 prevStartDay = startDay; 363 } 364 365 // Add in the event for this cursor position 366 rowInfo.add(new RowInfo(TYPE_MEETING, startDay, position, id, startTime, endTime, 367 instanceId, allDay)); 368 369 // If this event spans multiple days, then add it to the multipleDay 370 // list. 371 int endDay = cursor.getInt(AgendaWindowAdapter.INDEX_END_DAY); 372 373 // Skip over the days outside of the adapter's range 374 endDay = Math.min(endDay, dayAdapterInfo.end); 375 if (endDay > startDay) { 376 multipleDayList.add(new MultipleDayInfo(position, endDay, id, 377 Utils.getNextMidnight(tempTime, startTime, mTimeZone), 378 endTime, instanceId, allDay)); 379 } 380 } 381 382 // There are no more cursor events but we might still have multiple-day 383 // events left. So create day headers and events for those. 384 if (prevStartDay > 0) { 385 for (int currentDay = prevStartDay + 1; currentDay <= dayAdapterInfo.end; 386 currentDay++) { 387 boolean dayHeaderAdded = false; 388 Iterator<MultipleDayInfo> iter = multipleDayList.iterator(); 389 while (iter.hasNext()) { 390 MultipleDayInfo info = iter.next(); 391 // If this event has ended then remove it from the 392 // list. 393 if (info.mEndDay < currentDay) { 394 iter.remove(); 395 continue; 396 } 397 398 // If this is the first event for the day, then 399 // insert a day header. 400 if (!dayHeaderAdded) { 401 rowInfo.add(new RowInfo(TYPE_DAY, currentDay)); 402 dayHeaderAdded = true; 403 } 404 long nextMidnight = Utils.getNextMidnight(tempTime, info.mEventStartTimeMilli, 405 mTimeZone); 406 long infoEndTime = 407 (info.mEndDay == currentDay) ? info.mEventEndTimeMilli : nextMidnight; 408 rowInfo.add(new RowInfo(TYPE_MEETING, currentDay, info.mPosition, 409 info.mEventId, info.mEventStartTimeMilli, infoEndTime, 410 info.mInstanceId, info.mAllDay)); 411 412 info.mEventStartTimeMilli = nextMidnight; 413 } 414 } 415 } 416 mRowInfo = rowInfo; 417 } 418 419 private static class RowInfo { 420 // mType is either a day header (TYPE_DAY) or an event (TYPE_MEETING) 421 final int mType; 422 423 final int mDay; // Julian day 424 final int mPosition; // cursor position (not used for TYPE_DAY) 425 // This is used to mark a day header as the first day with events that is "today" 426 // or later. This flag is used by the adapter to create a view with a visual separator 427 // between the past and the present/future 428 boolean mFirstDayAfterYesterday; 429 final long mEventId; 430 final long mEventStartTimeMilli; 431 final long mEventEndTimeMilli; 432 final long mInstanceId; 433 final boolean mAllDay; 434 435 RowInfo(int type, int julianDay, int position, long id, long startTime, long endTime, 436 long instanceId, boolean allDay) { 437 mType = type; 438 mDay = julianDay; 439 mPosition = position; 440 mEventId = id; 441 mEventStartTimeMilli = startTime; 442 mEventEndTimeMilli = endTime; 443 mFirstDayAfterYesterday = false; 444 mInstanceId = instanceId; 445 mAllDay = allDay; 446 } 447 448 RowInfo(int type, int julianDay) { 449 mType = type; 450 mDay = julianDay; 451 mPosition = 0; 452 mEventId = 0; 453 mEventStartTimeMilli = 0; 454 mEventEndTimeMilli = 0; 455 mFirstDayAfterYesterday = false; 456 mInstanceId = -1; 457 mAllDay = false; 458 } 459 } 460 461 private static class MultipleDayInfo { 462 final int mPosition; 463 final int mEndDay; 464 final long mEventId; 465 long mEventStartTimeMilli; 466 long mEventEndTimeMilli; 467 final long mInstanceId; 468 final boolean mAllDay; 469 470 MultipleDayInfo(int position, int endDay, long id, long startTime, long endTime, 471 long instanceId, boolean allDay) { 472 mPosition = position; 473 mEndDay = endDay; 474 mEventId = id; 475 mEventStartTimeMilli = startTime; 476 mEventEndTimeMilli = endTime; 477 mInstanceId = instanceId; 478 mAllDay = allDay; 479 } 480 } 481 482 /** 483 * Finds the position in the cursor of the event that best matches the time and Id. 484 * It will try to find the event that has the specified id and start time, if such event 485 * doesn't exist, it will return the event with a matching id that is closest to the start time. 486 * If the id doesn't exist, it will return the event with start time closest to the specified 487 * time. 488 * @param time - start of event in milliseconds (or any arbitrary time if event id is unknown) 489 * @param id - Event id (-1 if unknown). 490 * @return Position of event (if found) or position of nearest event according to the time. 491 * Zero if no event found 492 */ 493 public int findEventPositionNearestTime(Time time, long id) { 494 if (mRowInfo == null) { 495 return 0; 496 } 497 long millis = time.toMillis(false /* use isDst */); 498 long minDistance = Integer.MAX_VALUE; // some big number 499 long IdFoundMinDistance = Integer.MAX_VALUE; // some big number 500 int minIndex = 0; 501 int idFoundMinIndex = 0; 502 int eventInTimeIndex = -1; 503 int allDayEventInTimeIndex = -1; 504 int allDayEventDay = 0; 505 int minDay = 0; 506 boolean idFound = false; 507 int len = mRowInfo.size(); 508 509 // Loop through the events and find the best match 510 // 1. Event id and start time matches requested id and time 511 // 2. Event id matches and closest time 512 // 3. No event id match , time matches a all day event (midnight) 513 // 4. No event id match , time is between event start and end 514 // 5. No event id match , all day event 515 // 6. The closest event to the requested time 516 517 for (int index = 0; index < len; index++) { 518 RowInfo row = mRowInfo.get(index); 519 if (row.mType == TYPE_DAY) { 520 continue; 521 } 522 523 // Found exact match - done 524 if (row.mEventId == id) { 525 if (row.mEventStartTimeMilli == millis) { 526 return index; 527 } 528 529 // Not an exact match, Save event index if it is the closest to time so far 530 long distance = Math.abs(millis - row.mEventStartTimeMilli); 531 if (distance < minDistance) { 532 IdFoundMinDistance = distance; 533 idFoundMinIndex = index; 534 } 535 idFound = true; 536 } 537 if (!idFound) { 538 // Found an event that contains the requested time 539 if (millis >= row.mEventStartTimeMilli && millis <= row.mEventEndTimeMilli) { 540 if (row.mAllDay) { 541 if (allDayEventInTimeIndex == -1) { 542 allDayEventInTimeIndex = index; 543 allDayEventDay = row.mDay; 544 } 545 } else if (eventInTimeIndex == -1){ 546 eventInTimeIndex = index; 547 } 548 } else if (eventInTimeIndex == -1){ 549 // Save event index if it is the closest to time so far 550 long distance = Math.abs(millis - row.mEventStartTimeMilli); 551 if (distance < minDistance) { 552 minDistance = distance; 553 minIndex = index; 554 minDay = row.mDay; 555 } 556 } 557 } 558 } 559 // We didn't find an exact match so take the best matching event 560 // Closest event with the same id 561 if (idFound) { 562 return idFoundMinIndex; 563 } 564 // Event which occurs at the searched time 565 if (eventInTimeIndex != -1) { 566 return eventInTimeIndex; 567 // All day event which occurs at the same day of the searched time as long as there is 568 // no regular event at the same day 569 } else if (allDayEventInTimeIndex != -1 && minDay != allDayEventDay) { 570 return allDayEventInTimeIndex; 571 } 572 // Closest event 573 return minIndex; 574 } 575 576 577 /** 578 * Returns a flag indicating if this position is the first day after "yesterday" that has 579 * events in it. 580 * 581 * @return a flag indicating if this is the "first day after yesterday" 582 */ 583 public boolean isFirstDayAfterYesterday(int position) { 584 int headerPos = getHeaderPosition(position); 585 RowInfo row = mRowInfo.get(headerPos); 586 if (row != null) { 587 return row.mFirstDayAfterYesterday; 588 } 589 return false; 590 } 591 592 /** 593 * Finds the Julian day containing the event at the given position. 594 * 595 * @param position the list position of an event 596 * @return the Julian day containing that event 597 */ 598 public int findJulianDayFromPosition(int position) { 599 if (mRowInfo == null || position < 0) { 600 return 0; 601 } 602 603 int len = mRowInfo.size(); 604 if (position >= len) return 0; // no row info at this position 605 606 for (int index = position; index >= 0; index--) { 607 RowInfo row = mRowInfo.get(index); 608 if (row.mType == TYPE_DAY) { 609 return row.mDay; 610 } 611 } 612 return 0; 613 } 614 615 /** 616 * Marks the current row as the first day that has events after "yesterday". 617 * Used to mark the separation between the past and the present/future 618 * 619 * @param position in the adapter 620 */ 621 public void setAsFirstDayAfterYesterday(int position) { 622 if (mRowInfo == null || position < 0 || position > mRowInfo.size()) { 623 return; 624 } 625 RowInfo row = mRowInfo.get(position); 626 row.mFirstDayAfterYesterday = true; 627 } 628 629 /** 630 * Converts a list position to a cursor position. The list contains 631 * day headers as well as events. The cursor contains only events. 632 * 633 * @param listPos the list position of an event 634 * @return the corresponding cursor position of that event 635 * if the position point to day header , it will give the position of the next event 636 * negated. 637 */ 638 public int getCursorPosition(int listPos) { 639 if (mRowInfo != null && listPos >= 0) { 640 RowInfo row = mRowInfo.get(listPos); 641 if (row.mType == TYPE_MEETING) { 642 return row.mPosition; 643 } else { 644 int nextPos = listPos + 1; 645 if (nextPos < mRowInfo.size()) { 646 nextPos = getCursorPosition(nextPos); 647 if (nextPos >= 0) { 648 return -nextPos; 649 } 650 } 651 } 652 } 653 return Integer.MIN_VALUE; 654 } 655 656 @Override 657 public boolean areAllItemsEnabled() { 658 return false; 659 } 660 661 @Override 662 public boolean isEnabled(int position) { 663 if (mRowInfo != null && position < mRowInfo.size()) { 664 RowInfo row = mRowInfo.get(position); 665 return row.mType == TYPE_MEETING; 666 } 667 return true; 668 } 669 } 670