1/*
2 * Copyright (C) 2007 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
19
20import android.app.Activity;
21import android.app.Fragment;
22import android.app.FragmentManager;
23import android.app.FragmentTransaction;
24import android.content.SharedPreferences;
25import android.os.Bundle;
26import android.provider.CalendarContract.Attendees;
27import android.text.format.Time;
28import android.util.Log;
29import android.view.LayoutInflater;
30import android.view.View;
31import android.view.ViewGroup;
32import android.widget.AbsListView;
33import android.widget.AbsListView.OnScrollListener;
34import android.widget.Adapter;
35import android.widget.HeaderViewListAdapter;
36
37import com.android.calendar.CalendarController;
38import com.android.calendar.CalendarController.EventInfo;
39import com.android.calendar.CalendarController.EventType;
40import com.android.calendar.CalendarController.ViewType;
41import com.android.calendar.EventInfoFragment;
42import com.android.calendar.GeneralPreferences;
43import com.android.calendar.R;
44import com.android.calendar.StickyHeaderListView;
45import com.android.calendar.Utils;
46
47import java.util.Date;
48
49public class AgendaFragment extends Fragment implements CalendarController.EventHandler,
50        OnScrollListener {
51
52    private static final String TAG = AgendaFragment.class.getSimpleName();
53    private static boolean DEBUG = false;
54
55    protected static final String BUNDLE_KEY_RESTORE_TIME = "key_restore_time";
56    protected static final String BUNDLE_KEY_RESTORE_INSTANCE_ID = "key_restore_instance_id";
57
58    private AgendaListView mAgendaListView;
59    private Activity mActivity;
60    private final Time mTime;
61    private String mTimeZone;
62    private final long mInitialTimeMillis;
63    private boolean mShowEventDetailsWithAgenda;
64    private CalendarController mController;
65    private EventInfoFragment mEventFragment;
66    private String mQuery;
67    private boolean mUsedForSearch = false;
68    private boolean mIsTabletConfig;
69    private EventInfo mOnAttachedInfo = null;
70    private boolean mOnAttachAllDay = false;
71    private AgendaWindowAdapter mAdapter = null;
72    private boolean mForceReplace = true;
73    private long mLastShownEventId = -1;
74
75
76
77    // Tracks the time of the top visible view in order to send UPDATE_TITLE messages to the action
78    // bar.
79    int  mJulianDayOnTop = -1;
80
81    private final Runnable mTZUpdater = new Runnable() {
82        @Override
83        public void run() {
84            mTimeZone = Utils.getTimeZone(getActivity(), this);
85            mTime.switchTimezone(mTimeZone);
86        }
87    };
88
89    public AgendaFragment() {
90        this(0, false);
91    }
92
93
94    // timeMillis - time of first event to show
95    // usedForSearch - indicates if this fragment is used in the search fragment
96    public AgendaFragment(long timeMillis, boolean usedForSearch) {
97        mInitialTimeMillis = timeMillis;
98        mTime = new Time();
99        mLastHandledEventTime = new Time();
100
101        if (mInitialTimeMillis == 0) {
102            mTime.setToNow();
103        } else {
104            mTime.set(mInitialTimeMillis);
105        }
106        mLastHandledEventTime.set(mTime);
107        mUsedForSearch = usedForSearch;
108    }
109
110    @Override
111    public void onAttach(Activity activity) {
112        super.onAttach(activity);
113        mTimeZone = Utils.getTimeZone(activity, mTZUpdater);
114        mTime.switchTimezone(mTimeZone);
115        mActivity = activity;
116        if (mOnAttachedInfo != null) {
117            showEventInfo(mOnAttachedInfo, mOnAttachAllDay, true);
118            mOnAttachedInfo = null;
119        }
120    }
121
122    @Override
123    public void onCreate(Bundle icicle) {
124        super.onCreate(icicle);
125        mController = CalendarController.getInstance(mActivity);
126        mShowEventDetailsWithAgenda =
127            Utils.getConfigBool(mActivity, R.bool.show_event_details_with_agenda);
128        mIsTabletConfig =
129            Utils.getConfigBool(mActivity, R.bool.tablet_config);
130        if (icicle != null) {
131            long prevTime = icicle.getLong(BUNDLE_KEY_RESTORE_TIME, -1);
132            if (prevTime != -1) {
133                mTime.set(prevTime);
134                if (DEBUG) {
135                    Log.d(TAG, "Restoring time to " + mTime.toString());
136                }
137            }
138        }
139    }
140
141    @Override
142    public View onCreateView(LayoutInflater inflater, ViewGroup container,
143            Bundle savedInstanceState) {
144
145
146        int screenWidth = mActivity.getResources().getDisplayMetrics().widthPixels;
147        View v = inflater.inflate(R.layout.agenda_fragment, null);
148
149        mAgendaListView = (AgendaListView)v.findViewById(R.id.agenda_events_list);
150        mAgendaListView.setClickable(true);
151
152        if (savedInstanceState != null) {
153            long instanceId = savedInstanceState.getLong(BUNDLE_KEY_RESTORE_INSTANCE_ID, -1);
154            if (instanceId != -1) {
155                mAgendaListView.setSelectedInstanceId(instanceId);
156            }
157        }
158
159        View eventView =  v.findViewById(R.id.agenda_event_info);
160        if (!mShowEventDetailsWithAgenda) {
161            eventView.setVisibility(View.GONE);
162        }
163
164        View topListView;
165        // Set adapter & HeaderIndexer for StickyHeaderListView
166        StickyHeaderListView lv =
167            (StickyHeaderListView)v.findViewById(R.id.agenda_sticky_header_list);
168        if (lv != null) {
169            Adapter a = mAgendaListView.getAdapter();
170            lv.setAdapter(a);
171            if (a instanceof HeaderViewListAdapter) {
172                mAdapter = (AgendaWindowAdapter) ((HeaderViewListAdapter)a).getWrappedAdapter();
173                lv.setIndexer(mAdapter);
174                lv.setHeaderHeightListener(mAdapter);
175            } else if (a instanceof AgendaWindowAdapter) {
176                mAdapter = (AgendaWindowAdapter)a;
177                lv.setIndexer(mAdapter);
178                lv.setHeaderHeightListener(mAdapter);
179            } else {
180                Log.wtf(TAG, "Cannot find HeaderIndexer for StickyHeaderListView");
181            }
182
183            // Set scroll listener so that the date on the ActionBar can be set while
184            // the user scrolls the view
185            lv.setOnScrollListener(this);
186            lv.setHeaderSeparator(getResources().getColor(R.color.agenda_list_separator_color), 1);
187            topListView = lv;
188        } else {
189            topListView = mAgendaListView;
190        }
191
192        // Since using weight for sizing the two panes of the agenda fragment causes the whole
193        // fragment to re-measure when the sticky header is replaced, calculate the weighted
194        // size of each pane here and set it
195
196        if (!mShowEventDetailsWithAgenda) {
197            ViewGroup.LayoutParams params = topListView.getLayoutParams();
198            params.width = screenWidth;
199            topListView.setLayoutParams(params);
200        } else {
201            ViewGroup.LayoutParams listParams = topListView.getLayoutParams();
202            listParams.width = screenWidth * 4 / 10;
203            topListView.setLayoutParams(listParams);
204            ViewGroup.LayoutParams detailsParams = eventView.getLayoutParams();
205            detailsParams.width = screenWidth - listParams.width;
206            eventView.setLayoutParams(detailsParams);
207        }
208        return v;
209    }
210
211    @Override
212    public void onResume() {
213        super.onResume();
214        if (DEBUG) {
215            Log.v(TAG, "OnResume to " + mTime.toString());
216        }
217
218        SharedPreferences prefs = GeneralPreferences.getSharedPreferences(
219                getActivity());
220        boolean hideDeclined = prefs.getBoolean(
221                GeneralPreferences.KEY_HIDE_DECLINED, false);
222
223        mAgendaListView.setHideDeclinedEvents(hideDeclined);
224        if (mLastHandledEventId != -1) {
225            mAgendaListView.goTo(mLastHandledEventTime, mLastHandledEventId, mQuery, true, false);
226            mLastHandledEventTime = null;
227            mLastHandledEventId = -1;
228        } else {
229            mAgendaListView.goTo(mTime, -1, mQuery, true, false);
230        }
231        mAgendaListView.onResume();
232
233//        // Register for Intent broadcasts
234//        IntentFilter filter = new IntentFilter();
235//        filter.addAction(Intent.ACTION_TIME_CHANGED);
236//        filter.addAction(Intent.ACTION_DATE_CHANGED);
237//        filter.addAction(Intent.ACTION_TIMEZONE_CHANGED);
238//        registerReceiver(mIntentReceiver, filter);
239//
240//        mContentResolver.registerContentObserver(Events.CONTENT_URI, true, mObserver);
241    }
242
243    @Override
244    public void onSaveInstanceState(Bundle outState) {
245        super.onSaveInstanceState(outState);
246        if (mAgendaListView == null) {
247            return;
248        }
249        if (mShowEventDetailsWithAgenda) {
250            long timeToSave;
251            if (mLastHandledEventTime != null) {
252                timeToSave = mLastHandledEventTime.toMillis(true);
253                mTime.set(mLastHandledEventTime);
254            } else {
255                timeToSave =  System.currentTimeMillis();
256                mTime.set(timeToSave);
257            }
258            outState.putLong(BUNDLE_KEY_RESTORE_TIME, timeToSave);
259            mController.setTime(timeToSave);
260        } else {
261            AgendaWindowAdapter.AgendaItem item = mAgendaListView.getFirstVisibleAgendaItem();
262            if (item != null) {
263                long firstVisibleTime = mAgendaListView.getFirstVisibleTime(item);
264                if (firstVisibleTime > 0) {
265                    mTime.set(firstVisibleTime);
266                    mController.setTime(firstVisibleTime);
267                    outState.putLong(BUNDLE_KEY_RESTORE_TIME, firstVisibleTime);
268                }
269                // Tell AllInOne the event id of the first visible event in the list. The id will be
270                // used in the GOTO when AllInOne is restored so that Agenda Fragment can select a
271                // specific event and not just the time.
272                mLastShownEventId = item.id;
273            }
274        }
275        if (DEBUG) {
276            Log.v(TAG, "onSaveInstanceState " + mTime.toString());
277        }
278
279        long selectedInstance = mAgendaListView.getSelectedInstanceId();
280        if (selectedInstance >= 0) {
281            outState.putLong(BUNDLE_KEY_RESTORE_INSTANCE_ID, selectedInstance);
282        }
283    }
284
285    /**
286     * This cleans up the event info fragment since the FragmentManager doesn't
287     * handle nested fragments. Without this, the action bar buttons added by
288     * the info fragment can come back on a rotation.
289     *
290     * @param fragmentManager
291     */
292    public void removeFragments(FragmentManager fragmentManager) {
293        if (getActivity().isFinishing()) {
294            return;
295        }
296        FragmentTransaction ft = fragmentManager.beginTransaction();
297        Fragment f = fragmentManager.findFragmentById(R.id.agenda_event_info);
298        if (f != null) {
299            ft.remove(f);
300        }
301        ft.commit();
302    }
303
304    @Override
305    public void onPause() {
306        super.onPause();
307
308        mAgendaListView.onPause();
309
310//        mContentResolver.unregisterContentObserver(mObserver);
311//        unregisterReceiver(mIntentReceiver);
312
313        // Record Agenda View as the (new) default detailed view.
314//        Utils.setDefaultView(this, CalendarApplication.AGENDA_VIEW_ID);
315    }
316
317    private void goTo(EventInfo event, boolean animate) {
318        if (event.selectedTime != null) {
319            mTime.set(event.selectedTime);
320        } else if (event.startTime != null) {
321            mTime.set(event.startTime);
322        }
323        if (mAgendaListView == null) {
324            // The view hasn't been set yet. Just save the time and use it
325            // later.
326            return;
327        }
328        mAgendaListView.goTo(mTime, event.id, mQuery, false,
329                ((event.extraLong & CalendarController.EXTRA_GOTO_TODAY) != 0  &&
330                        mShowEventDetailsWithAgenda) ? true : false);
331        AgendaAdapter.ViewHolder vh = mAgendaListView.getSelectedViewHolder();
332        // Make sure that on the first time the event info is shown to recreate it
333        Log.d(TAG, "selected viewholder is null: " + (vh == null));
334        showEventInfo(event, vh != null ? vh.allDay : false, mForceReplace);
335        mForceReplace = false;
336    }
337
338    private void search(String query, Time time) {
339        mQuery = query;
340        if (time != null) {
341            mTime.set(time);
342        }
343        if (mAgendaListView == null) {
344            // The view hasn't been set yet. Just return.
345            return;
346        }
347        mAgendaListView.goTo(time, -1, mQuery, true, false);
348    }
349
350    @Override
351    public void eventsChanged() {
352        if (mAgendaListView != null) {
353            mAgendaListView.refresh(true);
354        }
355    }
356
357    @Override
358    public long getSupportedEventTypes() {
359        return EventType.GO_TO | EventType.EVENTS_CHANGED | ((mUsedForSearch)?EventType.SEARCH:0);
360    }
361
362    private long mLastHandledEventId = -1;
363    private Time mLastHandledEventTime = null;
364    @Override
365    public void handleEvent(EventInfo event) {
366        if (event.eventType == EventType.GO_TO) {
367            // TODO support a range of time
368            // TODO support event_id
369            // TODO figure out the animate bit
370            mLastHandledEventId = event.id;
371            mLastHandledEventTime =
372                    (event.selectedTime != null) ? event.selectedTime : event.startTime;
373            goTo(event, true);
374        } else if (event.eventType == EventType.SEARCH) {
375            search(event.query, event.startTime);
376        } else if (event.eventType == EventType.EVENTS_CHANGED) {
377            eventsChanged();
378        }
379    }
380
381    public long getLastShowEventId() {
382        return mLastShownEventId;
383    }
384
385    // Shows the selected event in the Agenda view
386    private void showEventInfo(EventInfo event, boolean allDay, boolean replaceFragment) {
387
388        // Ignore unknown events
389        if (event.id == -1) {
390            Log.e(TAG, "showEventInfo, event ID = " + event.id);
391            return;
392        }
393
394        mLastShownEventId = event.id;
395
396        // Create a fragment to show the event to the side of the agenda list
397        if (mShowEventDetailsWithAgenda) {
398            FragmentManager fragmentManager = getFragmentManager();
399            if (fragmentManager == null) {
400                // Got a goto event before the fragment finished attaching,
401                // stash the event and handle it later.
402                mOnAttachedInfo = event;
403                mOnAttachAllDay = allDay;
404                return;
405            }
406            FragmentTransaction ft = fragmentManager.beginTransaction();
407
408            if (allDay) {
409                event.startTime.timezone = Time.TIMEZONE_UTC;
410                event.endTime.timezone = Time.TIMEZONE_UTC;
411            }
412
413            if (DEBUG) {
414                Log.d(TAG, "***");
415                Log.d(TAG, "showEventInfo: start: " + new Date(event.startTime.toMillis(true)));
416                Log.d(TAG, "showEventInfo: end: " + new Date(event.endTime.toMillis(true)));
417                Log.d(TAG, "showEventInfo: all day: " + allDay);
418                Log.d(TAG, "***");
419            }
420
421            long startMillis = event.startTime.toMillis(true);
422            long endMillis = event.endTime.toMillis(true);
423            EventInfoFragment fOld =
424                    (EventInfoFragment)fragmentManager.findFragmentById(R.id.agenda_event_info);
425            if (fOld == null || replaceFragment || fOld.getStartMillis() != startMillis ||
426                    fOld.getEndMillis() != endMillis || fOld.getEventId() != event.id) {
427                mEventFragment = new EventInfoFragment(mActivity, event.id,
428                        startMillis, endMillis,
429                        Attendees.ATTENDEE_STATUS_NONE, false,
430                        EventInfoFragment.DIALOG_WINDOW_STYLE, null);
431                ft.replace(R.id.agenda_event_info, mEventFragment);
432                ft.commit();
433            } else {
434                fOld.reloadEvents();
435            }
436        }
437    }
438
439    // OnScrollListener implementation to update the date on the pull-down menu of the app
440
441    @Override
442    public void onScrollStateChanged(AbsListView view, int scrollState) {
443        // Save scroll state so that the adapter can stop the scroll when the
444        // agenda list is fling state and it needs to set the agenda list to a new position
445        if (mAdapter != null) {
446            mAdapter.setScrollState(scrollState);
447        }
448    }
449
450    // Gets the time of the first visible view. If it is a new time, send a message to update
451    // the time on the ActionBar
452    @Override
453    public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
454            int totalItemCount) {
455        int julianDay = mAgendaListView.getJulianDayFromPosition(firstVisibleItem
456                - mAgendaListView.getHeaderViewsCount());
457        // On error - leave the old view
458        if (julianDay == 0) {
459            return;
460        }
461        // If the day changed, update the ActionBar
462        if (mJulianDayOnTop != julianDay) {
463            mJulianDayOnTop = julianDay;
464            Time t = new Time(mTimeZone);
465            t.setJulianDay(mJulianDayOnTop);
466            mController.setTime(t.toMillis(true));
467            // Cannot sent a message that eventually may change the layout of the views
468            // so instead post a runnable that will run when the layout is done
469            if (!mIsTabletConfig) {
470                view.post(new Runnable() {
471                    @Override
472                    public void run() {
473                        Time t = new Time(mTimeZone);
474                        t.setJulianDay(mJulianDayOnTop);
475                        mController.sendEvent(this, EventType.UPDATE_TITLE, t, t, null, -1,
476                                ViewType.CURRENT, 0, null, null);
477                    }
478                });
479            }
480        }
481    }
482}
483