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