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 AgendaByDayAdapter(Context context)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 getInstanceId(int position)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 getHeaderPosition(int position)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 getHeaderItemsCount(int position)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 getCount()120 public int getCount() { 121 if (mRowInfo != null) { 122 return mRowInfo.size(); 123 } 124 return mAgendaAdapter.getCount(); 125 } 126 getItem(int position)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 getItemId(int position)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 getViewTypeCount()152 public int getViewTypeCount() { 153 return TYPE_LAST; 154 } 155 156 @Override getItemViewType(int position)157 public int getItemViewType(int position) { 158 return mRowInfo != null && mRowInfo.size() > position ? 159 mRowInfo.get(position).mType : TYPE_DAY; 160 } 161 isDayHeaderView(int position)162 public boolean isDayHeaderView(int position) { 163 return (getItemViewType(position) == TYPE_DAY); 164 } 165 getView(int position, View convertView, ViewGroup parent)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 clearDayHeaderInfo()278 public void clearDayHeaderInfo() { 279 mRowInfo = null; 280 } 281 changeCursor(DayAdapterInfo info)282 public void changeCursor(DayAdapterInfo info) { 283 calculateDays(info); 284 mAgendaAdapter.changeCursor(info.cursor); 285 } 286 calculateDays(DayAdapterInfo dayAdapterInfo)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 RowInfo(int type, int julianDay, int position, long id, long startTime, long endTime, long instanceId, boolean allDay)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 RowInfo(int type, int julianDay)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 MultipleDayInfo(int position, int endDay, long id, long startTime, long endTime, long instanceId, boolean allDay)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 */ findEventPositionNearestTime(Time time, long id)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 */ isFirstDayAfterYesterday(int position)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 */ findJulianDayFromPosition(int position)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 */ setAsFirstDayAfterYesterday(int position)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 */ getCursorPosition(int listPos)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 areAllItemsEnabled()657 public boolean areAllItemsEnabled() { 658 return false; 659 } 660 661 @Override isEnabled(int position)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