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