FragmentActivity.java revision 681a6fb06bdedb8661a68a1b9e34727b6059aa39
1/* 2 * Copyright (C) 2011 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 android.support.v4.app; 18 19import android.app.Activity; 20import android.content.Context; 21import android.content.Intent; 22import android.content.res.Configuration; 23import android.content.res.TypedArray; 24import android.os.Bundle; 25import android.os.Handler; 26import android.os.Message; 27import android.os.Parcelable; 28import android.util.AttributeSet; 29import android.util.Log; 30import android.view.KeyEvent; 31import android.view.Menu; 32import android.view.MenuItem; 33import android.view.View; 34import android.view.Window; 35 36import java.io.FileDescriptor; 37import java.io.PrintWriter; 38import java.util.ArrayList; 39import java.util.HashMap; 40 41/** 42 * Base class for activities that want to use the support-based Fragment and 43 * Loader APIs. 44 * 45 * <p>Known issues and limitations:</p> 46 * <ul> 47 * <li> <p>When using the <fragment> tag, this implementation can not 48 * use the parent view's ID as the new fragment's ID. You must explicitly 49 * specify an ID (or tag) in the <fragment>.</p> 50 * <li> <p>The options menu can not currently be invalidated (causing it to be 51 * rebuilt). Your activity will need to build an options menu containing all 52 * items that may be shown by fragment it contains. The prepareOptionsMenu() 53 * method is called every time the menu is shown, allowing items to be hidden 54 * or shown.</p> 55 * <li> <p>Prior to Honeycomb (3.0), an activity's state was saved before pausing. 56 * Fragments are a significant amount of new state, and dynamic enough that one 57 * often wants them to change between pausing and stopping. These classes 58 * throw an exception if you try to change the fragment state after it has been 59 * saved, to avoid accidental loss of UI state. However this is too restrictive 60 * prior to Honeycomb, where the state is saved before pausing. To address this, 61 * when running on platforms prior to Honeycomb an exception will not be thrown 62 * if you change fragments between the state save and the activity being stopped. 63 * This means that is some cases if the activity is restored from its last saved 64 * state, this may be a snapshot slightly before what the user last saw.</p> 65 * </ul> 66 */ 67public class FragmentActivity extends Activity { 68 private static final String TAG = "FragmentActivity"; 69 70 private static final String FRAGMENTS_TAG = "android:support:fragments"; 71 72 static final int MSG_REALLY_STOPPED = 1; 73 74 final Handler mHandler = new Handler() { 75 @Override 76 public void handleMessage(Message msg) { 77 switch (msg.what) { 78 case MSG_REALLY_STOPPED: 79 if (mStopped) { 80 doReallyStop(false); 81 } 82 break; 83 default: 84 super.handleMessage(msg); 85 } 86 } 87 88 }; 89 final FragmentManagerImpl mFragments = new FragmentManagerImpl(); 90 91 boolean mResumed; 92 boolean mStopped; 93 boolean mReallyStopped; 94 95 boolean mCheckedForLoaderManager; 96 boolean mLoadersStarted; 97 HCSparseArray<LoaderManagerImpl> mAllLoaderManagers; 98 LoaderManagerImpl mLoaderManager; 99 100 static final class NonConfigurationInstances { 101 Object activity; 102 HashMap<String, Object> children; 103 ArrayList<Fragment> fragments; 104 HCSparseArray<LoaderManagerImpl> loaders; 105 } 106 107 static class FragmentTag { 108 public static final int[] Fragment = { 109 0x01010003, 0x010100d0, 0x010100d1 110 }; 111 public static final int Fragment_id = 1; 112 public static final int Fragment_name = 0; 113 public static final int Fragment_tag = 2; 114 } 115 116 // ------------------------------------------------------------------------ 117 // HOOKS INTO ACTIVITY 118 // ------------------------------------------------------------------------ 119 120 /** 121 * Dispatch incoming result to the correct fragment. 122 */ 123 @Override 124 protected void onActivityResult(int requestCode, int resultCode, Intent data) { 125 int index = requestCode>>16; 126 if (index != 0) { 127 index--; 128 if (mFragments.mActive == null || index < 0 || index >= mFragments.mActive.size()) { 129 Log.w(TAG, "Activity result fragment index out of range: 0x" 130 + Integer.toHexString(requestCode)); 131 return; 132 } 133 Fragment frag = mFragments.mActive.get(index); 134 if (frag == null) { 135 Log.w(TAG, "Activity result no fragment exists for index: 0x" 136 + Integer.toHexString(requestCode)); 137 } 138 frag.onActivityResult(requestCode&0xffff, resultCode, data); 139 return; 140 } 141 142 super.onActivityResult(requestCode, resultCode, data); 143 } 144 145 /** 146 * Take care of popping the fragment back stack or finishing the activity 147 * as appropriate. 148 */ 149 public void onBackPressed() { 150 if (!mFragments.popBackStackImmediate()) { 151 finish(); 152 } 153 } 154 155 /** 156 * Dispatch configuration change to all fragments. 157 */ 158 @Override 159 public void onConfigurationChanged(Configuration newConfig) { 160 super.onConfigurationChanged(newConfig); 161 mFragments.dispatchConfigurationChanged(newConfig); 162 } 163 164 /** 165 * Perform initialization of all fragments and loaders. 166 */ 167 @Override 168 protected void onCreate(Bundle savedInstanceState) { 169 mFragments.attachActivity(this); 170 // Old versions of the platform didn't do this! 171 if (getLayoutInflater().getFactory() == null) { 172 getLayoutInflater().setFactory(this); 173 } 174 175 super.onCreate(savedInstanceState); 176 177 NonConfigurationInstances nc = (NonConfigurationInstances) 178 getLastNonConfigurationInstance(); 179 if (nc != null) { 180 mAllLoaderManagers = nc.loaders; 181 } 182 if (savedInstanceState != null) { 183 Parcelable p = savedInstanceState.getParcelable(FRAGMENTS_TAG); 184 mFragments.restoreAllState(p, nc != null ? nc.fragments : null); 185 } 186 mFragments.dispatchCreate(); 187 } 188 189 /** 190 * Dispatch to Fragment.onCreateOptionsMenu(). 191 */ 192 @Override 193 public boolean onCreatePanelMenu(int featureId, Menu menu) { 194 if (featureId == Window.FEATURE_OPTIONS_PANEL) { 195 boolean show = super.onCreatePanelMenu(featureId, menu); 196 show |= mFragments.dispatchCreateOptionsMenu(menu, getMenuInflater()); 197 return show; 198 } 199 return super.onCreatePanelMenu(featureId, menu); 200 } 201 202 /** 203 * Add support for inflating the <fragment> tag. 204 */ 205 @Override 206 public View onCreateView(String name, Context context, AttributeSet attrs) { 207 if (!"fragment".equals(name)) { 208 return super.onCreateView(name, context, attrs); 209 } 210 211 String fname = attrs.getAttributeValue(null, "class"); 212 TypedArray a = context.obtainStyledAttributes(attrs, FragmentTag.Fragment); 213 if (fname == null) { 214 fname = a.getString(FragmentTag.Fragment_name); 215 } 216 int id = a.getResourceId(FragmentTag.Fragment_id, View.NO_ID); 217 String tag = a.getString(FragmentTag.Fragment_tag); 218 a.recycle(); 219 220 View parent = null; // XXX no way to get parent pre-Honeycomb. 221 int containerId = parent != null ? parent.getId() : 0; 222 if (containerId == View.NO_ID && id == View.NO_ID && tag == null) { 223 throw new IllegalArgumentException(attrs.getPositionDescription() 224 + ": Must specify unique android:id, android:tag, or have a parent with an id for " + fname); 225 } 226 227 // If we restored from a previous state, we may already have 228 // instantiated this fragment from the state and should use 229 // that instance instead of making a new one. 230 Fragment fragment = id != View.NO_ID ? mFragments.findFragmentById(id) : null; 231 if (fragment == null && tag != null) { 232 fragment = mFragments.findFragmentByTag(tag); 233 } 234 if (fragment == null && containerId != View.NO_ID) { 235 fragment = mFragments.findFragmentById(containerId); 236 } 237 238 if (FragmentManagerImpl.DEBUG) Log.v(TAG, "onCreateView: id=0x" 239 + Integer.toHexString(id) + " fname=" + fname 240 + " existing=" + fragment); 241 if (fragment == null) { 242 fragment = Fragment.instantiate(this, fname); 243 fragment.mFromLayout = true; 244 fragment.mFragmentId = id != 0 ? id : containerId; 245 fragment.mContainerId = containerId; 246 fragment.mTag = tag; 247 fragment.mInLayout = true; 248 fragment.mImmediateActivity = this; 249 fragment.mFragmentManager = mFragments; 250 fragment.onInflate(attrs, fragment.mSavedFragmentState); 251 mFragments.addFragment(fragment, true); 252 253 } else if (fragment.mInLayout) { 254 // A fragment already exists and it is not one we restored from 255 // previous state. 256 throw new IllegalArgumentException(attrs.getPositionDescription() 257 + ": Duplicate id 0x" + Integer.toHexString(id) 258 + ", tag " + tag + ", or parent id 0x" + Integer.toHexString(containerId) 259 + " with another fragment for " + fname); 260 } else { 261 // This fragment was retained from a previous instance; get it 262 // going now. 263 fragment.mInLayout = true; 264 fragment.mImmediateActivity = this; 265 // If this fragment is newly instantiated (either right now, or 266 // from last saved state), then give it the attributes to 267 // initialize itself. 268 if (!fragment.mRetaining) { 269 fragment.onInflate(attrs, fragment.mSavedFragmentState); 270 } 271 mFragments.moveToState(fragment); 272 } 273 274 if (fragment.mView == null) { 275 throw new IllegalStateException("Fragment " + fname 276 + " did not create a view."); 277 } 278 if (id != 0) { 279 fragment.mView.setId(id); 280 } 281 if (fragment.mView.getTag() == null) { 282 fragment.mView.setTag(tag); 283 } 284 return fragment.mView; 285 } 286 287 /** 288 * Destroy all fragments and loaders. 289 */ 290 @Override 291 protected void onDestroy() { 292 super.onDestroy(); 293 294 doReallyStop(false); 295 296 mFragments.dispatchDestroy(); 297 if (mLoaderManager != null) { 298 mLoaderManager.doDestroy(); 299 } 300 } 301 302 /** 303 * Take care of calling onBackPressed() for pre-Eclair platforms. 304 */ 305 @Override 306 public boolean onKeyDown(int keyCode, KeyEvent event) { 307 if (android.os.Build.VERSION.SDK_INT < 5 /* ECLAIR */ 308 && keyCode == KeyEvent.KEYCODE_BACK 309 && event.getRepeatCount() == 0) { 310 // Take care of calling this method on earlier versions of 311 // the platform where it doesn't exist. 312 onBackPressed(); 313 } 314 315 return super.onKeyDown(keyCode, event); 316 } 317 318 /** 319 * Dispatch onLowMemory() to all fragments. 320 */ 321 @Override 322 public void onLowMemory() { 323 super.onLowMemory(); 324 mFragments.dispatchLowMemory(); 325 } 326 327 /** 328 * Dispatch context and options menu to fragments. 329 */ 330 @Override 331 public boolean onMenuItemSelected(int featureId, MenuItem item) { 332 if (super.onMenuItemSelected(featureId, item)) { 333 return true; 334 } 335 336 switch (featureId) { 337 case Window.FEATURE_OPTIONS_PANEL: 338 return mFragments.dispatchOptionsItemSelected(item); 339 340 case Window.FEATURE_CONTEXT_MENU: 341 return mFragments.dispatchContextItemSelected(item); 342 343 default: 344 return false; 345 } 346 } 347 348 /** 349 * Call onOptionsMenuClosed() on fragments. 350 */ 351 @Override 352 public void onPanelClosed(int featureId, Menu menu) { 353 switch (featureId) { 354 case Window.FEATURE_OPTIONS_PANEL: 355 mFragments.dispatchOptionsMenuClosed(menu); 356 break; 357 } 358 super.onPanelClosed(featureId, menu); 359 } 360 361 /** 362 * Dispatch onPause() to fragments. 363 */ 364 @Override 365 protected void onPause() { 366 super.onPause(); 367 mResumed = false; 368 mFragments.dispatchPause(); 369 } 370 371 /** 372 * Dispatch onActivityCreated() on fragments. 373 */ 374 @Override 375 protected void onPostCreate(Bundle savedInstanceState) { 376 super.onPostCreate(savedInstanceState); 377 mFragments.dispatchActivityCreated(); 378 } 379 380 /** 381 * Dispatch onResume() to fragments. 382 */ 383 @Override 384 protected void onPostResume() { 385 super.onPostResume(); 386 mFragments.dispatchResume(); 387 mFragments.execPendingActions(); 388 } 389 390 /** 391 * Dispatch onPrepareOptionsMenu() to fragments. 392 */ 393 @Override 394 public boolean onPreparePanel(int featureId, View view, Menu menu) { 395 if (featureId == Window.FEATURE_OPTIONS_PANEL && menu != null) { 396 boolean goforit = super.onPreparePanel(featureId, view, menu); 397 goforit |= mFragments.dispatchPrepareOptionsMenu(menu); 398 return goforit && menu.hasVisibleItems(); 399 } 400 return super.onPreparePanel(featureId, view, menu); 401 } 402 403 /** 404 * Ensure any outstanding fragment transactions have been committed. 405 */ 406 @Override 407 protected void onResume() { 408 super.onResume(); 409 mResumed = true; 410 mFragments.execPendingActions(); 411 } 412 413 /** 414 * Retain all appropriate fragment and loader state. You can NOT 415 * override this yourself! 416 */ 417 @Override 418 public final Object onRetainNonConfigurationInstance() { 419 if (mStopped) { 420 doReallyStop(true); 421 } 422 423 ArrayList<Fragment> fragments = mFragments.retainNonConfig(); 424 boolean retainLoaders = false; 425 if (mAllLoaderManagers != null) { 426 // prune out any loader managers that were already stopped and so 427 // have nothing useful to retain. 428 for (int i=mAllLoaderManagers.size()-1; i>=0; i--) { 429 LoaderManagerImpl lm = mAllLoaderManagers.valueAt(i); 430 if (lm.mRetaining) { 431 retainLoaders = true; 432 } else { 433 lm.doDestroy(); 434 mAllLoaderManagers.removeAt(i); 435 } 436 } 437 } 438 if (fragments == null && !retainLoaders) { 439 return null; 440 } 441 442 NonConfigurationInstances nci = new NonConfigurationInstances(); 443 nci.activity = null; 444 nci.children = null; 445 nci.fragments = fragments; 446 nci.loaders = mAllLoaderManagers; 447 return nci; 448 } 449 450 /** 451 * Save all appropriate fragment state. 452 */ 453 @Override 454 protected void onSaveInstanceState(Bundle outState) { 455 super.onSaveInstanceState(outState); 456 Parcelable p = mFragments.saveAllState(); 457 if (p != null) { 458 outState.putParcelable(FRAGMENTS_TAG, p); 459 } 460 } 461 462 /** 463 * Dispatch onStart() to all fragments. Ensure any created loaders are 464 * now started. 465 */ 466 @Override 467 protected void onStart() { 468 super.onStart(); 469 470 mStopped = false; 471 mHandler.removeMessages(MSG_REALLY_STOPPED); 472 473 mFragments.noteStateNotSaved(); 474 mFragments.execPendingActions(); 475 476 477 if (!mLoadersStarted) { 478 mLoadersStarted = true; 479 if (mLoaderManager != null) { 480 mLoaderManager.doStart(); 481 } else if (!mCheckedForLoaderManager) { 482 mLoaderManager = getLoaderManager(-1, mLoadersStarted, false); 483 } 484 mCheckedForLoaderManager = true; 485 } 486 // XXX HC onStart goes here. 487 488 mFragments.dispatchStart(); 489 if (mAllLoaderManagers != null) { 490 for (int i=mAllLoaderManagers.size()-1; i>=0; i--) { 491 mAllLoaderManagers.valueAt(i).finishRetain(); 492 } 493 } 494 } 495 496 /** 497 * Dispatch onStop() to all fragments. Ensure all loaders are stopped. 498 */ 499 @Override 500 protected void onStop() { 501 super.onStop(); 502 503 mStopped = true; 504 mHandler.sendEmptyMessage(MSG_REALLY_STOPPED); 505 506 mFragments.dispatchStop(); 507 } 508 509 // ------------------------------------------------------------------------ 510 // NEW METHODS 511 // ------------------------------------------------------------------------ 512 513 /** 514 * Note: this can't be supported pre-HC. We'll see if we can figure out 515 * something clever for it. 516 */ 517 void supportInvalidateOptionsMenu() { 518 // XXX TODO 519 } 520 521 /** 522 * Print the Activity's state into the given stream. This gets invoked if 523 * you run "adb shell dumpsys activity <activity_component_name>". 524 * 525 * @param prefix Desired prefix to prepend at each line of output. 526 * @param fd The raw file descriptor that the dump is being sent to. 527 * @param writer The PrintWriter to which you should dump your state. This will be 528 * closed for you after you return. 529 * @param args additional arguments to the dump request. 530 */ 531 public void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) { 532 // XXX need to call superclass if running on HC. 533 writer.print(prefix); writer.print("Local FragmentActivity "); 534 writer.print(Integer.toHexString(System.identityHashCode(this))); 535 writer.println(" State:"); 536 String innerPrefix = prefix + " "; 537 writer.print(innerPrefix); writer.print("mResumed="); 538 writer.print(mResumed); writer.print(" mStopped="); 539 writer.print(mStopped); writer.print(" mReallyStopped="); 540 writer.println(mReallyStopped); 541 writer.print(innerPrefix); writer.print("mLoadersStarted="); 542 writer.println(mLoadersStarted); 543 if (mLoaderManager != null) { 544 writer.print(prefix); writer.print("Loader Manager "); 545 writer.print(Integer.toHexString(System.identityHashCode(mLoaderManager))); 546 writer.println(":"); 547 mLoaderManager.dump(prefix + " ", fd, writer, args); 548 } 549 mFragments.dump(prefix, fd, writer, args); 550 } 551 552 void doReallyStop(boolean retaining) { 553 if (!mReallyStopped) { 554 mReallyStopped = true; 555 mHandler.removeMessages(MSG_REALLY_STOPPED); 556 onReallyStop(retaining); 557 } 558 } 559 560 /** 561 * Pre-HC, we didn't have a way to determine whether an activity was 562 * being stopped for a config change or not until we saw 563 * onRetainNonConfigurationInstance() called after onStop(). However 564 * we need to know this, to know whether to retain fragments. This will 565 * tell us what we need to know. 566 */ 567 void onReallyStop(boolean retaining) { 568 if (mLoadersStarted) { 569 mLoadersStarted = false; 570 if (mLoaderManager != null) { 571 if (!retaining) { 572 mLoaderManager.doStop(); 573 } else { 574 mLoaderManager.doRetain(); 575 } 576 } 577 } 578 579 mFragments.dispatchReallyStop(retaining); 580 } 581 582 // ------------------------------------------------------------------------ 583 // FRAGMENT SUPPORT 584 // ------------------------------------------------------------------------ 585 586 /** 587 * Called when a fragment is attached to the activity. 588 */ 589 public void onAttachFragment(Fragment fragment) { 590 } 591 592 /** 593 * Return the FragmentManager for interacting with fragments associated 594 * with this activity. 595 */ 596 public FragmentManager getSupportFragmentManager() { 597 return mFragments; 598 } 599 600 /** 601 * Modifies the standard behavior to allow results to be delivered to fragments. 602 * This imposes a restriction that requestCode be <= 0xffff. 603 */ 604 @Override 605 public void startActivityForResult(Intent intent, int requestCode) { 606 if ((requestCode&0xffff0000) != 0) { 607 throw new IllegalArgumentException("Can only use lower 16 bits for requestCode"); 608 } 609 super.startActivityForResult(intent, requestCode); 610 } 611 612 /** 613 * Called by Fragment.startActivityForResult() to implement its behavior. 614 */ 615 public void startActivityFromFragment(Fragment fragment, Intent intent, 616 int requestCode) { 617 if ((requestCode&0xffff0000) != 0) { 618 throw new IllegalArgumentException("Can only use lower 16 bits for requestCode"); 619 } 620 super.startActivityForResult(intent, (fragment.mIndex+1)<<16 + (requestCode*0xffff)); 621 } 622 623 void invalidateFragmentIndex(int index) { 624 //Log.v(TAG, "invalidateFragmentIndex: index=" + index); 625 if (mAllLoaderManagers != null) { 626 LoaderManagerImpl lm = mAllLoaderManagers.get(index); 627 if (lm != null) { 628 lm.doDestroy(); 629 } 630 mAllLoaderManagers.remove(index); 631 } 632 } 633 634 // ------------------------------------------------------------------------ 635 // LOADER SUPPORT 636 // ------------------------------------------------------------------------ 637 638 /** 639 * Return the LoaderManager for this fragment, creating it if needed. 640 */ 641 public LoaderManager getSupportLoaderManager() { 642 if (mLoaderManager != null) { 643 return mLoaderManager; 644 } 645 mCheckedForLoaderManager = true; 646 mLoaderManager = getLoaderManager(-1, mLoadersStarted, true); 647 return mLoaderManager; 648 } 649 650 LoaderManagerImpl getLoaderManager(int index, boolean started, boolean create) { 651 if (mAllLoaderManagers == null) { 652 mAllLoaderManagers = new HCSparseArray<LoaderManagerImpl>(); 653 } 654 LoaderManagerImpl lm = mAllLoaderManagers.get(index); 655 if (lm == null) { 656 if (create) { 657 lm = new LoaderManagerImpl(this, started); 658 mAllLoaderManagers.put(index, lm); 659 } 660 } else { 661 lm.updateActivity(this); 662 } 663 return lm; 664 } 665} 666