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 17 package com.android.calendar.agenda; 18 19 import com.android.calendar.CalendarController; 20 import com.android.calendar.CalendarController.EventType; 21 import com.android.calendar.DeleteEventHelper; 22 import com.android.calendar.R; 23 import com.android.calendar.Utils; 24 import com.android.calendar.agenda.AgendaAdapter.ViewHolder; 25 import com.android.calendar.agenda.AgendaWindowAdapter.DayAdapterInfo; 26 import com.android.calendar.agenda.AgendaWindowAdapter.EventInfo; 27 28 import android.content.Context; 29 import android.graphics.Rect; 30 import android.os.Handler; 31 import android.provider.CalendarContract.Attendees; 32 import android.text.format.Time; 33 import android.util.AttributeSet; 34 import android.util.Log; 35 import android.view.View; 36 import android.widget.AdapterView; 37 import android.widget.AdapterView.OnItemClickListener; 38 import android.widget.ListView; 39 import android.widget.TextView; 40 41 public 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 181 // event is selected , otherwise show the selected event. 182 183 if (event != null && (oldInstanceId != mWindowAdapter.getSelectedInstanceId() || 184 !mShowEventDetailsWithAgenda)) { 185 long startTime = event.begin; 186 long endTime = event.end; 187 if (event.allDay) { 188 startTime = Utils.convertAlldayLocalToUTC(mTime, startTime, mTimeZone); 189 endTime = Utils.convertAlldayLocalToUTC(mTime, endTime, mTimeZone); 190 } 191 mTime.set(startTime); 192 CalendarController controller = CalendarController.getInstance(mContext); 193 controller.sendEventRelatedEventWithExtra(this, EventType.VIEW_EVENT, event.id, 194 startTime, endTime, 0, 0, CalendarController.EventInfo.buildViewExtraLong( 195 Attendees.ATTENDEE_STATUS_NONE, event.allDay), 196 controller.getTime()); 197 } 198 } 199 } 200 201 public void goTo(Time time, long id, String searchQuery, boolean forced, 202 boolean refreshEventInfo) { 203 if (time == null) { 204 time = mTime; 205 long goToTime = getFirstVisibleTime(); 206 if (goToTime <= 0) { 207 goToTime = System.currentTimeMillis(); 208 } 209 time.set(goToTime); 210 } 211 mTime.set(time); 212 mTime.switchTimezone(mTimeZone); 213 mTime.normalize(true); 214 if (DEBUG) { 215 Log.d(TAG, "Goto with time " + mTime.toString()); 216 } 217 mWindowAdapter.refresh(mTime, id, searchQuery, forced, refreshEventInfo); 218 } 219 220 public void refresh(boolean forced) { 221 mWindowAdapter.refresh(mTime, -1, null, forced, false); 222 } 223 224 public void deleteSelectedEvent() { 225 int position = getSelectedItemPosition(); 226 EventInfo event = mWindowAdapter.getEventByPosition(position); 227 if (event != null) { 228 mDeleteEventHelper.delete(event.begin, event.end, event.id, -1); 229 } 230 } 231 232 public View getFirstVisibleView() { 233 Rect r = new Rect(); 234 int childCount = getChildCount(); 235 for (int i = 0; i < childCount; ++i) { 236 View listItem = getChildAt(i); 237 listItem.getLocalVisibleRect(r); 238 if (r.top >= 0) { // if visible 239 return listItem; 240 } 241 } 242 return null; 243 } 244 245 public long getSelectedTime() { 246 int position = getSelectedItemPosition(); 247 if (position >= 0) { 248 EventInfo event = mWindowAdapter.getEventByPosition(position); 249 if (event != null) { 250 return event.begin; 251 } 252 } 253 return getFirstVisibleTime(); 254 } 255 256 public AgendaAdapter.ViewHolder getSelectedViewHolder() { 257 return mWindowAdapter.getSelectedViewHolder(); 258 } 259 260 public long getFirstVisibleTime() { 261 int position = getFirstVisiblePosition(); 262 if (DEBUG) { 263 Log.v(TAG, "getFirstVisiblePosition = " + position); 264 } 265 266 // mShowEventDetailsWithAgenda == true implies we have a sticky header. In that case 267 // we may need to take the second visible position, since the first one maybe the one 268 // under the sticky header. 269 if (mShowEventDetailsWithAgenda) { 270 View v = getFirstVisibleView (); 271 if (v != null) { 272 Rect r = new Rect (); 273 v.getLocalVisibleRect(r); 274 if (r.bottom - r.top <= mWindowAdapter.getStickyHeaderHeight()) { 275 position ++; 276 } 277 } 278 } 279 280 EventInfo event = mWindowAdapter.getEventByPosition(position, 281 false /* startDay = date separator date instead of actual event startday */); 282 if (event != null) { 283 Time t = new Time(mTimeZone); 284 t.set(event.begin); 285 // Save and restore the time since setJulianDay sets the time to 00:00:00 286 int hour = t.hour; 287 int minute = t.minute; 288 int second = t.second; 289 t.setJulianDay(event.startDay); 290 t.hour = hour; 291 t.minute = minute; 292 t.second = second; 293 if (DEBUG) { 294 t.normalize(true); 295 Log.d(TAG, "position " + position + " had time " + t.toString()); 296 } 297 return t.normalize(false); 298 } 299 return 0; 300 } 301 302 public int getJulianDayFromPosition(int position) { 303 DayAdapterInfo info = mWindowAdapter.getAdapterInfoByPosition(position); 304 if (info != null) { 305 return info.dayAdapter.findJulianDayFromPosition(position - info.offset); 306 } 307 return 0; 308 } 309 310 // Finds is a specific event (defined by start time and id) is visible 311 public boolean isEventVisible(Time startTime, long id) { 312 313 if (id == -1 || startTime == null) { 314 return false; 315 } 316 317 View child = getChildAt(0); 318 // View not set yet, so not child - return 319 if (child == null) { 320 return false; 321 } 322 int start = getPositionForView(child); 323 long milliTime = startTime.toMillis(true); 324 int childCount = getChildCount(); 325 int eventsInAdapter = mWindowAdapter.getCount(); 326 327 for (int i = 0; i < childCount; i++) { 328 if (i + start >= eventsInAdapter) { 329 break; 330 } 331 EventInfo event = mWindowAdapter.getEventByPosition(i + start); 332 if (event == null) { 333 continue; 334 } 335 if (event.id == id && event.begin == milliTime) { 336 View listItem = getChildAt(i); 337 if (listItem.getTop() <= getHeight() && 338 listItem.getTop() >= mWindowAdapter.getStickyHeaderHeight()) { 339 return true; 340 } 341 } 342 } 343 return false; 344 } 345 346 public long getSelectedInstanceId() { 347 return mWindowAdapter.getSelectedInstanceId(); 348 } 349 350 public void setSelectedInstanceId(long id) { 351 mWindowAdapter.setSelectedInstanceId(id); 352 } 353 354 // Move the currently selected or visible focus down by offset amount. 355 // offset could be negative. 356 public void shiftSelection(int offset) { 357 shiftPosition(offset); 358 int position = getSelectedItemPosition(); 359 if (position != INVALID_POSITION) { 360 setSelectionFromTop(position + offset, 0); 361 } 362 } 363 364 private void shiftPosition(int offset) { 365 if (DEBUG) { 366 Log.v(TAG, "Shifting position " + offset); 367 } 368 369 View firstVisibleItem = getFirstVisibleView(); 370 371 if (firstVisibleItem != null) { 372 Rect r = new Rect(); 373 firstVisibleItem.getLocalVisibleRect(r); 374 // if r.top is < 0, getChildAt(0) and getFirstVisiblePosition() is 375 // returning an item above the first visible item. 376 int position = getPositionForView(firstVisibleItem); 377 setSelectionFromTop(position + offset, r.top > 0 ? -r.top : r.top); 378 if (DEBUG) { 379 if (firstVisibleItem.getTag() instanceof AgendaAdapter.ViewHolder) { 380 ViewHolder viewHolder = (AgendaAdapter.ViewHolder) firstVisibleItem.getTag(); 381 Log.v(TAG, "Shifting from " + position + " by " + offset + ". Title " 382 + viewHolder.title.getText()); 383 } else if (firstVisibleItem.getTag() instanceof AgendaByDayAdapter.ViewHolder) { 384 AgendaByDayAdapter.ViewHolder viewHolder = 385 (AgendaByDayAdapter.ViewHolder) firstVisibleItem.getTag(); 386 Log.v(TAG, "Shifting from " + position + " by " + offset + ". Date " 387 + viewHolder.dateView.getText()); 388 } else if (firstVisibleItem instanceof TextView) { 389 Log.v(TAG, "Shifting: Looking at header here. " + getSelectedItemPosition()); 390 } 391 } 392 } else if (getSelectedItemPosition() >= 0) { 393 if (DEBUG) { 394 Log.v(TAG, "Shifting selection from " + getSelectedItemPosition() + 395 " by " + offset); 396 } 397 setSelection(getSelectedItemPosition() + offset); 398 } 399 } 400 401 public void setHideDeclinedEvents(boolean hideDeclined) { 402 mWindowAdapter.setHideDeclinedEvents(hideDeclined); 403 } 404 405 public void onResume() { 406 mTZUpdater.run(); 407 Utils.setMidnightUpdater(mHandler, mMidnightUpdater, mTimeZone); 408 setPastEventsUpdater(); 409 mWindowAdapter.onResume(); 410 } 411 412 public void onPause() { 413 Utils.resetMidnightUpdater(mHandler, mMidnightUpdater); 414 resetPastEventsUpdater(); 415 } 416 } 417