/* * Copyright (C) 2009 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.calendar.agenda; import com.android.calendar.CalendarController; import com.android.calendar.CalendarController.EventType; import com.android.calendar.DeleteEventHelper; import com.android.calendar.R; import com.android.calendar.Utils; import com.android.calendar.agenda.AgendaAdapter.ViewHolder; import com.android.calendar.agenda.AgendaWindowAdapter.DayAdapterInfo; import com.android.calendar.agenda.AgendaWindowAdapter.AgendaItem; import android.content.Context; import android.graphics.Rect; import android.os.Handler; import android.provider.CalendarContract.Attendees; import android.text.format.Time; import android.util.AttributeSet; import android.util.Log; import android.view.View; import android.widget.AdapterView; import android.widget.AdapterView.OnItemClickListener; import android.widget.ListView; import android.widget.TextView; public class AgendaListView extends ListView implements OnItemClickListener { private static final String TAG = "AgendaListView"; private static final boolean DEBUG = false; private static final int EVENT_UPDATE_TIME = 300000; // 5 minutes private AgendaWindowAdapter mWindowAdapter; private DeleteEventHelper mDeleteEventHelper; private Context mContext; private String mTimeZone; private Time mTime; private boolean mShowEventDetailsWithAgenda; private Handler mHandler = null; private final Runnable mTZUpdater = new Runnable() { @Override public void run() { mTimeZone = Utils.getTimeZone(mContext, this); mTime.switchTimezone(mTimeZone); } }; // runs every midnight and refreshes the view in order to update the past/present // separator private final Runnable mMidnightUpdater = new Runnable() { @Override public void run() { refresh(true); Utils.setMidnightUpdater(mHandler, mMidnightUpdater, mTimeZone); } }; // Runs every EVENT_UPDATE_TIME to gray out past events private final Runnable mPastEventUpdater = new Runnable() { @Override public void run() { if (updatePastEvents() == true) { refresh(true); } setPastEventsUpdater(); } }; public AgendaListView(Context context, AttributeSet attrs) { super(context, attrs); initView(context); } private void initView(Context context) { mContext = context; mTimeZone = Utils.getTimeZone(context, mTZUpdater); mTime = new Time(mTimeZone); setOnItemClickListener(this); setVerticalScrollBarEnabled(false); mWindowAdapter = new AgendaWindowAdapter(context, this, Utils.getConfigBool(context, R.bool.show_event_details_with_agenda)); mWindowAdapter.setSelectedInstanceId(-1/* TODO:instanceId */); setAdapter(mWindowAdapter); setCacheColorHint(context.getResources().getColor(R.color.agenda_item_not_selected)); mDeleteEventHelper = new DeleteEventHelper(context, null, false /* don't exit when done */); mShowEventDetailsWithAgenda = Utils.getConfigBool(mContext, R.bool.show_event_details_with_agenda); // Hide ListView dividers, they are done in the item views themselves setDivider(null); setDividerHeight(0); mHandler = new Handler(); } // Sets a thread to run every EVENT_UPDATE_TIME in order to update the list // with grayed out past events private void setPastEventsUpdater() { // Run the thread in the nearest rounded EVENT_UPDATE_TIME long now = System.currentTimeMillis(); long roundedTime = (now / EVENT_UPDATE_TIME) * EVENT_UPDATE_TIME; mHandler.removeCallbacks(mPastEventUpdater); mHandler.postDelayed(mPastEventUpdater, EVENT_UPDATE_TIME - (now - roundedTime)); } // Stop the past events thread private void resetPastEventsUpdater() { mHandler.removeCallbacks(mPastEventUpdater); } // Go over all visible views and checks if all past events are grayed out. // Returns true is there is at least one event that ended and it is not // grayed out. private boolean updatePastEvents() { int childCount = getChildCount(); boolean needUpdate = false; long now = System.currentTimeMillis(); Time time = new Time(mTimeZone); time.set(now); int todayJulianDay = Time.getJulianDay(now, time.gmtoff); // Go over views in list for (int i = 0; i < childCount; ++i) { View listItem = getChildAt(i); Object o = listItem.getTag(); if (o instanceof AgendaByDayAdapter.ViewHolder) { // day view - check if day in the past and not grayed yet AgendaByDayAdapter.ViewHolder holder = (AgendaByDayAdapter.ViewHolder) o; if (holder.julianDay <= todayJulianDay && !holder.grayed) { needUpdate = true; break; } } else if (o instanceof AgendaAdapter.ViewHolder) { // meeting view - check if event in the past or started already and not grayed yet // All day meetings for a day are grayed out AgendaAdapter.ViewHolder holder = (AgendaAdapter.ViewHolder) o; if (!holder.grayed && ((!holder.allDay && holder.startTimeMilli <= now) || (holder.allDay && holder.julianDay <= todayJulianDay))) { needUpdate = true; break; } } } return needUpdate; } @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); mWindowAdapter.close(); } // Implementation of the interface OnItemClickListener @Override public void onItemClick(AdapterView a, View v, int position, long id) { if (id != -1) { // Switch to the EventInfo view AgendaItem item = mWindowAdapter.getAgendaItemByPosition(position); long oldInstanceId = mWindowAdapter.getSelectedInstanceId(); mWindowAdapter.setSelectedView(v); // If events are shown to the side of the agenda list , do nothing // when the same event is selected , otherwise show the selected event. if (item != null && (oldInstanceId != mWindowAdapter.getSelectedInstanceId() || !mShowEventDetailsWithAgenda)) { long startTime = item.begin; long endTime = item.end; // Holder in view holds the start of the specific part of a multi-day event , // use it for the goto long holderStartTime; Object holder = v.getTag(); if (holder instanceof AgendaAdapter.ViewHolder) { holderStartTime = ((AgendaAdapter.ViewHolder) holder).startTimeMilli; } else { holderStartTime = startTime; } if (item.allDay) { startTime = Utils.convertAlldayLocalToUTC(mTime, startTime, mTimeZone); endTime = Utils.convertAlldayLocalToUTC(mTime, endTime, mTimeZone); } mTime.set(startTime); CalendarController controller = CalendarController.getInstance(mContext); controller.sendEventRelatedEventWithExtra(this, EventType.VIEW_EVENT, item.id, startTime, endTime, 0, 0, CalendarController.EventInfo.buildViewExtraLong( Attendees.ATTENDEE_STATUS_NONE, item.allDay), holderStartTime); } } } public void goTo(Time time, long id, String searchQuery, boolean forced, boolean refreshEventInfo) { if (time == null) { time = mTime; long goToTime = getFirstVisibleTime(null); if (goToTime <= 0) { goToTime = System.currentTimeMillis(); } time.set(goToTime); } mTime.set(time); mTime.switchTimezone(mTimeZone); mTime.normalize(true); if (DEBUG) { Log.d(TAG, "Goto with time " + mTime.toString()); } mWindowAdapter.refresh(mTime, id, searchQuery, forced, refreshEventInfo); } public void refresh(boolean forced) { mWindowAdapter.refresh(mTime, -1, null, forced, false); } public void deleteSelectedEvent() { int position = getSelectedItemPosition(); AgendaItem agendaItem = mWindowAdapter.getAgendaItemByPosition(position); if (agendaItem != null) { mDeleteEventHelper.delete(agendaItem.begin, agendaItem.end, agendaItem.id, -1); } } public View getFirstVisibleView() { Rect r = new Rect(); int childCount = getChildCount(); for (int i = 0; i < childCount; ++i) { View listItem = getChildAt(i); listItem.getLocalVisibleRect(r); if (r.top >= 0) { // if visible return listItem; } } return null; } public long getSelectedTime() { int position = getSelectedItemPosition(); if (position >= 0) { AgendaItem item = mWindowAdapter.getAgendaItemByPosition(position); if (item != null) { return item.begin; } } return getFirstVisibleTime(null); } public AgendaAdapter.ViewHolder getSelectedViewHolder() { return mWindowAdapter.getSelectedViewHolder(); } public long getFirstVisibleTime(AgendaItem item) { AgendaItem agendaItem = item; if (item == null) { agendaItem = getFirstVisibleAgendaItem(); } if (agendaItem != null) { Time t = new Time(mTimeZone); t.set(agendaItem.begin); // Save and restore the time since setJulianDay sets the time to 00:00:00 int hour = t.hour; int minute = t.minute; int second = t.second; t.setJulianDay(agendaItem.startDay); t.hour = hour; t.minute = minute; t.second = second; if (DEBUG) { t.normalize(true); Log.d(TAG, "first position had time " + t.toString()); } return t.normalize(false); } return 0; } public AgendaItem getFirstVisibleAgendaItem() { int position = getFirstVisiblePosition(); if (DEBUG) { Log.v(TAG, "getFirstVisiblePosition = " + position); } // mShowEventDetailsWithAgenda == true implies we have a sticky header. In that case // we may need to take the second visible position, since the first one maybe the one // under the sticky header. if (mShowEventDetailsWithAgenda) { View v = getFirstVisibleView (); if (v != null) { Rect r = new Rect (); v.getLocalVisibleRect(r); if (r.bottom - r.top <= mWindowAdapter.getStickyHeaderHeight()) { position ++; } } } return mWindowAdapter.getAgendaItemByPosition(position, false /* startDay = date separator date instead of actual event startday */); } public int getJulianDayFromPosition(int position) { DayAdapterInfo info = mWindowAdapter.getAdapterInfoByPosition(position); if (info != null) { return info.dayAdapter.findJulianDayFromPosition(position - info.offset); } return 0; } // Finds is a specific event (defined by start time and id) is visible public boolean isAgendaItemVisible(Time startTime, long id) { if (id == -1 || startTime == null) { return false; } View child = getChildAt(0); // View not set yet, so not child - return if (child == null) { return false; } int start = getPositionForView(child); long milliTime = startTime.toMillis(true); int childCount = getChildCount(); int eventsInAdapter = mWindowAdapter.getCount(); for (int i = 0; i < childCount; i++) { if (i + start >= eventsInAdapter) { break; } AgendaItem agendaItem = mWindowAdapter.getAgendaItemByPosition(i + start); if (agendaItem == null) { continue; } if (agendaItem.id == id && agendaItem.begin == milliTime) { View listItem = getChildAt(i); if (listItem.getTop() <= getHeight() && listItem.getTop() >= mWindowAdapter.getStickyHeaderHeight()) { return true; } } } return false; } public long getSelectedInstanceId() { return mWindowAdapter.getSelectedInstanceId(); } public void setSelectedInstanceId(long id) { mWindowAdapter.setSelectedInstanceId(id); } // Move the currently selected or visible focus down by offset amount. // offset could be negative. public void shiftSelection(int offset) { shiftPosition(offset); int position = getSelectedItemPosition(); if (position != INVALID_POSITION) { setSelectionFromTop(position + offset, 0); } } private void shiftPosition(int offset) { if (DEBUG) { Log.v(TAG, "Shifting position " + offset); } View firstVisibleItem = getFirstVisibleView(); if (firstVisibleItem != null) { Rect r = new Rect(); firstVisibleItem.getLocalVisibleRect(r); // if r.top is < 0, getChildAt(0) and getFirstVisiblePosition() is // returning an item above the first visible item. int position = getPositionForView(firstVisibleItem); setSelectionFromTop(position + offset, r.top > 0 ? -r.top : r.top); if (DEBUG) { if (firstVisibleItem.getTag() instanceof AgendaAdapter.ViewHolder) { ViewHolder viewHolder = (AgendaAdapter.ViewHolder) firstVisibleItem.getTag(); Log.v(TAG, "Shifting from " + position + " by " + offset + ". Title " + viewHolder.title.getText()); } else if (firstVisibleItem.getTag() instanceof AgendaByDayAdapter.ViewHolder) { AgendaByDayAdapter.ViewHolder viewHolder = (AgendaByDayAdapter.ViewHolder) firstVisibleItem.getTag(); Log.v(TAG, "Shifting from " + position + " by " + offset + ". Date " + viewHolder.dateView.getText()); } else if (firstVisibleItem instanceof TextView) { Log.v(TAG, "Shifting: Looking at header here. " + getSelectedItemPosition()); } } } else if (getSelectedItemPosition() >= 0) { if (DEBUG) { Log.v(TAG, "Shifting selection from " + getSelectedItemPosition() + " by " + offset); } setSelection(getSelectedItemPosition() + offset); } } public void setHideDeclinedEvents(boolean hideDeclined) { mWindowAdapter.setHideDeclinedEvents(hideDeclined); } public void onResume() { mTZUpdater.run(); Utils.setMidnightUpdater(mHandler, mMidnightUpdater, mTimeZone); setPastEventsUpdater(); mWindowAdapter.onResume(); } public void onPause() { Utils.resetMidnightUpdater(mHandler, mMidnightUpdater); resetPastEventsUpdater(); } }