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; 18 19 import android.content.Context; 20 import android.database.Cursor; 21 import android.text.format.DateUtils; 22 import android.text.format.Time; 23 import android.view.LayoutInflater; 24 import android.view.View; 25 import android.view.ViewGroup; 26 import android.widget.BaseAdapter; 27 import android.widget.TextView; 28 29 import com.android.calendar.AgendaWindowAdapter.DayAdapterInfo; 30 31 import java.util.ArrayList; 32 import java.util.Formatter; 33 import java.util.Iterator; 34 import java.util.LinkedList; 35 import java.util.Locale; 36 37 public class AgendaByDayAdapter extends BaseAdapter { 38 private static final int TYPE_DAY = 0; 39 private static final int TYPE_MEETING = 1; 40 static final int TYPE_LAST = 2; 41 42 private final Context mContext; 43 private final AgendaAdapter mAgendaAdapter; 44 private final LayoutInflater mInflater; 45 private ArrayList<RowInfo> mRowInfo; 46 private int mTodayJulianDay; 47 private Time mTmpTime = new Time(); 48 // Note: Formatter is not thread safe. Fine for now as it is only used by the main thread. 49 private Formatter mFormatter; 50 private StringBuilder mStringBuilder; 51 52 static class ViewHolder { 53 TextView dateView; 54 } 55 56 public AgendaByDayAdapter(Context context) { 57 mContext = context; 58 mAgendaAdapter = new AgendaAdapter(context, R.layout.agenda_item); 59 mInflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 60 mStringBuilder = new StringBuilder(50); 61 mFormatter = new Formatter(mStringBuilder, Locale.getDefault()); 62 } 63 64 public int getCount() { 65 if (mRowInfo != null) { 66 return mRowInfo.size(); 67 } 68 return mAgendaAdapter.getCount(); 69 } 70 71 public Object getItem(int position) { 72 if (mRowInfo != null) { 73 RowInfo row = mRowInfo.get(position); 74 if (row.mType == TYPE_DAY) { 75 return row; 76 } else { 77 return mAgendaAdapter.getItem(row.mData); 78 } 79 } 80 return mAgendaAdapter.getItem(position); 81 } 82 83 public long getItemId(int position) { 84 if (mRowInfo != null) { 85 RowInfo row = mRowInfo.get(position); 86 if (row.mType == TYPE_DAY) { 87 return -position; 88 } else { 89 return mAgendaAdapter.getItemId(row.mData); 90 } 91 } 92 return mAgendaAdapter.getItemId(position); 93 } 94 95 @Override 96 public int getViewTypeCount() { 97 return TYPE_LAST; 98 } 99 100 @Override 101 public int getItemViewType(int position) { 102 return mRowInfo != null && mRowInfo.size() > position ? 103 mRowInfo.get(position).mType : TYPE_DAY; 104 } 105 106 public View getView(int position, View convertView, ViewGroup parent) { 107 if ((mRowInfo == null) || (position > mRowInfo.size())) { 108 // If we have no row info, mAgendaAdapter returns the view. 109 return mAgendaAdapter.getView(position, convertView, parent); 110 } 111 112 RowInfo row = mRowInfo.get(position); 113 if (row.mType == TYPE_DAY) { 114 ViewHolder holder = null; 115 View agendaDayView = null; 116 if ((convertView != null) && (convertView.getTag() != null)) { 117 // Listview may get confused and pass in a different type of 118 // view since we keep shifting data around. Not a big problem. 119 Object tag = convertView.getTag(); 120 if (tag instanceof ViewHolder) { 121 agendaDayView = convertView; 122 holder = (ViewHolder) tag; 123 } 124 } 125 126 if (holder == null) { 127 // Create a new AgendaView with a ViewHolder for fast access to 128 // views w/o calling findViewById() 129 holder = new ViewHolder(); 130 agendaDayView = mInflater.inflate(R.layout.agenda_day, parent, false); 131 holder.dateView = (TextView) agendaDayView.findViewById(R.id.date); 132 agendaDayView.setTag(holder); 133 } 134 135 // Re-use the member variable "mTime" which is set to the local timezone. 136 Time date = mTmpTime; 137 long millis = date.setJulianDay(row.mData); 138 int flags = DateUtils.FORMAT_SHOW_YEAR 139 | DateUtils.FORMAT_SHOW_DATE; 140 141 mStringBuilder.setLength(0); 142 String dateViewText; 143 if (row.mData == mTodayJulianDay) { 144 dateViewText = mContext.getString(R.string.agenda_today, DateUtils.formatDateRange( 145 mContext, mFormatter, millis, millis, flags).toString()); 146 } else { 147 flags |= DateUtils.FORMAT_SHOW_WEEKDAY; 148 dateViewText = DateUtils.formatDateRange(mContext, mFormatter, millis, millis, 149 flags).toString(); 150 } 151 152 if (AgendaWindowAdapter.BASICLOG) { 153 dateViewText += " P:" + position; 154 } 155 holder.dateView.setText(dateViewText); 156 157 return agendaDayView; 158 } else if (row.mType == TYPE_MEETING) { 159 View x = mAgendaAdapter.getView(row.mData, convertView, parent); 160 TextView y = ((AgendaAdapter.ViewHolder) x.getTag()).title; 161 if (AgendaWindowAdapter.BASICLOG) { 162 y.setText(y.getText() + " P:" + position); 163 } else { 164 y.setText(y.getText()); 165 } 166 return x; 167 } else { 168 // Error 169 throw new IllegalStateException("Unknown event type:" + row.mType); 170 } 171 } 172 173 public void clearDayHeaderInfo() { 174 mRowInfo = null; 175 } 176 177 public void changeCursor(DayAdapterInfo info) { 178 calculateDays(info); 179 mAgendaAdapter.changeCursor(info.cursor); 180 } 181 182 public void calculateDays(DayAdapterInfo dayAdapterInfo) { 183 Cursor cursor = dayAdapterInfo.cursor; 184 ArrayList<RowInfo> rowInfo = new ArrayList<RowInfo>(); 185 int prevStartDay = -1; 186 Time time = new Time(); 187 long now = System.currentTimeMillis(); 188 time.set(now); 189 mTodayJulianDay = Time.getJulianDay(now, time.gmtoff); 190 LinkedList<MultipleDayInfo> multipleDayList = new LinkedList<MultipleDayInfo>(); 191 for (int position = 0; cursor.moveToNext(); position++) { 192 boolean allDay = cursor.getInt(AgendaWindowAdapter.INDEX_ALL_DAY) != 0; 193 int startDay = cursor.getInt(AgendaWindowAdapter.INDEX_START_DAY); 194 195 // Skip over the days outside of the adapter's range 196 startDay = Math.max(startDay, dayAdapterInfo.start); 197 198 if (startDay != prevStartDay) { 199 // Check if we skipped over any empty days 200 if (prevStartDay == -1) { 201 rowInfo.add(new RowInfo(TYPE_DAY, startDay)); 202 } else { 203 // If there are any multiple-day events that span the empty 204 // range of days, then create day headers and events for 205 // those multiple-day events. 206 boolean dayHeaderAdded = false; 207 for (int currentDay = prevStartDay + 1; currentDay <= startDay; currentDay++) { 208 dayHeaderAdded = false; 209 Iterator<MultipleDayInfo> iter = multipleDayList.iterator(); 210 while (iter.hasNext()) { 211 MultipleDayInfo info = iter.next(); 212 // If this event has ended then remove it from the 213 // list. 214 if (info.mEndDay < currentDay) { 215 iter.remove(); 216 continue; 217 } 218 219 // If this is the first event for the day, then 220 // insert a day header. 221 if (!dayHeaderAdded) { 222 rowInfo.add(new RowInfo(TYPE_DAY, currentDay)); 223 dayHeaderAdded = true; 224 } 225 rowInfo.add(new RowInfo(TYPE_MEETING, info.mPosition)); 226 } 227 } 228 229 // If the day header was not added for the start day, then 230 // add it now. 231 if (!dayHeaderAdded) { 232 rowInfo.add(new RowInfo(TYPE_DAY, startDay)); 233 } 234 } 235 prevStartDay = startDay; 236 } 237 238 // Add in the event for this cursor position 239 rowInfo.add(new RowInfo(TYPE_MEETING, position)); 240 241 // If this event spans multiple days, then add it to the multipleDay 242 // list. 243 int endDay = cursor.getInt(AgendaWindowAdapter.INDEX_END_DAY); 244 245 // Skip over the days outside of the adapter's range 246 endDay = Math.min(endDay, dayAdapterInfo.end); 247 if (endDay > startDay) { 248 multipleDayList.add(new MultipleDayInfo(position, endDay)); 249 } 250 } 251 252 // There are no more cursor events but we might still have multiple-day 253 // events left. So create day headers and events for those. 254 if (prevStartDay > 0) { 255 for (int currentDay = prevStartDay + 1; currentDay <= dayAdapterInfo.end; 256 currentDay++) { 257 boolean dayHeaderAdded = false; 258 Iterator<MultipleDayInfo> iter = multipleDayList.iterator(); 259 while (iter.hasNext()) { 260 MultipleDayInfo info = iter.next(); 261 // If this event has ended then remove it from the 262 // list. 263 if (info.mEndDay < currentDay) { 264 iter.remove(); 265 continue; 266 } 267 268 // If this is the first event for the day, then 269 // insert a day header. 270 if (!dayHeaderAdded) { 271 rowInfo.add(new RowInfo(TYPE_DAY, currentDay)); 272 dayHeaderAdded = true; 273 } 274 rowInfo.add(new RowInfo(TYPE_MEETING, info.mPosition)); 275 } 276 } 277 } 278 mRowInfo = rowInfo; 279 } 280 281 private static class RowInfo { 282 // mType is either a day header (TYPE_DAY) or an event (TYPE_MEETING) 283 final int mType; 284 285 // If mType is TYPE_DAY, then mData is the Julian day. Otherwise 286 // mType is TYPE_MEETING and mData is the cursor position. 287 final int mData; 288 289 RowInfo(int type, int data) { 290 mType = type; 291 mData = data; 292 } 293 } 294 295 private static class MultipleDayInfo { 296 final int mPosition; 297 final int mEndDay; 298 299 MultipleDayInfo(int position, int endDay) { 300 mPosition = position; 301 mEndDay = endDay; 302 } 303 } 304 305 /** 306 * Searches for the day that matches the given Time object and returns the 307 * list position of that day. If there are no events for that day, then it 308 * finds the nearest day (before or after) that has events and returns the 309 * list position for that day. 310 * 311 * @param time the date to search for 312 * @return the cursor position of the first event for that date, or zero 313 * if no match was found 314 */ 315 public int findDayPositionNearestTime(Time time) { 316 if (mRowInfo == null) { 317 return 0; 318 } 319 long millis = time.toMillis(false /* use isDst */); 320 int julianDay = Time.getJulianDay(millis, time.gmtoff); 321 int minDistance = 1000; // some big number 322 int minIndex = 0; 323 int len = mRowInfo.size(); 324 for (int index = 0; index < len; index++) { 325 RowInfo row = mRowInfo.get(index); 326 if (row.mType == TYPE_DAY) { 327 int distance = Math.abs(julianDay - row.mData); 328 if (distance == 0) { 329 return index; 330 } 331 if (distance < minDistance) { 332 minDistance = distance; 333 minIndex = index; 334 } 335 } 336 } 337 338 // We didn't find an exact match so take the nearest day that had 339 // events. 340 return minIndex; 341 } 342 343 /** 344 * Finds the Julian day containing the event at the given position. 345 * 346 * @param position the list position of an event 347 * @return the Julian day containing that event 348 */ 349 public int findJulianDayFromPosition(int position) { 350 if (mRowInfo == null || position < 0) { 351 return 0; 352 } 353 354 int len = mRowInfo.size(); 355 if (position >= len) return 0; // no row info at this position 356 357 for (int index = position; index >= 0; index--) { 358 RowInfo row = mRowInfo.get(index); 359 if (row.mType == TYPE_DAY) { 360 return row.mData; 361 } 362 } 363 return 0; 364 } 365 366 /** 367 * Converts a list position to a cursor position. The list contains 368 * day headers as well as events. The cursor contains only events. 369 * 370 * @param listPos the list position of an event 371 * @return the corresponding cursor position of that event 372 */ 373 public int getCursorPosition(int listPos) { 374 if (mRowInfo != null && listPos >= 0) { 375 RowInfo row = mRowInfo.get(listPos); 376 if (row.mType == TYPE_MEETING) { 377 return row.mData; 378 } else { 379 int nextPos = listPos + 1; 380 if (nextPos < mRowInfo.size()) { 381 nextPos = getCursorPosition(nextPos); 382 if (nextPos >= 0) { 383 return -nextPos; 384 } 385 } 386 } 387 } 388 return Integer.MIN_VALUE; 389 } 390 391 @Override 392 public boolean areAllItemsEnabled() { 393 return false; 394 } 395 396 @Override 397 public boolean isEnabled(int position) { 398 if (mRowInfo != null && position < mRowInfo.size()) { 399 RowInfo row = mRowInfo.get(position); 400 return row.mType == TYPE_MEETING; 401 } 402 return true; 403 } 404 } 405