1 /* 2 * Copyright (C) 2006 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 android.app; 18 19 import android.content.Intent; 20 import android.content.pm.ActivityInfo; 21 import android.os.Binder; 22 import android.os.Bundle; 23 import android.util.Log; 24 import android.view.Window; 25 26 import java.util.ArrayList; 27 import java.util.HashMap; 28 import java.util.Map; 29 30 /** 31 * <p>Helper class for managing multiple running embedded activities in the same 32 * process. This class is not normally used directly, but rather created for 33 * you as part of the {@link android.app.ActivityGroup} implementation. 34 * 35 * @see ActivityGroup 36 * 37 * @deprecated Use the new {@link Fragment} and {@link FragmentManager} APIs 38 * instead; these are also 39 * available on older platforms through the Android compatibility package. 40 */ 41 @Deprecated 42 public class LocalActivityManager { 43 private static final String TAG = "LocalActivityManager"; 44 private static final boolean localLOGV = false; 45 46 // Internal token for an Activity being managed by LocalActivityManager. 47 private static class LocalActivityRecord extends Binder { 48 LocalActivityRecord(String _id, Intent _intent) { 49 id = _id; 50 intent = _intent; 51 } 52 53 final String id; // Unique name of this record. 54 Intent intent; // Which activity to run here. 55 ActivityInfo activityInfo; // Package manager info about activity. 56 Activity activity; // Currently instantiated activity. 57 Window window; // Activity's top-level window. 58 Bundle instanceState; // Last retrieved freeze state. 59 int curState = RESTORED; // Current state the activity is in. 60 } 61 62 static final int RESTORED = 0; // State restored, but no startActivity(). 63 static final int INITIALIZING = 1; // Ready to launch (after startActivity()). 64 static final int CREATED = 2; // Created, not started or resumed. 65 static final int STARTED = 3; // Created and started, not resumed. 66 static final int RESUMED = 4; // Created started and resumed. 67 static final int DESTROYED = 5; // No longer with us. 68 69 /** Thread our activities are running in. */ 70 private final ActivityThread mActivityThread; 71 /** The containing activity that owns the activities we create. */ 72 private final Activity mParent; 73 74 /** The activity that is currently resumed. */ 75 private LocalActivityRecord mResumed; 76 /** id -> record of all known activities. */ 77 private final Map<String, LocalActivityRecord> mActivities 78 = new HashMap<String, LocalActivityRecord>(); 79 /** array of all known activities for easy iterating. */ 80 private final ArrayList<LocalActivityRecord> mActivityArray 81 = new ArrayList<LocalActivityRecord>(); 82 83 /** True if only one activity can be resumed at a time */ 84 private boolean mSingleMode; 85 86 /** Set to true once we find out the container is finishing. */ 87 private boolean mFinishing; 88 89 /** Current state the owner (ActivityGroup) is in */ 90 private int mCurState = INITIALIZING; 91 92 /** String ids of running activities starting with least recently used. */ 93 // TODO: put back in stopping of activities. 94 //private List<LocalActivityRecord> mLRU = new ArrayList(); 95 96 /** 97 * Create a new LocalActivityManager for holding activities running within 98 * the given <var>parent</var>. 99 * 100 * @param parent the host of the embedded activities 101 * @param singleMode True if the LocalActivityManger should keep a maximum 102 * of one activity resumed 103 */ 104 public LocalActivityManager(Activity parent, boolean singleMode) { 105 mActivityThread = ActivityThread.currentActivityThread(); 106 mParent = parent; 107 mSingleMode = singleMode; 108 } 109 110 private void moveToState(LocalActivityRecord r, int desiredState) { 111 if (r.curState == RESTORED || r.curState == DESTROYED) { 112 // startActivity() has not yet been called, so nothing to do. 113 return; 114 } 115 116 if (r.curState == INITIALIZING) { 117 // Get the lastNonConfigurationInstance for the activity 118 HashMap<String, Object> lastNonConfigurationInstances = 119 mParent.getLastNonConfigurationChildInstances(); 120 Object instanceObj = null; 121 if (lastNonConfigurationInstances != null) { 122 instanceObj = lastNonConfigurationInstances.get(r.id); 123 } 124 Activity.NonConfigurationInstances instance = null; 125 if (instanceObj != null) { 126 instance = new Activity.NonConfigurationInstances(); 127 instance.activity = instanceObj; 128 } 129 130 // We need to have always created the activity. 131 if (localLOGV) Log.v(TAG, r.id + ": starting " + r.intent); 132 if (r.activityInfo == null) { 133 r.activityInfo = mActivityThread.resolveActivityInfo(r.intent); 134 } 135 r.activity = mActivityThread.startActivityNow( 136 mParent, r.id, r.intent, r.activityInfo, r, r.instanceState, instance); 137 if (r.activity == null) { 138 return; 139 } 140 r.window = r.activity.getWindow(); 141 r.instanceState = null; 142 r.curState = STARTED; 143 144 if (desiredState == RESUMED) { 145 if (localLOGV) Log.v(TAG, r.id + ": resuming"); 146 mActivityThread.performResumeActivity(r, true); 147 r.curState = RESUMED; 148 } 149 150 // Don't do anything more here. There is an important case: 151 // if this is being done as part of onCreate() of the group, then 152 // the launching of the activity gets its state a little ahead 153 // of our own (it is now STARTED, while we are only CREATED). 154 // If we just leave things as-is, we'll deal with it as the 155 // group's state catches up. 156 return; 157 } 158 159 switch (r.curState) { 160 case CREATED: 161 if (desiredState == STARTED) { 162 if (localLOGV) Log.v(TAG, r.id + ": restarting"); 163 mActivityThread.performRestartActivity(r); 164 r.curState = STARTED; 165 } 166 if (desiredState == RESUMED) { 167 if (localLOGV) Log.v(TAG, r.id + ": restarting and resuming"); 168 mActivityThread.performRestartActivity(r); 169 mActivityThread.performResumeActivity(r, true); 170 r.curState = RESUMED; 171 } 172 return; 173 174 case STARTED: 175 if (desiredState == RESUMED) { 176 // Need to resume it... 177 if (localLOGV) Log.v(TAG, r.id + ": resuming"); 178 mActivityThread.performResumeActivity(r, true); 179 r.instanceState = null; 180 r.curState = RESUMED; 181 } 182 if (desiredState == CREATED) { 183 if (localLOGV) Log.v(TAG, r.id + ": stopping"); 184 mActivityThread.performStopActivity(r, false); 185 r.curState = CREATED; 186 } 187 return; 188 189 case RESUMED: 190 if (desiredState == STARTED) { 191 if (localLOGV) Log.v(TAG, r.id + ": pausing"); 192 performPause(r, mFinishing); 193 r.curState = STARTED; 194 } 195 if (desiredState == CREATED) { 196 if (localLOGV) Log.v(TAG, r.id + ": pausing"); 197 performPause(r, mFinishing); 198 if (localLOGV) Log.v(TAG, r.id + ": stopping"); 199 mActivityThread.performStopActivity(r, false); 200 r.curState = CREATED; 201 } 202 return; 203 } 204 } 205 206 private void performPause(LocalActivityRecord r, boolean finishing) { 207 boolean needState = r.instanceState == null; 208 Bundle instanceState = mActivityThread.performPauseActivity(r, 209 finishing, needState); 210 if (needState) { 211 r.instanceState = instanceState; 212 } 213 } 214 215 /** 216 * Start a new activity running in the group. Every activity you start 217 * must have a unique string ID associated with it -- this is used to keep 218 * track of the activity, so that if you later call startActivity() again 219 * on it the same activity object will be retained. 220 * 221 * <p>When there had previously been an activity started under this id, 222 * it may either be destroyed and a new one started, or the current 223 * one re-used, based on these conditions, in order:</p> 224 * 225 * <ul> 226 * <li> If the Intent maps to a different activity component than is 227 * currently running, the current activity is finished and a new one 228 * started. 229 * <li> If the current activity uses a non-multiple launch mode (such 230 * as singleTop), or the Intent has the 231 * {@link Intent#FLAG_ACTIVITY_SINGLE_TOP} flag set, then the current 232 * activity will remain running and its 233 * {@link Activity#onNewIntent(Intent) Activity.onNewIntent()} method 234 * called. 235 * <li> If the new Intent is the same (excluding extras) as the previous 236 * one, and the new Intent does not have the 237 * {@link Intent#FLAG_ACTIVITY_CLEAR_TOP} set, then the current activity 238 * will remain running as-is. 239 * <li> Otherwise, the current activity will be finished and a new 240 * one started. 241 * </ul> 242 * 243 * <p>If the given Intent can not be resolved to an available Activity, 244 * this method throws {@link android.content.ActivityNotFoundException}. 245 * 246 * <p>Warning: There is an issue where, if the Intent does not 247 * include an explicit component, we can restore the state for a different 248 * activity class than was previously running when the state was saved (if 249 * the set of available activities changes between those points). 250 * 251 * @param id Unique identifier of the activity to be started 252 * @param intent The Intent describing the activity to be started 253 * 254 * @return Returns the window of the activity. The caller needs to take 255 * care of adding this window to a view hierarchy, and likewise dealing 256 * with removing the old window if the activity has changed. 257 * 258 * @throws android.content.ActivityNotFoundException 259 */ 260 public Window startActivity(String id, Intent intent) { 261 if (mCurState == INITIALIZING) { 262 throw new IllegalStateException( 263 "Activities can't be added until the containing group has been created."); 264 } 265 266 boolean adding = false; 267 boolean sameIntent = false; 268 269 ActivityInfo aInfo = null; 270 271 // Already have information about the new activity id? 272 LocalActivityRecord r = mActivities.get(id); 273 if (r == null) { 274 // Need to create it... 275 r = new LocalActivityRecord(id, intent); 276 adding = true; 277 } else if (r.intent != null) { 278 sameIntent = r.intent.filterEquals(intent); 279 if (sameIntent) { 280 // We are starting the same activity. 281 aInfo = r.activityInfo; 282 } 283 } 284 if (aInfo == null) { 285 aInfo = mActivityThread.resolveActivityInfo(intent); 286 } 287 288 // Pause the currently running activity if there is one and only a single 289 // activity is allowed to be running at a time. 290 if (mSingleMode) { 291 LocalActivityRecord old = mResumed; 292 293 // If there was a previous activity, and it is not the current 294 // activity, we need to stop it. 295 if (old != null && old != r && mCurState == RESUMED) { 296 moveToState(old, STARTED); 297 } 298 } 299 300 if (adding) { 301 // It's a brand new world. 302 mActivities.put(id, r); 303 mActivityArray.add(r); 304 } else if (r.activityInfo != null) { 305 // If the new activity is the same as the current one, then 306 // we may be able to reuse it. 307 if (aInfo == r.activityInfo || 308 (aInfo.name.equals(r.activityInfo.name) && 309 aInfo.packageName.equals(r.activityInfo.packageName))) { 310 if (aInfo.launchMode != ActivityInfo.LAUNCH_MULTIPLE || 311 (intent.getFlags()&Intent.FLAG_ACTIVITY_SINGLE_TOP) != 0) { 312 // The activity wants onNewIntent() called. 313 ArrayList<Intent> intents = new ArrayList<Intent>(1); 314 intents.add(intent); 315 if (localLOGV) Log.v(TAG, r.id + ": new intent"); 316 mActivityThread.performNewIntents(r, intents); 317 r.intent = intent; 318 moveToState(r, mCurState); 319 if (mSingleMode) { 320 mResumed = r; 321 } 322 return r.window; 323 } 324 if (sameIntent && 325 (intent.getFlags()&Intent.FLAG_ACTIVITY_CLEAR_TOP) == 0) { 326 // We are showing the same thing, so this activity is 327 // just resumed and stays as-is. 328 r.intent = intent; 329 moveToState(r, mCurState); 330 if (mSingleMode) { 331 mResumed = r; 332 } 333 return r.window; 334 } 335 } 336 337 // The new activity is different than the current one, or it 338 // is a multiple launch activity, so we need to destroy what 339 // is currently there. 340 performDestroy(r, true); 341 } 342 343 r.intent = intent; 344 r.curState = INITIALIZING; 345 r.activityInfo = aInfo; 346 347 moveToState(r, mCurState); 348 349 // When in single mode keep track of the current activity 350 if (mSingleMode) { 351 mResumed = r; 352 } 353 return r.window; 354 } 355 356 private Window performDestroy(LocalActivityRecord r, boolean finish) { 357 Window win; 358 win = r.window; 359 if (r.curState == RESUMED && !finish) { 360 performPause(r, finish); 361 } 362 if (localLOGV) Log.v(TAG, r.id + ": destroying"); 363 mActivityThread.performDestroyActivity(r, finish); 364 r.activity = null; 365 r.window = null; 366 if (finish) { 367 r.instanceState = null; 368 } 369 r.curState = DESTROYED; 370 return win; 371 } 372 373 /** 374 * Destroy the activity associated with a particular id. This activity 375 * will go through the normal lifecycle events and fine onDestroy(), and 376 * then the id removed from the group. 377 * 378 * @param id Unique identifier of the activity to be destroyed 379 * @param finish If true, this activity will be finished, so its id and 380 * all state are removed from the group. 381 * 382 * @return Returns the window that was used to display the activity, or 383 * null if there was none. 384 */ 385 public Window destroyActivity(String id, boolean finish) { 386 LocalActivityRecord r = mActivities.get(id); 387 Window win = null; 388 if (r != null) { 389 win = performDestroy(r, finish); 390 if (finish) { 391 mActivities.remove(id); 392 mActivityArray.remove(r); 393 } 394 } 395 return win; 396 } 397 398 /** 399 * Retrieve the Activity that is currently running. 400 * 401 * @return the currently running (resumed) Activity, or null if there is 402 * not one 403 * 404 * @see #startActivity 405 * @see #getCurrentId 406 */ 407 public Activity getCurrentActivity() { 408 return mResumed != null ? mResumed.activity : null; 409 } 410 411 /** 412 * Retrieve the ID of the activity that is currently running. 413 * 414 * @return the ID of the currently running (resumed) Activity, or null if 415 * there is not one 416 * 417 * @see #startActivity 418 * @see #getCurrentActivity 419 */ 420 public String getCurrentId() { 421 return mResumed != null ? mResumed.id : null; 422 } 423 424 /** 425 * Return the Activity object associated with a string ID. 426 * 427 * @see #startActivity 428 * 429 * @return the associated Activity object, or null if the id is unknown or 430 * its activity is not currently instantiated 431 */ 432 public Activity getActivity(String id) { 433 LocalActivityRecord r = mActivities.get(id); 434 return r != null ? r.activity : null; 435 } 436 437 /** 438 * Restore a state that was previously returned by {@link #saveInstanceState}. This 439 * adds to the activity group information about all activity IDs that had 440 * previously been saved, even if they have not been started yet, so if the 441 * user later navigates to them the correct state will be restored. 442 * 443 * <p>Note: This does <b>not</b> change the current running activity, or 444 * start whatever activity was previously running when the state was saved. 445 * That is up to the client to do, in whatever way it thinks is best. 446 * 447 * @param state a previously saved state; does nothing if this is null 448 * 449 * @see #saveInstanceState 450 */ 451 public void dispatchCreate(Bundle state) { 452 if (state != null) { 453 for (String id : state.keySet()) { 454 try { 455 final Bundle astate = state.getBundle(id); 456 LocalActivityRecord r = mActivities.get(id); 457 if (r != null) { 458 r.instanceState = astate; 459 } else { 460 r = new LocalActivityRecord(id, null); 461 r.instanceState = astate; 462 mActivities.put(id, r); 463 mActivityArray.add(r); 464 } 465 } catch (Exception e) { 466 // Recover from -all- app errors. 467 Log.e(TAG, "Exception thrown when restoring LocalActivityManager state", e); 468 } 469 } 470 } 471 472 mCurState = CREATED; 473 } 474 475 /** 476 * Retrieve the state of all activities known by the group. For 477 * activities that have previously run and are now stopped or finished, the 478 * last saved state is used. For the current running activity, its 479 * {@link Activity#onSaveInstanceState} is called to retrieve its current state. 480 * 481 * @return a Bundle holding the newly created state of all known activities 482 * 483 * @see #dispatchCreate 484 */ 485 public Bundle saveInstanceState() { 486 Bundle state = null; 487 488 // FIXME: child activities will freeze as part of onPaused. Do we 489 // need to do this here? 490 final int N = mActivityArray.size(); 491 for (int i=0; i<N; i++) { 492 final LocalActivityRecord r = mActivityArray.get(i); 493 if (state == null) { 494 state = new Bundle(); 495 } 496 if ((r.instanceState != null || r.curState == RESUMED) 497 && r.activity != null) { 498 // We need to save the state now, if we don't currently 499 // already have it or the activity is currently resumed. 500 final Bundle childState = new Bundle(); 501 r.activity.performSaveInstanceState(childState); 502 r.instanceState = childState; 503 } 504 if (r.instanceState != null) { 505 state.putBundle(r.id, r.instanceState); 506 } 507 } 508 509 return state; 510 } 511 512 /** 513 * Called by the container activity in its {@link Activity#onResume} so 514 * that LocalActivityManager can perform the corresponding action on the 515 * activities it holds. 516 * 517 * @see Activity#onResume 518 */ 519 public void dispatchResume() { 520 mCurState = RESUMED; 521 if (mSingleMode) { 522 if (mResumed != null) { 523 moveToState(mResumed, RESUMED); 524 } 525 } else { 526 final int N = mActivityArray.size(); 527 for (int i=0; i<N; i++) { 528 moveToState(mActivityArray.get(i), RESUMED); 529 } 530 } 531 } 532 533 /** 534 * Called by the container activity in its {@link Activity#onPause} so 535 * that LocalActivityManager can perform the corresponding action on the 536 * activities it holds. 537 * 538 * @param finishing set to true if the parent activity has been finished; 539 * this can be determined by calling 540 * Activity.isFinishing() 541 * 542 * @see Activity#onPause 543 * @see Activity#isFinishing 544 */ 545 public void dispatchPause(boolean finishing) { 546 if (finishing) { 547 mFinishing = true; 548 } 549 mCurState = STARTED; 550 if (mSingleMode) { 551 if (mResumed != null) { 552 moveToState(mResumed, STARTED); 553 } 554 } else { 555 final int N = mActivityArray.size(); 556 for (int i=0; i<N; i++) { 557 LocalActivityRecord r = mActivityArray.get(i); 558 if (r.curState == RESUMED) { 559 moveToState(r, STARTED); 560 } 561 } 562 } 563 } 564 565 /** 566 * Called by the container activity in its {@link Activity#onStop} so 567 * that LocalActivityManager can perform the corresponding action on the 568 * activities it holds. 569 * 570 * @see Activity#onStop 571 */ 572 public void dispatchStop() { 573 mCurState = CREATED; 574 final int N = mActivityArray.size(); 575 for (int i=0; i<N; i++) { 576 LocalActivityRecord r = mActivityArray.get(i); 577 moveToState(r, CREATED); 578 } 579 } 580 581 /** 582 * Call onRetainNonConfigurationInstance on each child activity and store the 583 * results in a HashMap by id. Only construct the HashMap if there is a non-null 584 * object to store. Note that this does not support nested ActivityGroups. 585 * 586 * {@hide} 587 */ 588 public HashMap<String,Object> dispatchRetainNonConfigurationInstance() { 589 HashMap<String,Object> instanceMap = null; 590 591 final int N = mActivityArray.size(); 592 for (int i=0; i<N; i++) { 593 LocalActivityRecord r = mActivityArray.get(i); 594 if ((r != null) && (r.activity != null)) { 595 Object instance = r.activity.onRetainNonConfigurationInstance(); 596 if (instance != null) { 597 if (instanceMap == null) { 598 instanceMap = new HashMap<String,Object>(); 599 } 600 instanceMap.put(r.id, instance); 601 } 602 } 603 } 604 return instanceMap; 605 } 606 607 /** 608 * Remove all activities from this LocalActivityManager, performing an 609 * {@link Activity#onDestroy} on any that are currently instantiated. 610 */ 611 public void removeAllActivities() { 612 dispatchDestroy(true); 613 } 614 615 /** 616 * Called by the container activity in its {@link Activity#onDestroy} so 617 * that LocalActivityManager can perform the corresponding action on the 618 * activities it holds. 619 * 620 * @see Activity#onDestroy 621 */ 622 public void dispatchDestroy(boolean finishing) { 623 final int N = mActivityArray.size(); 624 for (int i=0; i<N; i++) { 625 LocalActivityRecord r = mActivityArray.get(i); 626 if (localLOGV) Log.v(TAG, r.id + ": destroying"); 627 mActivityThread.performDestroyActivity(r, finishing); 628 } 629 mActivities.clear(); 630 mActivityArray.clear(); 631 } 632 } 633