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.Log;
24import android.view.Window;
25
26import java.util.ArrayList;
27import java.util.HashMap;
28import 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
42public 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