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