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