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