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