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