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 public 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