1/* 2 * Copyright (C) 2009 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.CalendarController; 20import com.android.calendar.CalendarController.EventType; 21import com.android.calendar.DeleteEventHelper; 22import com.android.calendar.R; 23import com.android.calendar.Utils; 24import com.android.calendar.agenda.AgendaAdapter.ViewHolder; 25import com.android.calendar.agenda.AgendaWindowAdapter.DayAdapterInfo; 26import com.android.calendar.agenda.AgendaWindowAdapter.EventInfo; 27 28import android.content.Context; 29import android.graphics.Rect; 30import android.os.Handler; 31import android.provider.CalendarContract.Attendees; 32import android.text.format.Time; 33import android.util.AttributeSet; 34import android.util.Log; 35import android.view.View; 36import android.widget.AdapterView; 37import android.widget.AdapterView.OnItemClickListener; 38import android.widget.ListView; 39import android.widget.TextView; 40 41public class AgendaListView extends ListView implements OnItemClickListener { 42 43 private static final String TAG = "AgendaListView"; 44 private static final boolean DEBUG = false; 45 private static final int EVENT_UPDATE_TIME = 300000; // 5 minutes 46 47 private AgendaWindowAdapter mWindowAdapter; 48 private DeleteEventHelper mDeleteEventHelper; 49 private Context mContext; 50 private String mTimeZone; 51 private Time mTime; 52 private boolean mShowEventDetailsWithAgenda; 53 private Handler mHandler = null; 54 55 private final Runnable mTZUpdater = new Runnable() { 56 @Override 57 public void run() { 58 mTimeZone = Utils.getTimeZone(mContext, this); 59 mTime.switchTimezone(mTimeZone); 60 } 61 }; 62 63 // runs every midnight and refreshes the view in order to update the past/present 64 // separator 65 private final Runnable mMidnightUpdater = new Runnable() { 66 @Override 67 public void run() { 68 refresh(true); 69 Utils.setMidnightUpdater(mHandler, mMidnightUpdater, mTimeZone); 70 } 71 }; 72 73 // Runs every EVENT_UPDATE_TIME to gray out past events 74 private final Runnable mPastEventUpdater = new Runnable() { 75 @Override 76 public void run() { 77 if (updatePastEvents() == true) { 78 refresh(true); 79 } 80 setPastEventsUpdater(); 81 } 82 }; 83 84 public AgendaListView(Context context, AttributeSet attrs) { 85 super(context, attrs); 86 initView(context); 87 } 88 89 private void initView(Context context) { 90 mContext = context; 91 mTimeZone = Utils.getTimeZone(context, mTZUpdater); 92 mTime = new Time(mTimeZone); 93 setOnItemClickListener(this); 94 setVerticalScrollBarEnabled(false); 95 mWindowAdapter = new AgendaWindowAdapter(context, this, 96 Utils.getConfigBool(context, R.bool.show_event_details_with_agenda)); 97 mWindowAdapter.setSelectedInstanceId(-1/* TODO:instanceId */); 98 setAdapter(mWindowAdapter); 99 setCacheColorHint(context.getResources().getColor(R.color.agenda_item_not_selected)); 100 mDeleteEventHelper = 101 new DeleteEventHelper(context, null, false /* don't exit when done */); 102 mShowEventDetailsWithAgenda = Utils.getConfigBool(mContext, 103 R.bool.show_event_details_with_agenda); 104 // Hide ListView dividers, they are done in the item views themselves 105 setDivider(null); 106 setDividerHeight(0); 107 108 mHandler = new Handler(); 109 } 110 111 // Sets a thread to run every EVENT_UPDATE_TIME in order to update the list 112 // with grayed out past events 113 private void setPastEventsUpdater() { 114 115 // Run the thread in the nearest rounded EVENT_UPDATE_TIME 116 long now = System.currentTimeMillis(); 117 long roundedTime = (now / EVENT_UPDATE_TIME) * EVENT_UPDATE_TIME; 118 mHandler.removeCallbacks(mPastEventUpdater); 119 mHandler.postDelayed(mPastEventUpdater, EVENT_UPDATE_TIME - (now - roundedTime)); 120 } 121 122 // Stop the past events thread 123 private void resetPastEventsUpdater() { 124 mHandler.removeCallbacks(mPastEventUpdater); 125 } 126 127 // Go over all visible views and checks if all past events are grayed out. 128 // Returns true is there is at least one event that ended and it is not 129 // grayed out. 130 private boolean updatePastEvents() { 131 132 int childCount = getChildCount(); 133 boolean needUpdate = false; 134 long now = System.currentTimeMillis(); 135 Time time = new Time(mTimeZone); 136 time.set(now); 137 int todayJulianDay = Time.getJulianDay(now, time.gmtoff); 138 139 // Go over views in list 140 for (int i = 0; i < childCount; ++i) { 141 View listItem = getChildAt(i); 142 Object o = listItem.getTag(); 143 if (o instanceof AgendaByDayAdapter.ViewHolder) { 144 // day view - check if day in the past and not grayed yet 145 AgendaByDayAdapter.ViewHolder holder = (AgendaByDayAdapter.ViewHolder) o; 146 if (holder.julianDay <= todayJulianDay && !holder.grayed) { 147 needUpdate = true; 148 break; 149 } 150 } else if (o instanceof AgendaAdapter.ViewHolder) { 151 // meeting view - check if event in the past or started already and not grayed yet 152 // All day meetings for a day are grayed out 153 AgendaAdapter.ViewHolder holder = (AgendaAdapter.ViewHolder) o; 154 if (!holder.grayed && ((!holder.allDay && holder.startTimeMilli <= now) || 155 (holder.allDay && holder.julianDay <= todayJulianDay))) { 156 needUpdate = true; 157 break; 158 } 159 } 160 } 161 return needUpdate; 162 } 163 164 @Override 165 protected void onDetachedFromWindow() { 166 super.onDetachedFromWindow(); 167 mWindowAdapter.close(); 168 } 169 170 // Implementation of the interface OnItemClickListener 171 @Override 172 public void onItemClick(AdapterView<?> a, View v, int position, long id) { 173 if (id != -1) { 174 // Switch to the EventInfo view 175 EventInfo event = mWindowAdapter.getEventByPosition(position); 176 long oldInstanceId = mWindowAdapter.getSelectedInstanceId(); 177 mWindowAdapter.setSelectedView(v); 178 179 // If events are shown to the side of the agenda list , do nothing 180 // when the same event is selected , otherwise show the selected event. 181 182 if (event != null && (oldInstanceId != mWindowAdapter.getSelectedInstanceId() || 183 !mShowEventDetailsWithAgenda)) { 184 long startTime = event.begin; 185 long endTime = event.end; 186 // Holder in view holds the start of the specific part of a multi-day event , 187 // use it for the goto 188 long holderStartTime; 189 Object holder = v.getTag(); 190 if (holder instanceof AgendaAdapter.ViewHolder) { 191 holderStartTime = ((AgendaAdapter.ViewHolder) holder).startTimeMilli; 192 } else { 193 holderStartTime = startTime; 194 } 195 if (event.allDay) { 196 startTime = Utils.convertAlldayLocalToUTC(mTime, startTime, mTimeZone); 197 endTime = Utils.convertAlldayLocalToUTC(mTime, endTime, mTimeZone); 198 } 199 mTime.set(startTime); 200 CalendarController controller = CalendarController.getInstance(mContext); 201 controller.sendEventRelatedEventWithExtra(this, EventType.VIEW_EVENT, event.id, 202 startTime, endTime, 0, 0, CalendarController.EventInfo.buildViewExtraLong( 203 Attendees.ATTENDEE_STATUS_NONE, event.allDay), holderStartTime); 204 } 205 } 206 } 207 208 public void goTo(Time time, long id, String searchQuery, boolean forced, 209 boolean refreshEventInfo) { 210 if (time == null) { 211 time = mTime; 212 long goToTime = getFirstVisibleTime(null); 213 if (goToTime <= 0) { 214 goToTime = System.currentTimeMillis(); 215 } 216 time.set(goToTime); 217 } 218 mTime.set(time); 219 mTime.switchTimezone(mTimeZone); 220 mTime.normalize(true); 221 if (DEBUG) { 222 Log.d(TAG, "Goto with time " + mTime.toString()); 223 } 224 mWindowAdapter.refresh(mTime, id, searchQuery, forced, refreshEventInfo); 225 } 226 227 public void refresh(boolean forced) { 228 mWindowAdapter.refresh(mTime, -1, null, forced, false); 229 } 230 231 public void deleteSelectedEvent() { 232 int position = getSelectedItemPosition(); 233 EventInfo event = mWindowAdapter.getEventByPosition(position); 234 if (event != null) { 235 mDeleteEventHelper.delete(event.begin, event.end, event.id, -1); 236 } 237 } 238 239 public View getFirstVisibleView() { 240 Rect r = new Rect(); 241 int childCount = getChildCount(); 242 for (int i = 0; i < childCount; ++i) { 243 View listItem = getChildAt(i); 244 listItem.getLocalVisibleRect(r); 245 if (r.top >= 0) { // if visible 246 return listItem; 247 } 248 } 249 return null; 250 } 251 252 public long getSelectedTime() { 253 int position = getSelectedItemPosition(); 254 if (position >= 0) { 255 EventInfo event = mWindowAdapter.getEventByPosition(position); 256 if (event != null) { 257 return event.begin; 258 } 259 } 260 return getFirstVisibleTime(null); 261 } 262 263 public AgendaAdapter.ViewHolder getSelectedViewHolder() { 264 return mWindowAdapter.getSelectedViewHolder(); 265 } 266 267 public long getFirstVisibleTime(EventInfo e) { 268 EventInfo event = e; 269 if (e == null) { 270 event = getFirstVisibleEvent(); 271 } 272 if (event != null) { 273 Time t = new Time(mTimeZone); 274 t.set(event.begin); 275 // Save and restore the time since setJulianDay sets the time to 00:00:00 276 int hour = t.hour; 277 int minute = t.minute; 278 int second = t.second; 279 t.setJulianDay(event.startDay); 280 t.hour = hour; 281 t.minute = minute; 282 t.second = second; 283 if (DEBUG) { 284 t.normalize(true); 285 Log.d(TAG, "first position had time " + t.toString()); 286 } 287 return t.normalize(false); 288 } 289 return 0; 290 } 291 292 public EventInfo getFirstVisibleEvent() { 293 int position = getFirstVisiblePosition(); 294 if (DEBUG) { 295 Log.v(TAG, "getFirstVisiblePosition = " + position); 296 } 297 298 // mShowEventDetailsWithAgenda == true implies we have a sticky header. In that case 299 // we may need to take the second visible position, since the first one maybe the one 300 // under the sticky header. 301 if (mShowEventDetailsWithAgenda) { 302 View v = getFirstVisibleView (); 303 if (v != null) { 304 Rect r = new Rect (); 305 v.getLocalVisibleRect(r); 306 if (r.bottom - r.top <= mWindowAdapter.getStickyHeaderHeight()) { 307 position ++; 308 } 309 } 310 } 311 312 return mWindowAdapter.getEventByPosition(position, 313 false /* startDay = date separator date instead of actual event startday */); 314 315 } 316 317 public int getJulianDayFromPosition(int position) { 318 DayAdapterInfo info = mWindowAdapter.getAdapterInfoByPosition(position); 319 if (info != null) { 320 return info.dayAdapter.findJulianDayFromPosition(position - info.offset); 321 } 322 return 0; 323 } 324 325 // Finds is a specific event (defined by start time and id) is visible 326 public boolean isEventVisible(Time startTime, long id) { 327 328 if (id == -1 || startTime == null) { 329 return false; 330 } 331 332 View child = getChildAt(0); 333 // View not set yet, so not child - return 334 if (child == null) { 335 return false; 336 } 337 int start = getPositionForView(child); 338 long milliTime = startTime.toMillis(true); 339 int childCount = getChildCount(); 340 int eventsInAdapter = mWindowAdapter.getCount(); 341 342 for (int i = 0; i < childCount; i++) { 343 if (i + start >= eventsInAdapter) { 344 break; 345 } 346 EventInfo event = mWindowAdapter.getEventByPosition(i + start); 347 if (event == null) { 348 continue; 349 } 350 if (event.id == id && event.begin == milliTime) { 351 View listItem = getChildAt(i); 352 if (listItem.getTop() <= getHeight() && 353 listItem.getTop() >= mWindowAdapter.getStickyHeaderHeight()) { 354 return true; 355 } 356 } 357 } 358 return false; 359 } 360 361 public long getSelectedInstanceId() { 362 return mWindowAdapter.getSelectedInstanceId(); 363 } 364 365 public void setSelectedInstanceId(long id) { 366 mWindowAdapter.setSelectedInstanceId(id); 367 } 368 369 // Move the currently selected or visible focus down by offset amount. 370 // offset could be negative. 371 public void shiftSelection(int offset) { 372 shiftPosition(offset); 373 int position = getSelectedItemPosition(); 374 if (position != INVALID_POSITION) { 375 setSelectionFromTop(position + offset, 0); 376 } 377 } 378 379 private void shiftPosition(int offset) { 380 if (DEBUG) { 381 Log.v(TAG, "Shifting position " + offset); 382 } 383 384 View firstVisibleItem = getFirstVisibleView(); 385 386 if (firstVisibleItem != null) { 387 Rect r = new Rect(); 388 firstVisibleItem.getLocalVisibleRect(r); 389 // if r.top is < 0, getChildAt(0) and getFirstVisiblePosition() is 390 // returning an item above the first visible item. 391 int position = getPositionForView(firstVisibleItem); 392 setSelectionFromTop(position + offset, r.top > 0 ? -r.top : r.top); 393 if (DEBUG) { 394 if (firstVisibleItem.getTag() instanceof AgendaAdapter.ViewHolder) { 395 ViewHolder viewHolder = (AgendaAdapter.ViewHolder) firstVisibleItem.getTag(); 396 Log.v(TAG, "Shifting from " + position + " by " + offset + ". Title " 397 + viewHolder.title.getText()); 398 } else if (firstVisibleItem.getTag() instanceof AgendaByDayAdapter.ViewHolder) { 399 AgendaByDayAdapter.ViewHolder viewHolder = 400 (AgendaByDayAdapter.ViewHolder) firstVisibleItem.getTag(); 401 Log.v(TAG, "Shifting from " + position + " by " + offset + ". Date " 402 + viewHolder.dateView.getText()); 403 } else if (firstVisibleItem instanceof TextView) { 404 Log.v(TAG, "Shifting: Looking at header here. " + getSelectedItemPosition()); 405 } 406 } 407 } else if (getSelectedItemPosition() >= 0) { 408 if (DEBUG) { 409 Log.v(TAG, "Shifting selection from " + getSelectedItemPosition() + 410 " by " + offset); 411 } 412 setSelection(getSelectedItemPosition() + offset); 413 } 414 } 415 416 public void setHideDeclinedEvents(boolean hideDeclined) { 417 mWindowAdapter.setHideDeclinedEvents(hideDeclined); 418 } 419 420 public void onResume() { 421 mTZUpdater.run(); 422 Utils.setMidnightUpdater(mHandler, mMidnightUpdater, mTimeZone); 423 setPastEventsUpdater(); 424 mWindowAdapter.onResume(); 425 } 426 427 public void onPause() { 428 Utils.resetMidnightUpdater(mHandler, mMidnightUpdater); 429 resetPastEventsUpdater(); 430 } 431} 432