FragmentActivity.java revision 218c1e661578e2a17928f7dbb590b43d1c79aeb7
1/*
2 * Copyright (C) 2011 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.support.v4.app;
18
19import android.app.Activity;
20import android.content.Context;
21import android.content.Intent;
22import android.content.res.Configuration;
23import android.content.res.TypedArray;
24import android.os.Bundle;
25import android.os.Handler;
26import android.os.Message;
27import android.os.Parcelable;
28import android.util.AttributeSet;
29import android.util.Log;
30import android.view.KeyEvent;
31import android.view.Menu;
32import android.view.MenuItem;
33import android.view.View;
34import android.view.Window;
35
36import java.io.FileDescriptor;
37import java.io.PrintWriter;
38import java.util.ArrayList;
39import java.util.HashMap;
40
41/**
42 * Base class for activities that want to use the support-based Fragment and
43 * Loader APIs.
44 *
45 * <p>Known limitations:</p>
46 * <ul>
47 * <li> <p>When using the &lt;fragment> tag, this implementation can not
48 * use the parent view's ID as the new fragment's ID.  You must explicitly
49 * specify an ID (or tag) in the &lt;fragment>.</p>
50 * <li> <p>Prior to Honeycomb (3.0), an activity's state was saved before pausing.
51 * Fragments are a significant amount of new state, and dynamic enough that one
52 * often wants them to change between pausing and stopping.  These classes
53 * throw an exception if you try to change the fragment state after it has been
54 * saved, to avoid accidental loss of UI state.  However this is too restrictive
55 * prior to Honeycomb, where the state is saved before pausing.  To address this,
56 * when running on platforms prior to Honeycomb an exception will not be thrown
57 * if you change fragments between the state save and the activity being stopped.
58 * This means that is some cases if the activity is restored from its last saved
59 * state, this may be a snapshot slightly before what the user last saw.</p>
60 * </ul>
61 */
62public class FragmentActivity extends Activity {
63    private static final String TAG = "FragmentActivity";
64
65    private static final String FRAGMENTS_TAG = "android:support:fragments";
66
67    // This is the SDK API version of Honeycomb (3.0).
68    private static final int HONEYCOMB = 11;
69
70    static final int MSG_REALLY_STOPPED = 1;
71    static final int MSG_RESUME_PENDING = 2;
72
73    final Handler mHandler = new Handler() {
74        @Override
75        public void handleMessage(Message msg) {
76            switch (msg.what) {
77                case MSG_REALLY_STOPPED:
78                    if (mStopped) {
79                        doReallyStop(false);
80                    }
81                    break;
82                case MSG_RESUME_PENDING:
83                    mFragments.dispatchResume();
84                    mFragments.execPendingActions();
85                    break;
86                default:
87                    super.handleMessage(msg);
88            }
89        }
90
91    };
92    final FragmentManagerImpl mFragments = new FragmentManagerImpl();
93
94    boolean mCreated;
95    boolean mResumed;
96    boolean mStopped;
97    boolean mReallyStopped;
98    boolean mRetaining;
99
100    boolean mOptionsMenuInvalidated;
101
102    boolean mCheckedForLoaderManager;
103    boolean mLoadersStarted;
104    HCSparseArray<LoaderManagerImpl> mAllLoaderManagers;
105    LoaderManagerImpl mLoaderManager;
106
107    static final class NonConfigurationInstances {
108        Object activity;
109        HashMap<String, Object> children;
110        ArrayList<Fragment> fragments;
111        HCSparseArray<LoaderManagerImpl> loaders;
112    }
113
114    static class FragmentTag {
115        public static final int[] Fragment = {
116            0x01010003, 0x010100d0, 0x010100d1
117        };
118        public static final int Fragment_id = 1;
119        public static final int Fragment_name = 0;
120        public static final int Fragment_tag = 2;
121    }
122
123    // ------------------------------------------------------------------------
124    // HOOKS INTO ACTIVITY
125    // ------------------------------------------------------------------------
126
127    /**
128     * Dispatch incoming result to the correct fragment.
129     */
130    @Override
131    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
132        int index = requestCode>>16;
133        if (index != 0) {
134            index--;
135            if (mFragments.mActive == null || index < 0 || index >= mFragments.mActive.size()) {
136                Log.w(TAG, "Activity result fragment index out of range: 0x"
137                        + Integer.toHexString(requestCode));
138                return;
139            }
140            Fragment frag = mFragments.mActive.get(index);
141            if (frag == null) {
142                Log.w(TAG, "Activity result no fragment exists for index: 0x"
143                        + Integer.toHexString(requestCode));
144            }
145            frag.onActivityResult(requestCode&0xffff, resultCode, data);
146            return;
147        }
148
149        super.onActivityResult(requestCode, resultCode, data);
150    }
151
152    /**
153     * Take care of popping the fragment back stack or finishing the activity
154     * as appropriate.
155     */
156    public void onBackPressed() {
157        if (!mFragments.popBackStackImmediate()) {
158            finish();
159        }
160    }
161
162    /**
163     * Dispatch configuration change to all fragments.
164     */
165    @Override
166    public void onConfigurationChanged(Configuration newConfig) {
167        super.onConfigurationChanged(newConfig);
168        mFragments.dispatchConfigurationChanged(newConfig);
169    }
170
171    /**
172     * Perform initialization of all fragments and loaders.
173     */
174    @Override
175    protected void onCreate(Bundle savedInstanceState) {
176        mFragments.attachActivity(this);
177        // Old versions of the platform didn't do this!
178        if (getLayoutInflater().getFactory() == null) {
179            getLayoutInflater().setFactory(this);
180        }
181
182        super.onCreate(savedInstanceState);
183
184        NonConfigurationInstances nc = (NonConfigurationInstances)
185                getLastNonConfigurationInstance();
186        if (nc != null) {
187            mAllLoaderManagers = nc.loaders;
188        }
189        if (savedInstanceState != null) {
190            Parcelable p = savedInstanceState.getParcelable(FRAGMENTS_TAG);
191            mFragments.restoreAllState(p, nc != null ? nc.fragments : null);
192        }
193        mFragments.dispatchCreate();
194    }
195
196    /**
197     * Dispatch to Fragment.onCreateOptionsMenu().
198     */
199    @Override
200    public boolean onCreatePanelMenu(int featureId, Menu menu) {
201        if (featureId == Window.FEATURE_OPTIONS_PANEL) {
202            boolean show = super.onCreatePanelMenu(featureId, menu);
203            show |= mFragments.dispatchCreateOptionsMenu(menu, getMenuInflater());
204            if (android.os.Build.VERSION.SDK_INT >= HONEYCOMB) {
205                return show;
206            }
207            // Prior to Honeycomb, the framework can't invalidate the options
208            // menu, so we must always say we have one in case the app later
209            // invalidates it and needs to have it shown.
210            return true;
211        }
212        return super.onCreatePanelMenu(featureId, menu);
213    }
214
215    /**
216     * Add support for inflating the &lt;fragment> tag.
217     */
218    @Override
219    public View onCreateView(String name, Context context, AttributeSet attrs) {
220        if (!"fragment".equals(name)) {
221            return super.onCreateView(name, context, attrs);
222        }
223
224        String fname = attrs.getAttributeValue(null, "class");
225        TypedArray a =  context.obtainStyledAttributes(attrs, FragmentTag.Fragment);
226        if (fname == null) {
227            fname = a.getString(FragmentTag.Fragment_name);
228        }
229        int id = a.getResourceId(FragmentTag.Fragment_id, View.NO_ID);
230        String tag = a.getString(FragmentTag.Fragment_tag);
231        a.recycle();
232
233        View parent = null; // NOTE: no way to get parent pre-Honeycomb.
234        int containerId = parent != null ? parent.getId() : 0;
235        if (containerId == View.NO_ID && id == View.NO_ID && tag == null) {
236            throw new IllegalArgumentException(attrs.getPositionDescription()
237                    + ": Must specify unique android:id, android:tag, or have a parent with an id for " + fname);
238        }
239
240        // If we restored from a previous state, we may already have
241        // instantiated this fragment from the state and should use
242        // that instance instead of making a new one.
243        Fragment fragment = id != View.NO_ID ? mFragments.findFragmentById(id) : null;
244        if (fragment == null && tag != null) {
245            fragment = mFragments.findFragmentByTag(tag);
246        }
247        if (fragment == null && containerId != View.NO_ID) {
248            fragment = mFragments.findFragmentById(containerId);
249        }
250
251        if (FragmentManagerImpl.DEBUG) Log.v(TAG, "onCreateView: id=0x"
252                + Integer.toHexString(id) + " fname=" + fname
253                + " existing=" + fragment);
254        if (fragment == null) {
255            fragment = Fragment.instantiate(this, fname);
256            fragment.mFromLayout = true;
257            fragment.mFragmentId = id != 0 ? id : containerId;
258            fragment.mContainerId = containerId;
259            fragment.mTag = tag;
260            fragment.mInLayout = true;
261            fragment.mFragmentManager = mFragments;
262            fragment.onInflate(this, attrs, fragment.mSavedFragmentState);
263            mFragments.addFragment(fragment, true);
264
265        } else if (fragment.mInLayout) {
266            // A fragment already exists and it is not one we restored from
267            // previous state.
268            throw new IllegalArgumentException(attrs.getPositionDescription()
269                    + ": Duplicate id 0x" + Integer.toHexString(id)
270                    + ", tag " + tag + ", or parent id 0x" + Integer.toHexString(containerId)
271                    + " with another fragment for " + fname);
272        } else {
273            // This fragment was retained from a previous instance; get it
274            // going now.
275            fragment.mInLayout = true;
276            // If this fragment is newly instantiated (either right now, or
277            // from last saved state), then give it the attributes to
278            // initialize itself.
279            if (!fragment.mRetaining) {
280                fragment.onInflate(this, attrs, fragment.mSavedFragmentState);
281            }
282            mFragments.moveToState(fragment);
283        }
284
285        if (fragment.mView == null) {
286            throw new IllegalStateException("Fragment " + fname
287                    + " did not create a view.");
288        }
289        if (id != 0) {
290            fragment.mView.setId(id);
291        }
292        if (fragment.mView.getTag() == null) {
293            fragment.mView.setTag(tag);
294        }
295        return fragment.mView;
296    }
297
298    /**
299     * Destroy all fragments and loaders.
300     */
301    @Override
302    protected void onDestroy() {
303        super.onDestroy();
304
305        doReallyStop(false);
306
307        mFragments.dispatchDestroy();
308        if (mLoaderManager != null) {
309            mLoaderManager.doDestroy();
310        }
311    }
312
313    /**
314     * Take care of calling onBackPressed() for pre-Eclair platforms.
315     */
316    @Override
317    public boolean onKeyDown(int keyCode, KeyEvent event) {
318        if (android.os.Build.VERSION.SDK_INT < 5 /* ECLAIR */
319                && keyCode == KeyEvent.KEYCODE_BACK
320                && event.getRepeatCount() == 0) {
321            // Take care of calling this method on earlier versions of
322            // the platform where it doesn't exist.
323            onBackPressed();
324            return true;
325        }
326
327        return super.onKeyDown(keyCode, event);
328    }
329
330    /**
331     * Dispatch onLowMemory() to all fragments.
332     */
333    @Override
334    public void onLowMemory() {
335        super.onLowMemory();
336        mFragments.dispatchLowMemory();
337    }
338
339    /**
340     * Dispatch context and options menu to fragments.
341     */
342    @Override
343    public boolean onMenuItemSelected(int featureId, MenuItem item) {
344        if (super.onMenuItemSelected(featureId, item)) {
345            return true;
346        }
347
348        switch (featureId) {
349            case Window.FEATURE_OPTIONS_PANEL:
350                return mFragments.dispatchOptionsItemSelected(item);
351
352            case Window.FEATURE_CONTEXT_MENU:
353                return mFragments.dispatchContextItemSelected(item);
354
355            default:
356                return false;
357        }
358    }
359
360    /**
361     * Call onOptionsMenuClosed() on fragments.
362     */
363    @Override
364    public void onPanelClosed(int featureId, Menu menu) {
365        switch (featureId) {
366            case Window.FEATURE_OPTIONS_PANEL:
367                mFragments.dispatchOptionsMenuClosed(menu);
368                break;
369        }
370        super.onPanelClosed(featureId, menu);
371    }
372
373    /**
374     * Dispatch onPause() to fragments.
375     */
376    @Override
377    protected void onPause() {
378        super.onPause();
379        mResumed = false;
380        if (mHandler.hasMessages(MSG_RESUME_PENDING)) {
381            mHandler.removeMessages(MSG_RESUME_PENDING);
382            mFragments.dispatchResume();
383        }
384        mFragments.dispatchPause();
385    }
386
387    /**
388     * Dispatch onResume() to fragments.
389     */
390    @Override
391    protected void onResume() {
392        super.onResume();
393        mHandler.sendEmptyMessage(MSG_RESUME_PENDING);
394        mResumed = true;
395        mFragments.execPendingActions();
396    }
397
398    /**
399     * Dispatch onResume() to fragments.
400     */
401    @Override
402    protected void onPostResume() {
403        super.onPostResume();
404        mHandler.removeMessages(MSG_RESUME_PENDING);
405        mFragments.dispatchResume();
406        mFragments.execPendingActions();
407    }
408
409    /**
410     * Dispatch onPrepareOptionsMenu() to fragments.
411     */
412    @Override
413    public boolean onPreparePanel(int featureId, View view, Menu menu) {
414        if (featureId == Window.FEATURE_OPTIONS_PANEL && menu != null) {
415            if (mOptionsMenuInvalidated) {
416                mOptionsMenuInvalidated = false;
417                menu.clear();
418                onCreatePanelMenu(featureId, menu);
419            }
420            boolean goforit = super.onPreparePanel(featureId, view, menu);
421            goforit |= mFragments.dispatchPrepareOptionsMenu(menu);
422            return goforit && menu.hasVisibleItems();
423        }
424        return super.onPreparePanel(featureId, view, menu);
425    }
426
427    /**
428     * Retain all appropriate fragment and loader state.  You can NOT
429     * override this yourself!
430     */
431    @Override
432    public final Object onRetainNonConfigurationInstance() {
433        if (mStopped) {
434            doReallyStop(true);
435        }
436
437        ArrayList<Fragment> fragments = mFragments.retainNonConfig();
438        boolean retainLoaders = false;
439        if (mAllLoaderManagers != null) {
440            // prune out any loader managers that were already stopped and so
441            // have nothing useful to retain.
442            for (int i=mAllLoaderManagers.size()-1; i>=0; i--) {
443                LoaderManagerImpl lm = mAllLoaderManagers.valueAt(i);
444                if (lm.mRetaining) {
445                    retainLoaders = true;
446                } else {
447                    lm.doDestroy();
448                    mAllLoaderManagers.removeAt(i);
449                }
450            }
451        }
452        if (fragments == null && !retainLoaders) {
453            return null;
454        }
455
456        NonConfigurationInstances nci = new NonConfigurationInstances();
457        nci.activity = null;
458        nci.children = null;
459        nci.fragments = fragments;
460        nci.loaders = mAllLoaderManagers;
461        return nci;
462    }
463
464    /**
465     * Save all appropriate fragment state.
466     */
467    @Override
468    protected void onSaveInstanceState(Bundle outState) {
469        super.onSaveInstanceState(outState);
470        Parcelable p = mFragments.saveAllState();
471        if (p != null) {
472            outState.putParcelable(FRAGMENTS_TAG, p);
473        }
474    }
475
476    /**
477     * Dispatch onStart() to all fragments.  Ensure any created loaders are
478     * now started.
479     */
480    @Override
481    protected void onStart() {
482        super.onStart();
483
484        mStopped = false;
485        mReallyStopped = false;
486        mHandler.removeMessages(MSG_REALLY_STOPPED);
487
488        if (!mCreated) {
489            mCreated = true;
490            mFragments.dispatchActivityCreated();
491        }
492
493        mFragments.noteStateNotSaved();
494        mFragments.execPendingActions();
495
496        if (!mLoadersStarted) {
497            mLoadersStarted = true;
498            if (mLoaderManager != null) {
499                mLoaderManager.doStart();
500            } else if (!mCheckedForLoaderManager) {
501                mLoaderManager = getLoaderManager(-1, mLoadersStarted, false);
502            }
503            mCheckedForLoaderManager = true;
504        }
505        // NOTE: HC onStart goes here.
506
507        mFragments.dispatchStart();
508        if (mAllLoaderManagers != null) {
509            for (int i=mAllLoaderManagers.size()-1; i>=0; i--) {
510                LoaderManagerImpl lm = mAllLoaderManagers.valueAt(i);
511                lm.finishRetain();
512                lm.doReportStart();
513            }
514        }
515    }
516
517    /**
518     * Dispatch onStop() to all fragments.  Ensure all loaders are stopped.
519     */
520    @Override
521    protected void onStop() {
522        super.onStop();
523
524        mStopped = true;
525        mHandler.sendEmptyMessage(MSG_REALLY_STOPPED);
526
527        mFragments.dispatchStop();
528    }
529
530    // ------------------------------------------------------------------------
531    // NEW METHODS
532    // ------------------------------------------------------------------------
533
534    void supportInvalidateOptionsMenu() {
535        if (android.os.Build.VERSION.SDK_INT >= HONEYCOMB) {
536            // If we are running on HC or greater, we can use the framework
537            // API to invalidate the options menu.
538            ActivityCompatHoneycomb.invalidateOptionsMenu(this);
539            return;
540        }
541
542        // Whoops, older platform...  we'll use a hack, to manually rebuild
543        // the options menu the next time it is prepared.
544        mOptionsMenuInvalidated = true;
545    }
546
547    /**
548     * Print the Activity's state into the given stream.  This gets invoked if
549     * you run "adb shell dumpsys activity <activity_component_name>".
550     *
551     * @param prefix Desired prefix to prepend at each line of output.
552     * @param fd The raw file descriptor that the dump is being sent to.
553     * @param writer The PrintWriter to which you should dump your state.  This will be
554     * closed for you after you return.
555     * @param args additional arguments to the dump request.
556     */
557    public void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) {
558        if (android.os.Build.VERSION.SDK_INT >= HONEYCOMB) {
559            // XXX This can only work if we can call the super-class impl. :/
560            //ActivityCompatHoneycomb.dump(this, prefix, fd, writer, args);
561        }
562        writer.print(prefix); writer.print("Local FragmentActivity ");
563                writer.print(Integer.toHexString(System.identityHashCode(this)));
564                writer.println(" State:");
565        String innerPrefix = prefix + "  ";
566        writer.print(innerPrefix); writer.print("mCreated=");
567                writer.print(mCreated); writer.print("mResumed=");
568                writer.print(mResumed); writer.print(" mStopped=");
569                writer.print(mStopped); writer.print(" mReallyStopped=");
570                writer.println(mReallyStopped);
571        writer.print(innerPrefix); writer.print("mLoadersStarted=");
572                writer.println(mLoadersStarted);
573        if (mLoaderManager != null) {
574            writer.print(prefix); writer.print("Loader Manager ");
575                    writer.print(Integer.toHexString(System.identityHashCode(mLoaderManager)));
576                    writer.println(":");
577            mLoaderManager.dump(prefix + "  ", fd, writer, args);
578        }
579        mFragments.dump(prefix, fd, writer, args);
580    }
581
582    void doReallyStop(boolean retaining) {
583        if (!mReallyStopped) {
584            mReallyStopped = true;
585            mRetaining = retaining;
586            mHandler.removeMessages(MSG_REALLY_STOPPED);
587            onReallyStop();
588        }
589    }
590
591    /**
592     * Pre-HC, we didn't have a way to determine whether an activity was
593     * being stopped for a config change or not until we saw
594     * onRetainNonConfigurationInstance() called after onStop().  However
595     * we need to know this, to know whether to retain fragments.  This will
596     * tell us what we need to know.
597     */
598    void onReallyStop() {
599        if (mLoadersStarted) {
600            mLoadersStarted = false;
601            if (mLoaderManager != null) {
602                if (!mRetaining) {
603                    mLoaderManager.doStop();
604                } else {
605                    mLoaderManager.doRetain();
606                }
607            }
608        }
609
610        mFragments.dispatchReallyStop();
611    }
612
613    // ------------------------------------------------------------------------
614    // FRAGMENT SUPPORT
615    // ------------------------------------------------------------------------
616
617    /**
618     * Called when a fragment is attached to the activity.
619     */
620    public void onAttachFragment(Fragment fragment) {
621    }
622
623    /**
624     * Return the FragmentManager for interacting with fragments associated
625     * with this activity.
626     */
627    public FragmentManager getSupportFragmentManager() {
628        return mFragments;
629    }
630
631    /**
632     * Modifies the standard behavior to allow results to be delivered to fragments.
633     * This imposes a restriction that requestCode be <= 0xffff.
634     */
635    @Override
636    public void startActivityForResult(Intent intent, int requestCode) {
637        if (requestCode != -1 && (requestCode&0xffff0000) != 0) {
638            throw new IllegalArgumentException("Can only use lower 16 bits for requestCode");
639        }
640        super.startActivityForResult(intent, requestCode);
641    }
642
643    /**
644     * Called by Fragment.startActivityForResult() to implement its behavior.
645     */
646    public void startActivityFromFragment(Fragment fragment, Intent intent,
647            int requestCode) {
648        if (requestCode == -1) {
649            super.startActivityForResult(intent, -1);
650            return;
651        }
652        if ((requestCode&0xffff0000) != 0) {
653            throw new IllegalArgumentException("Can only use lower 16 bits for requestCode");
654        }
655        super.startActivityForResult(intent, ((fragment.mIndex+1)<<16) + (requestCode&0xffff));
656    }
657
658    void invalidateSupportFragmentIndex(int index) {
659        //Log.v(TAG, "invalidateFragmentIndex: index=" + index);
660        if (mAllLoaderManagers != null) {
661            LoaderManagerImpl lm = mAllLoaderManagers.get(index);
662            if (lm != null && !lm.mRetaining) {
663                lm.doDestroy();
664                mAllLoaderManagers.remove(index);
665            }
666        }
667    }
668
669    // ------------------------------------------------------------------------
670    // LOADER SUPPORT
671    // ------------------------------------------------------------------------
672
673    /**
674     * Return the LoaderManager for this fragment, creating it if needed.
675     */
676    public LoaderManager getSupportLoaderManager() {
677        if (mLoaderManager != null) {
678            return mLoaderManager;
679        }
680        mCheckedForLoaderManager = true;
681        mLoaderManager = getLoaderManager(-1, mLoadersStarted, true);
682        return mLoaderManager;
683    }
684
685    LoaderManagerImpl getLoaderManager(int index, boolean started, boolean create) {
686        if (mAllLoaderManagers == null) {
687            mAllLoaderManagers = new HCSparseArray<LoaderManagerImpl>();
688        }
689        LoaderManagerImpl lm = mAllLoaderManagers.get(index);
690        if (lm == null) {
691            if (create) {
692                lm = new LoaderManagerImpl(this, started);
693                mAllLoaderManagers.put(index, lm);
694            }
695        } else {
696            lm.updateActivity(this);
697        }
698        return lm;
699    }
700}
701