FragmentActivity.java revision 9277b9e4419c1f0b5236d9b573a7cc0b23d56402
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 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>Prior to Honeycomb (3.0), an activity's state was saved before pausing. 51 * Fragments are a significant amount of new state, and dynamic enough that one 52 * often wants them to change between pausing and stopping. These classes 53 * throw an exception if you try to change the fragment state after it has been 54 * saved, to avoid accidental loss of UI state. However this is too restrictive 55 * prior to Honeycomb, where the state is saved before pausing. To address this, 56 * when running on platforms prior to Honeycomb an exception will not be thrown 57 * if you change fragments between the state save and the activity being stopped. 58 * This means that is some cases if the activity is restored from its last saved 59 * state, this may be a snapshot slightly before what the user last saw.</p> 60 * </ul> 61 */ 62public class FragmentActivity extends Activity { 63 private static final String TAG = "FragmentActivity"; 64 65 private static final String FRAGMENTS_TAG = "android:support:fragments"; 66 67 // This is the SDK API version of Honeycomb (3.0). 68 private static final int HONEYCOMB = 11; 69 70 static final int MSG_REALLY_STOPPED = 1; 71 72 final Handler mHandler = new Handler() { 73 @Override 74 public void handleMessage(Message msg) { 75 switch (msg.what) { 76 case MSG_REALLY_STOPPED: 77 if (mStopped) { 78 doReallyStop(false); 79 } 80 break; 81 default: 82 super.handleMessage(msg); 83 } 84 } 85 86 }; 87 final FragmentManagerImpl mFragments = new FragmentManagerImpl(); 88 89 boolean mResumed; 90 boolean mStopped; 91 boolean mReallyStopped; 92 93 boolean mOptionsMenuInvalidated; 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 if (android.os.Build.VERSION.SDK_INT >= HONEYCOMB) { 198 return show; 199 } 200 // Prior to Honeycomb, the framework can't invalidate the options 201 // menu, so we must always say we have one in case the app later 202 // invalidates it and needs to have it shown. 203 return true; 204 } 205 return super.onCreatePanelMenu(featureId, menu); 206 } 207 208 /** 209 * Add support for inflating the <fragment> tag. 210 */ 211 @Override 212 public View onCreateView(String name, Context context, AttributeSet attrs) { 213 if (!"fragment".equals(name)) { 214 return super.onCreateView(name, context, attrs); 215 } 216 217 String fname = attrs.getAttributeValue(null, "class"); 218 TypedArray a = context.obtainStyledAttributes(attrs, FragmentTag.Fragment); 219 if (fname == null) { 220 fname = a.getString(FragmentTag.Fragment_name); 221 } 222 int id = a.getResourceId(FragmentTag.Fragment_id, View.NO_ID); 223 String tag = a.getString(FragmentTag.Fragment_tag); 224 a.recycle(); 225 226 View parent = null; // NOTE: no way to get parent pre-Honeycomb. 227 int containerId = parent != null ? parent.getId() : 0; 228 if (containerId == View.NO_ID && id == View.NO_ID && tag == null) { 229 throw new IllegalArgumentException(attrs.getPositionDescription() 230 + ": Must specify unique android:id, android:tag, or have a parent with an id for " + fname); 231 } 232 233 // If we restored from a previous state, we may already have 234 // instantiated this fragment from the state and should use 235 // that instance instead of making a new one. 236 Fragment fragment = id != View.NO_ID ? mFragments.findFragmentById(id) : null; 237 if (fragment == null && tag != null) { 238 fragment = mFragments.findFragmentByTag(tag); 239 } 240 if (fragment == null && containerId != View.NO_ID) { 241 fragment = mFragments.findFragmentById(containerId); 242 } 243 244 if (FragmentManagerImpl.DEBUG) Log.v(TAG, "onCreateView: id=0x" 245 + Integer.toHexString(id) + " fname=" + fname 246 + " existing=" + fragment); 247 if (fragment == null) { 248 fragment = Fragment.instantiate(this, fname); 249 fragment.mFromLayout = true; 250 fragment.mFragmentId = id != 0 ? id : containerId; 251 fragment.mContainerId = containerId; 252 fragment.mTag = tag; 253 fragment.mInLayout = true; 254 fragment.mImmediateActivity = this; 255 fragment.mFragmentManager = mFragments; 256 fragment.onInflate(this, attrs, fragment.mSavedFragmentState); 257 mFragments.addFragment(fragment, true); 258 259 } else if (fragment.mInLayout) { 260 // A fragment already exists and it is not one we restored from 261 // previous state. 262 throw new IllegalArgumentException(attrs.getPositionDescription() 263 + ": Duplicate id 0x" + Integer.toHexString(id) 264 + ", tag " + tag + ", or parent id 0x" + Integer.toHexString(containerId) 265 + " with another fragment for " + fname); 266 } else { 267 // This fragment was retained from a previous instance; get it 268 // going now. 269 fragment.mInLayout = true; 270 fragment.mImmediateActivity = this; 271 // If this fragment is newly instantiated (either right now, or 272 // from last saved state), then give it the attributes to 273 // initialize itself. 274 if (!fragment.mRetaining) { 275 fragment.onInflate(this, attrs, fragment.mSavedFragmentState); 276 } 277 mFragments.moveToState(fragment); 278 } 279 280 if (fragment.mView == null) { 281 throw new IllegalStateException("Fragment " + fname 282 + " did not create a view."); 283 } 284 if (id != 0) { 285 fragment.mView.setId(id); 286 } 287 if (fragment.mView.getTag() == null) { 288 fragment.mView.setTag(tag); 289 } 290 return fragment.mView; 291 } 292 293 /** 294 * Destroy all fragments and loaders. 295 */ 296 @Override 297 protected void onDestroy() { 298 super.onDestroy(); 299 300 doReallyStop(false); 301 302 mFragments.dispatchDestroy(); 303 if (mLoaderManager != null) { 304 mLoaderManager.doDestroy(); 305 } 306 } 307 308 /** 309 * Take care of calling onBackPressed() for pre-Eclair platforms. 310 */ 311 @Override 312 public boolean onKeyDown(int keyCode, KeyEvent event) { 313 if (android.os.Build.VERSION.SDK_INT < 5 /* ECLAIR */ 314 && keyCode == KeyEvent.KEYCODE_BACK 315 && event.getRepeatCount() == 0) { 316 // Take care of calling this method on earlier versions of 317 // the platform where it doesn't exist. 318 onBackPressed(); 319 return true; 320 } 321 322 return super.onKeyDown(keyCode, event); 323 } 324 325 /** 326 * Dispatch onLowMemory() to all fragments. 327 */ 328 @Override 329 public void onLowMemory() { 330 super.onLowMemory(); 331 mFragments.dispatchLowMemory(); 332 } 333 334 /** 335 * Dispatch context and options menu to fragments. 336 */ 337 @Override 338 public boolean onMenuItemSelected(int featureId, MenuItem item) { 339 if (super.onMenuItemSelected(featureId, item)) { 340 return true; 341 } 342 343 switch (featureId) { 344 case Window.FEATURE_OPTIONS_PANEL: 345 return mFragments.dispatchOptionsItemSelected(item); 346 347 case Window.FEATURE_CONTEXT_MENU: 348 return mFragments.dispatchContextItemSelected(item); 349 350 default: 351 return false; 352 } 353 } 354 355 /** 356 * Call onOptionsMenuClosed() on fragments. 357 */ 358 @Override 359 public void onPanelClosed(int featureId, Menu menu) { 360 switch (featureId) { 361 case Window.FEATURE_OPTIONS_PANEL: 362 mFragments.dispatchOptionsMenuClosed(menu); 363 break; 364 } 365 super.onPanelClosed(featureId, menu); 366 } 367 368 /** 369 * Dispatch onPause() to fragments. 370 */ 371 @Override 372 protected void onPause() { 373 super.onPause(); 374 mResumed = false; 375 mFragments.dispatchPause(); 376 } 377 378 /** 379 * Dispatch onActivityCreated() on fragments. 380 */ 381 @Override 382 protected void onPostCreate(Bundle savedInstanceState) { 383 super.onPostCreate(savedInstanceState); 384 mFragments.dispatchActivityCreated(); 385 } 386 387 /** 388 * Dispatch onResume() to fragments. 389 */ 390 @Override 391 protected void onPostResume() { 392 super.onPostResume(); 393 mFragments.dispatchResume(); 394 mFragments.execPendingActions(); 395 } 396 397 /** 398 * Dispatch onPrepareOptionsMenu() to fragments. 399 */ 400 @Override 401 public boolean onPreparePanel(int featureId, View view, Menu menu) { 402 if (featureId == Window.FEATURE_OPTIONS_PANEL && menu != null) { 403 if (mOptionsMenuInvalidated) { 404 mOptionsMenuInvalidated = false; 405 menu.clear(); 406 onCreatePanelMenu(featureId, menu); 407 } 408 boolean goforit = super.onPreparePanel(featureId, view, menu); 409 goforit |= mFragments.dispatchPrepareOptionsMenu(menu); 410 return goforit && menu.hasVisibleItems(); 411 } 412 return super.onPreparePanel(featureId, view, menu); 413 } 414 415 /** 416 * Ensure any outstanding fragment transactions have been committed. 417 */ 418 @Override 419 protected void onResume() { 420 super.onResume(); 421 mResumed = true; 422 mFragments.execPendingActions(); 423 } 424 425 /** 426 * Retain all appropriate fragment and loader state. You can NOT 427 * override this yourself! 428 */ 429 @Override 430 public final Object onRetainNonConfigurationInstance() { 431 if (mStopped) { 432 doReallyStop(true); 433 } 434 435 ArrayList<Fragment> fragments = mFragments.retainNonConfig(); 436 boolean retainLoaders = false; 437 if (mAllLoaderManagers != null) { 438 // prune out any loader managers that were already stopped and so 439 // have nothing useful to retain. 440 for (int i=mAllLoaderManagers.size()-1; i>=0; i--) { 441 LoaderManagerImpl lm = mAllLoaderManagers.valueAt(i); 442 if (lm.mRetaining) { 443 retainLoaders = true; 444 } else { 445 lm.doDestroy(); 446 mAllLoaderManagers.removeAt(i); 447 } 448 } 449 } 450 if (fragments == null && !retainLoaders) { 451 return null; 452 } 453 454 NonConfigurationInstances nci = new NonConfigurationInstances(); 455 nci.activity = null; 456 nci.children = null; 457 nci.fragments = fragments; 458 nci.loaders = mAllLoaderManagers; 459 return nci; 460 } 461 462 /** 463 * Save all appropriate fragment state. 464 */ 465 @Override 466 protected void onSaveInstanceState(Bundle outState) { 467 super.onSaveInstanceState(outState); 468 Parcelable p = mFragments.saveAllState(); 469 if (p != null) { 470 outState.putParcelable(FRAGMENTS_TAG, p); 471 } 472 } 473 474 /** 475 * Dispatch onStart() to all fragments. Ensure any created loaders are 476 * now started. 477 */ 478 @Override 479 protected void onStart() { 480 super.onStart(); 481 482 mStopped = false; 483 mHandler.removeMessages(MSG_REALLY_STOPPED); 484 485 mFragments.noteStateNotSaved(); 486 mFragments.execPendingActions(); 487 488 489 if (!mLoadersStarted) { 490 mLoadersStarted = true; 491 if (mLoaderManager != null) { 492 mLoaderManager.doStart(); 493 } else if (!mCheckedForLoaderManager) { 494 mLoaderManager = getLoaderManager(-1, mLoadersStarted, false); 495 } 496 mCheckedForLoaderManager = true; 497 } 498 // NOTE: HC onStart goes here. 499 500 mFragments.dispatchStart(); 501 if (mAllLoaderManagers != null) { 502 for (int i=mAllLoaderManagers.size()-1; i>=0; i--) { 503 mAllLoaderManagers.valueAt(i).finishRetain(); 504 } 505 } 506 } 507 508 /** 509 * Dispatch onStop() to all fragments. Ensure all loaders are stopped. 510 */ 511 @Override 512 protected void onStop() { 513 super.onStop(); 514 515 mStopped = true; 516 mHandler.sendEmptyMessage(MSG_REALLY_STOPPED); 517 518 mFragments.dispatchStop(); 519 } 520 521 // ------------------------------------------------------------------------ 522 // NEW METHODS 523 // ------------------------------------------------------------------------ 524 525 void supportInvalidateOptionsMenu() { 526 if (android.os.Build.VERSION.SDK_INT >= HONEYCOMB) { 527 // If we are running on HC or greater, we can use the framework 528 // API to invalidate the options menu. 529 ActivityCompatHoneycomb.invalidateOptionsMenu(this); 530 return; 531 } 532 533 // Whoops, older platform... we'll use a hack, to manually rebuild 534 // the options menu the next time it is prepared. 535 mOptionsMenuInvalidated = true; 536 } 537 538 /** 539 * Print the Activity's state into the given stream. This gets invoked if 540 * you run "adb shell dumpsys activity <activity_component_name>". 541 * 542 * @param prefix Desired prefix to prepend at each line of output. 543 * @param fd The raw file descriptor that the dump is being sent to. 544 * @param writer The PrintWriter to which you should dump your state. This will be 545 * closed for you after you return. 546 * @param args additional arguments to the dump request. 547 */ 548 public void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) { 549 if (android.os.Build.VERSION.SDK_INT >= HONEYCOMB) { 550 // XXX This can only work if we can call the super-class impl. :/ 551 //ActivityCompatHoneycomb.dump(this, prefix, fd, writer, args); 552 } 553 writer.print(prefix); writer.print("Local FragmentActivity "); 554 writer.print(Integer.toHexString(System.identityHashCode(this))); 555 writer.println(" State:"); 556 String innerPrefix = prefix + " "; 557 writer.print(innerPrefix); writer.print("mResumed="); 558 writer.print(mResumed); writer.print(" mStopped="); 559 writer.print(mStopped); writer.print(" mReallyStopped="); 560 writer.println(mReallyStopped); 561 writer.print(innerPrefix); writer.print("mLoadersStarted="); 562 writer.println(mLoadersStarted); 563 if (mLoaderManager != null) { 564 writer.print(prefix); writer.print("Loader Manager "); 565 writer.print(Integer.toHexString(System.identityHashCode(mLoaderManager))); 566 writer.println(":"); 567 mLoaderManager.dump(prefix + " ", fd, writer, args); 568 } 569 mFragments.dump(prefix, fd, writer, args); 570 } 571 572 void doReallyStop(boolean retaining) { 573 if (!mReallyStopped) { 574 mReallyStopped = true; 575 mHandler.removeMessages(MSG_REALLY_STOPPED); 576 onReallyStop(retaining); 577 } 578 } 579 580 /** 581 * Pre-HC, we didn't have a way to determine whether an activity was 582 * being stopped for a config change or not until we saw 583 * onRetainNonConfigurationInstance() called after onStop(). However 584 * we need to know this, to know whether to retain fragments. This will 585 * tell us what we need to know. 586 */ 587 void onReallyStop(boolean retaining) { 588 if (mLoadersStarted) { 589 mLoadersStarted = false; 590 if (mLoaderManager != null) { 591 if (!retaining) { 592 mLoaderManager.doStop(); 593 } else { 594 mLoaderManager.doRetain(); 595 } 596 } 597 } 598 599 mFragments.dispatchReallyStop(retaining); 600 } 601 602 // ------------------------------------------------------------------------ 603 // FRAGMENT SUPPORT 604 // ------------------------------------------------------------------------ 605 606 /** 607 * Called when a fragment is attached to the activity. 608 */ 609 public void onAttachFragment(Fragment fragment) { 610 } 611 612 /** 613 * Return the FragmentManager for interacting with fragments associated 614 * with this activity. 615 */ 616 public FragmentManager getSupportFragmentManager() { 617 return mFragments; 618 } 619 620 /** 621 * Modifies the standard behavior to allow results to be delivered to fragments. 622 * This imposes a restriction that requestCode be <= 0xffff. 623 */ 624 @Override 625 public void startActivityForResult(Intent intent, int requestCode) { 626 if (requestCode != -1 && (requestCode&0xffff0000) != 0) { 627 throw new IllegalArgumentException("Can only use lower 16 bits for requestCode"); 628 } 629 super.startActivityForResult(intent, requestCode); 630 } 631 632 /** 633 * Called by Fragment.startActivityForResult() to implement its behavior. 634 */ 635 public void startActivityFromFragment(Fragment fragment, Intent intent, 636 int requestCode) { 637 if (requestCode == -1) { 638 super.startActivityForResult(intent, -1); 639 return; 640 } 641 if ((requestCode&0xffff0000) != 0) { 642 throw new IllegalArgumentException("Can only use lower 16 bits for requestCode"); 643 } 644 super.startActivityForResult(intent, (fragment.mIndex+1)<<16 + (requestCode*0xffff)); 645 } 646 647 void invalidateFragmentIndex(int index) { 648 //Log.v(TAG, "invalidateFragmentIndex: index=" + index); 649 if (mAllLoaderManagers != null) { 650 LoaderManagerImpl lm = mAllLoaderManagers.get(index); 651 if (lm != null) { 652 lm.doDestroy(); 653 } 654 mAllLoaderManagers.remove(index); 655 } 656 } 657 658 // ------------------------------------------------------------------------ 659 // LOADER SUPPORT 660 // ------------------------------------------------------------------------ 661 662 /** 663 * Return the LoaderManager for this fragment, creating it if needed. 664 */ 665 public LoaderManager getSupportLoaderManager() { 666 if (mLoaderManager != null) { 667 return mLoaderManager; 668 } 669 mCheckedForLoaderManager = true; 670 mLoaderManager = getLoaderManager(-1, mLoadersStarted, true); 671 return mLoaderManager; 672 } 673 674 LoaderManagerImpl getLoaderManager(int index, boolean started, boolean create) { 675 if (mAllLoaderManagers == null) { 676 mAllLoaderManagers = new HCSparseArray<LoaderManagerImpl>(); 677 } 678 LoaderManagerImpl lm = mAllLoaderManagers.get(index); 679 if (lm == null) { 680 if (create) { 681 lm = new LoaderManagerImpl(this, started); 682 mAllLoaderManagers.put(index, lm); 683 } 684 } else { 685 lm.updateActivity(this); 686 } 687 return lm; 688 } 689} 690