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 
17 package com.android.calendar.agenda;
18 
19 
20 import android.app.Activity;
21 import android.app.Fragment;
22 import android.app.FragmentManager;
23 import android.app.FragmentTransaction;
24 import android.content.SharedPreferences;
25 import android.os.Bundle;
26 import android.provider.CalendarContract.Attendees;
27 import android.text.format.Time;
28 import android.util.Log;
29 import android.view.LayoutInflater;
30 import android.view.View;
31 import android.view.ViewGroup;
32 import android.widget.AbsListView;
33 import android.widget.AbsListView.OnScrollListener;
34 import android.widget.Adapter;
35 import android.widget.HeaderViewListAdapter;
36 
37 import com.android.calendar.CalendarController;
38 import com.android.calendar.CalendarController.EventInfo;
39 import com.android.calendar.CalendarController.EventType;
40 import com.android.calendar.CalendarController.ViewType;
41 import com.android.calendar.EventInfoFragment;
42 import com.android.calendar.GeneralPreferences;
43 import com.android.calendar.R;
44 import com.android.calendar.StickyHeaderListView;
45 import com.android.calendar.Utils;
46 
47 import java.util.Date;
48 
49 public 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 
AgendaFragment()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
AgendaFragment(long timeMillis, boolean usedForSearch)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
onAttach(Activity activity)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
onCreate(Bundle icicle)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
onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)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
onResume()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
onSaveInstanceState(Bundle outState)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      */
removeFragments(FragmentManager fragmentManager)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
onPause()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 
goTo(EventInfo event, boolean animate)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 
search(String query, Time time)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
eventsChanged()351     public void eventsChanged() {
352         if (mAgendaListView != null) {
353             mAgendaListView.refresh(true);
354         }
355     }
356 
357     @Override
getSupportedEventTypes()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
handleEvent(EventInfo event)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 
getLastShowEventId()381     public long getLastShowEventId() {
382         return mLastShownEventId;
383     }
384 
385     // Shows the selected event in the Agenda view
showEventInfo(EventInfo event, boolean allDay, boolean replaceFragment)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
onScrollStateChanged(AbsListView view, int scrollState)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
onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount)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