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