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.AgendaItem;
     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             AgendaItem item = mWindowAdapter.getAgendaItemByPosition(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 (item != null && (oldInstanceId != mWindowAdapter.getSelectedInstanceId() ||
    183                     !mShowEventDetailsWithAgenda)) {
    184                 long startTime = item.begin;
    185                 long endTime = item.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 (item.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, item.id,
    202                         startTime, endTime, 0, 0, CalendarController.EventInfo.buildViewExtraLong(
    203                                 Attendees.ATTENDEE_STATUS_NONE, item.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         AgendaItem agendaItem = mWindowAdapter.getAgendaItemByPosition(position);
    234         if (agendaItem != null) {
    235             mDeleteEventHelper.delete(agendaItem.begin, agendaItem.end, agendaItem.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             AgendaItem item = mWindowAdapter.getAgendaItemByPosition(position);
    256             if (item != null) {
    257                 return item.begin;
    258             }
    259         }
    260         return getFirstVisibleTime(null);
    261     }
    262 
    263     public AgendaAdapter.ViewHolder getSelectedViewHolder() {
    264         return mWindowAdapter.getSelectedViewHolder();
    265     }
    266 
    267     public long getFirstVisibleTime(AgendaItem item) {
    268         AgendaItem agendaItem = item;
    269         if (item == null) {
    270             agendaItem = getFirstVisibleAgendaItem();
    271         }
    272         if (agendaItem != null) {
    273             Time t = new Time(mTimeZone);
    274             t.set(agendaItem.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(agendaItem.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 AgendaItem getFirstVisibleAgendaItem() {
    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.getAgendaItemByPosition(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 isAgendaItemVisible(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             AgendaItem agendaItem = mWindowAdapter.getAgendaItemByPosition(i + start);
    347             if (agendaItem == null) {
    348                 continue;
    349             }
    350             if (agendaItem.id == id && agendaItem.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