LoaderManager.java revision db7f38673c33ba55801019007ff18fa4c57f75e0
1/*
2 * Copyright (C) 2010 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.Loader;
20import android.os.Bundle;
21import android.util.DebugUtils;
22import android.util.Log;
23import android.util.SparseArray;
24
25import java.io.FileDescriptor;
26import java.io.PrintWriter;
27import java.lang.reflect.Modifier;
28
29/**
30 * Interface associated with an {@link Activity} or {@link Fragment} for managing
31 * one or more {@link android.content.Loader} instances associated with it.  This
32 * helps an application manage longer-running operations in conjunction with the
33 * Activity or Fragment lifecycle; the most common use of this is with a
34 * {@link android.content.CursorLoader}, however applications are free to write
35 * their own loaders for loading other types of data.
36 *
37 * <p>As an example, here is the full implementation of a {@link Fragment}
38 * that displays a {@link android.widget.ListView} containing the results of
39 * a query against the contacts content provider.  It uses a
40 * {@link android.content.CursorLoader} to manage the query on the provider.
41 *
42 * {@sample development/samples/ApiDemos/src/com/example/android/apis/app/FragmentListCursorLoader.java
43 *      fragment_cursor}
44 */
45public abstract class LoaderManager {
46    /**
47     * Callback interface for a client to interact with the manager.
48     */
49    public interface LoaderCallbacks<D> {
50        /**
51         * Instantiate and return a new Loader for the given ID.
52         *
53         * @param id The ID whose loader is to be created.
54         * @param args Any arguments supplied by the caller.
55         * @return Return a new Loader instance that is ready to start loading.
56         */
57        public Loader<D> onCreateLoader(int id, Bundle args);
58
59        /**
60         * Called when a previously created loader has finished its load.  Note
61         * that normally an application is <em>not</em> allowed to commit fragment
62         * transactions while in this call, since it can happen after an
63         * activity's state is saved.  See {@link FragmentManager#beginTransaction()
64         * FragmentManager.openTransaction()} for further discussion on this.
65         *
66         * <p>This function is guaranteed to be called prior to the release of
67         * the last data that was supplied for this Loader.  At this point
68         * you should remove all use of the old data (since it will be released
69         * soon), but should not do your own release of the data since its Loader
70         * owns it and will take care of that.  The Loader will take care of
71         * management of its data so you don't have to.  In particular:
72         *
73         * <ul>
74         * <li> <p>The Loader will monitor for changes to the data, and report
75         * them to you through new calls here.  You should not monitor the
76         * data yourself.  For example, if the data is a {@link android.database.Cursor}
77         * and you place it in a {@link android.widget.CursorAdapter}, use
78         * the {@link android.widget.CursorAdapter#CursorAdapter(android.content.Context,
79         * android.database.Cursor, int)} constructor <em>without</em> passing
80         * in either {@link android.widget.CursorAdapter#FLAG_AUTO_REQUERY}
81         * or {@link android.widget.CursorAdapter#FLAG_REGISTER_CONTENT_OBSERVER}
82         * (that is, use 0 for the flags argument).  This prevents the CursorAdapter
83         * from doing its own observing of the Cursor, which is not needed since
84         * when a change happens you will get a new Cursor throw another call
85         * here.
86         * <li> The Loader will release the data once it knows the application
87         * is no longer using it.  For example, if the data is
88         * a {@link android.database.Cursor} from a {@link android.content.CursorLoader},
89         * you should not call close() on it yourself.  If the Cursor is being placed in a
90         * {@link android.widget.CursorAdapter}, you should use the
91         * {@link android.widget.CursorAdapter#swapCursor(android.database.Cursor)}
92         * method so that the old Cursor is not closed.
93         * </ul>
94         *
95         * @param loader The Loader that has finished.
96         * @param data The data generated by the Loader.
97         */
98        public void onLoadFinished(Loader<D> loader, D data);
99
100        /**
101         * Called when a previously created loader is being reset, and thus
102         * making its data unavailable.  The application should at this point
103         * remove any references it has to the Loader's data.
104         *
105         * @param loader The Loader that is being reset.
106         */
107        public void onLoaderReset(Loader<D> loader);
108    }
109
110    /**
111     * Ensures a loader is initialized and active.  If the loader doesn't
112     * already exist, one is created and (if the activity/fragment is currently
113     * started) starts the loader.  Otherwise the last created
114     * loader is re-used.
115     *
116     * <p>In either case, the given callback is associated with the loader, and
117     * will be called as the loader state changes.  If at the point of call
118     * the caller is in its started state, and the requested loader
119     * already exists and has generated its data, then
120     * callback {@link LoaderCallbacks#onLoadFinished} will
121     * be called immediately (inside of this function), so you must be prepared
122     * for this to happen.
123     *
124     * @param id A unique identifier for this loader.  Can be whatever you want.
125     * Identifiers are scoped to a particular LoaderManager instance.
126     * @param args Optional arguments to supply to the loader at construction.
127     * If a loader already exists (a new one does not need to be created), this
128     * parameter will be ignored and the last arguments continue to be used.
129     * @param callback Interface the LoaderManager will call to report about
130     * changes in the state of the loader.  Required.
131     */
132    public abstract <D> Loader<D> initLoader(int id, Bundle args,
133            LoaderManager.LoaderCallbacks<D> callback);
134
135    /**
136     * Starts a new or restarts an existing {@link android.content.Loader} in
137     * this manager, registers the callbacks to it,
138     * and (if the activity/fragment is currently started) starts loading it.
139     * If a loader with the same id has previously been
140     * started it will automatically be destroyed when the new loader completes
141     * its work. The callback will be delivered before the old loader
142     * is destroyed.
143     *
144     * @param id A unique identifier for this loader.  Can be whatever you want.
145     * Identifiers are scoped to a particular LoaderManager instance.
146     * @param args Optional arguments to supply to the loader at construction.
147     * @param callback Interface the LoaderManager will call to report about
148     * changes in the state of the loader.  Required.
149     */
150    public abstract <D> Loader<D> restartLoader(int id, Bundle args,
151            LoaderManager.LoaderCallbacks<D> callback);
152
153    /**
154     * Stops and removes the loader with the given ID.  If this loader
155     * had previously reported data to the client through
156     * {@link LoaderCallbacks#onLoadFinished(Loader, Object)}, a call
157     * will be made to {@link LoaderCallbacks#onLoaderReset(Loader)}.
158     */
159    public abstract void destroyLoader(int id);
160
161    /**
162     * @deprecated Renamed to {@link #destroyLoader}.
163     */
164    @Deprecated
165    public void stopLoader(int id) {
166        destroyLoader(id);
167    }
168
169    /**
170     * Return the Loader with the given id or null if no matching Loader
171     * is found.
172     */
173    public abstract <D> Loader<D> getLoader(int id);
174
175    /**
176     * Print the LoaderManager's state into the given stream.
177     *
178     * @param prefix Text to print at the front of each line.
179     * @param fd The raw file descriptor that the dump is being sent to.
180     * @param writer A PrintWriter to which the dump is to be set.
181     * @param args Additional arguments to the dump request.
182     */
183    public abstract void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args);
184
185    /**
186     * Control whether the framework's internal loader manager debugging
187     * logs are turned on.  If enabled, you will see output in logcat as
188     * the framework performs loader operations.
189     */
190    public static void enableDebugLogging(boolean enabled) {
191        LoaderManagerImpl.DEBUG = enabled;
192    }
193}
194
195class LoaderManagerImpl extends LoaderManager {
196    static final String TAG = "LoaderManager";
197    static boolean DEBUG = true;
198
199    // These are the currently active loaders.  A loader is here
200    // from the time its load is started until it has been explicitly
201    // stopped or restarted by the application.
202    final SparseArray<LoaderInfo> mLoaders = new SparseArray<LoaderInfo>();
203
204    // These are previously run loaders.  This list is maintained internally
205    // to avoid destroying a loader while an application is still using it.
206    // It allows an application to restart a loader, but continue using its
207    // previously run loader until the new loader's data is available.
208    final SparseArray<LoaderInfo> mInactiveLoaders = new SparseArray<LoaderInfo>();
209
210    Activity mActivity;
211    boolean mStarted;
212    boolean mRetaining;
213    boolean mRetainingStarted;
214
215    final class LoaderInfo implements Loader.OnLoadCompleteListener<Object> {
216        final int mId;
217        final Bundle mArgs;
218        LoaderManager.LoaderCallbacks<Object> mCallbacks;
219        Loader<Object> mLoader;
220        Object mData;
221        Object mDeliveredData;
222        boolean mStarted;
223        boolean mRetaining;
224        boolean mRetainingStarted;
225        boolean mDestroyed;
226        boolean mListenerRegistered;
227
228        public LoaderInfo(int id, Bundle args, LoaderManager.LoaderCallbacks<Object> callbacks) {
229            mId = id;
230            mArgs = args;
231            mCallbacks = callbacks;
232        }
233
234        void start() {
235            if (mRetaining && mRetainingStarted) {
236                // Our owner is started, but we were being retained from a
237                // previous instance in the started state...  so there is really
238                // nothing to do here, since the loaders are still started.
239                mStarted = true;
240                return;
241            }
242
243            if (mStarted) {
244                // If loader already started, don't restart.
245                return;
246            }
247
248            mStarted = true;
249
250            if (DEBUG) Log.v(TAG, "  Starting: " + this);
251            if (mLoader == null && mCallbacks != null) {
252               mLoader = mCallbacks.onCreateLoader(mId, mArgs);
253            }
254            if (mLoader != null) {
255                if (mLoader.getClass().isMemberClass()
256                        && !Modifier.isStatic(mLoader.getClass().getModifiers())) {
257                    throw new IllegalArgumentException(
258                            "Object returned from onCreateLoader must not be a non-static inner member class: "
259                            + mLoader);
260                }
261                if (!mListenerRegistered) {
262                    mLoader.registerListener(mId, this);
263                    mListenerRegistered = true;
264                }
265                mLoader.startLoading();
266            }
267        }
268
269        void retain() {
270            if (DEBUG) Log.v(TAG, "  Retaining: " + this);
271            mRetaining = true;
272            mRetainingStarted = mStarted;
273            mStarted = false;
274            mCallbacks = null;
275        }
276
277        void finishRetain() {
278            if (mRetaining) {
279                if (DEBUG) Log.v(TAG, "  Finished Retaining: " + this);
280                mRetaining = false;
281                if (mStarted != mRetainingStarted) {
282                    if (!mStarted) {
283                        // This loader was retained in a started state, but
284                        // at the end of retaining everything our owner is
285                        // no longer started...  so make it stop.
286                        stop();
287                    }
288                }
289            }
290
291            if (mStarted && mData != null) {
292                // This loader has retained its data, either completely across
293                // a configuration change or just whatever the last data set
294                // was after being restarted from a stop, and now at the point of
295                // finishing the retain we find we remain started, have
296                // our data, and the owner has a new callback...  so
297                // let's deliver the data now.
298                callOnLoadFinished(mLoader, mData);
299            }
300        }
301
302        void stop() {
303            if (DEBUG) Log.v(TAG, "  Stopping: " + this);
304            mStarted = false;
305            if (!mRetaining) {
306                if (mLoader != null && mListenerRegistered) {
307                    // Let the loader know we're done with it
308                    mListenerRegistered = false;
309                    mLoader.unregisterListener(this);
310                    mLoader.stopLoading();
311                }
312            }
313        }
314
315        void destroy() {
316            if (DEBUG) Log.v(TAG, "  Destroying: " + this);
317            mDestroyed = true;
318            boolean needReset = mDeliveredData != null;
319            mDeliveredData = null;
320            if (mCallbacks != null && mLoader != null && mData != null && needReset) {
321                if (DEBUG) Log.v(TAG, "  Reseting: " + this);
322                String lastBecause = null;
323                if (mActivity != null) {
324                    lastBecause = mActivity.mFragments.mNoTransactionsBecause;
325                    mActivity.mFragments.mNoTransactionsBecause = "onLoaderReset";
326                }
327                try {
328                    mCallbacks.onLoaderReset(mLoader);
329                } finally {
330                    if (mActivity != null) {
331                        mActivity.mFragments.mNoTransactionsBecause = lastBecause;
332                    }
333                }
334            }
335            mCallbacks = null;
336            mData = null;
337            if (mLoader != null) {
338                if (mListenerRegistered) {
339                    mListenerRegistered = false;
340                    mLoader.unregisterListener(this);
341                }
342                mLoader.reset();
343            }
344        }
345
346        @Override public void onLoadComplete(Loader<Object> loader, Object data) {
347            if (DEBUG) Log.v(TAG, "onLoadComplete: " + this);
348
349            if (mDestroyed) {
350                if (DEBUG) Log.v(TAG, "  Ignoring load complete -- destroyed");
351                return;
352            }
353
354            // Notify of the new data so the app can switch out the old data before
355            // we try to destroy it.
356            if (data == null || mData != data) {
357                mData = data;
358                if (mStarted) {
359                    callOnLoadFinished(loader, data);
360                }
361            }
362
363            //if (DEBUG) Log.v(TAG, "  onLoadFinished returned: " + this);
364
365            // We have now given the application the new loader with its
366            // loaded data, so it should have stopped using the previous
367            // loader.  If there is a previous loader on the inactive list,
368            // clean it up.
369            LoaderInfo info = mInactiveLoaders.get(mId);
370            if (info != null && info != this) {
371                info.mDeliveredData = null;
372                info.destroy();
373                mInactiveLoaders.remove(mId);
374            }
375        }
376
377        void callOnLoadFinished(Loader<Object> loader, Object data) {
378            if (mCallbacks != null) {
379                String lastBecause = null;
380                if (mActivity != null) {
381                    lastBecause = mActivity.mFragments.mNoTransactionsBecause;
382                    mActivity.mFragments.mNoTransactionsBecause = "onLoadFinished";
383                }
384                try {
385                    if (DEBUG) Log.v(TAG, "  onLoadFinished in " + loader + ": "
386                            + loader.dataToString(data));
387                    mCallbacks.onLoadFinished(loader, data);
388                } finally {
389                    if (mActivity != null) {
390                        mActivity.mFragments.mNoTransactionsBecause = lastBecause;
391                    }
392                }
393                mDeliveredData = data;
394            }
395        }
396
397        @Override
398        public String toString() {
399            StringBuilder sb = new StringBuilder(64);
400            sb.append("LoaderInfo{");
401            sb.append(Integer.toHexString(System.identityHashCode(this)));
402            sb.append(" #");
403            sb.append(mId);
404            sb.append(" : ");
405            DebugUtils.buildShortClassTag(mLoader, sb);
406            sb.append("}}");
407            return sb.toString();
408        }
409
410        public void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) {
411            writer.print(prefix); writer.print("mId="); writer.print(mId);
412                    writer.print(" mArgs="); writer.println(mArgs);
413            writer.print(prefix); writer.print("mCallbacks="); writer.println(mCallbacks);
414            writer.print(prefix); writer.print("mLoader="); writer.println(mLoader);
415            if (mLoader != null) {
416                mLoader.dump(prefix + "  ", fd, writer, args);
417            }
418            writer.print(prefix); writer.print("mData="); writer.println(mData);
419            writer.print(prefix); writer.print("mDeliveredData="); writer.println(mDeliveredData);
420            writer.print(prefix); writer.print("mStarted="); writer.print(mStarted);
421                    writer.print(" mRetaining="); writer.print(mRetaining);
422                    writer.print(" mDestroyed="); writer.println(mDestroyed);
423            writer.print(prefix); writer.print("mListenerRegistered=");
424                    writer.println(mListenerRegistered);
425        }
426    }
427
428    LoaderManagerImpl(Activity activity, boolean started) {
429        mActivity = activity;
430        mStarted = started;
431    }
432
433    void updateActivity(Activity activity) {
434        mActivity = activity;
435    }
436
437    private LoaderInfo createLoader(int id, Bundle args,
438            LoaderManager.LoaderCallbacks<Object> callback) {
439        LoaderInfo info = new LoaderInfo(id, args,  (LoaderManager.LoaderCallbacks<Object>)callback);
440        mLoaders.put(id, info);
441        Loader<Object> loader = callback.onCreateLoader(id, args);
442        info.mLoader = (Loader<Object>)loader;
443        if (mStarted) {
444            // The activity will start all existing loaders in it's onStart(),
445            // so only start them here if we're past that point of the activitiy's
446            // life cycle
447            info.start();
448        }
449        return info;
450    }
451
452    @SuppressWarnings("unchecked")
453    public <D> Loader<D> initLoader(int id, Bundle args, LoaderManager.LoaderCallbacks<D> callback) {
454        LoaderInfo info = mLoaders.get(id);
455
456        if (DEBUG) Log.v(TAG, "initLoader in " + this + ": args=" + args);
457
458        if (info == null) {
459            // Loader doesn't already exist; create.
460            info = createLoader(id, args,  (LoaderManager.LoaderCallbacks<Object>)callback);
461            if (DEBUG) Log.v(TAG, "  Created new loader " + info);
462        } else {
463            if (DEBUG) Log.v(TAG, "  Re-using existing loader " + info);
464            info.mCallbacks = (LoaderManager.LoaderCallbacks<Object>)callback;
465        }
466
467        if (info.mData != null && mStarted) {
468            // If the loader has already generated its data, report it now.
469            info.callOnLoadFinished(info.mLoader, info.mData);
470        }
471
472        return (Loader<D>)info.mLoader;
473    }
474
475    @SuppressWarnings("unchecked")
476    public <D> Loader<D> restartLoader(int id, Bundle args, LoaderManager.LoaderCallbacks<D> callback) {
477        LoaderInfo info = mLoaders.get(id);
478        if (DEBUG) Log.v(TAG, "restartLoader in " + this + ": args=" + args);
479        if (info != null) {
480            LoaderInfo inactive = mInactiveLoaders.get(id);
481            if (inactive != null) {
482                if (info.mData != null) {
483                    // This loader now has data...  we are probably being
484                    // called from within onLoadComplete, where we haven't
485                    // yet destroyed the last inactive loader.  So just do
486                    // that now.
487                    if (DEBUG) Log.v(TAG, "  Removing last inactive loader: " + info);
488                    inactive.mDeliveredData = null;
489                    inactive.destroy();
490                    mInactiveLoaders.put(id, info);
491                } else {
492                    // We already have an inactive loader for this ID that we are
493                    // waiting for!  Now we have three active loaders... let's just
494                    // drop the one in the middle, since we are still waiting for
495                    // its result but that result is already out of date.
496                    if (DEBUG) Log.v(TAG, "  Removing intermediate loader: " + info);
497                    info.destroy();
498                }
499            } else {
500                // Keep track of the previous instance of this loader so we can destroy
501                // it when the new one completes.
502                if (DEBUG) Log.v(TAG, "  Making last loader inactive: " + info);
503                mInactiveLoaders.put(id, info);
504            }
505        }
506
507        info = createLoader(id, args,  (LoaderManager.LoaderCallbacks<Object>)callback);
508        return (Loader<D>)info.mLoader;
509    }
510
511    public void destroyLoader(int id) {
512        if (DEBUG) Log.v(TAG, "destroyLoader in " + this + " of " + id);
513        int idx = mLoaders.indexOfKey(id);
514        if (idx >= 0) {
515            LoaderInfo info = mLoaders.valueAt(idx);
516            mLoaders.removeAt(idx);
517            info.destroy();
518        }
519        idx = mInactiveLoaders.indexOfKey(id);
520        if (idx >= 0) {
521            LoaderInfo info = mInactiveLoaders.valueAt(idx);
522            mInactiveLoaders.removeAt(idx);
523            info.destroy();
524        }
525    }
526
527    @SuppressWarnings("unchecked")
528    public <D> Loader<D> getLoader(int id) {
529        LoaderInfo loaderInfo = mLoaders.get(id);
530        if (loaderInfo != null) {
531            return (Loader<D>)mLoaders.get(id).mLoader;
532        }
533        return null;
534    }
535
536    void doStart() {
537        if (DEBUG) Log.v(TAG, "Starting in " + this);
538        if (mStarted) {
539            RuntimeException e = new RuntimeException("here");
540            e.fillInStackTrace();
541            Log.w(TAG, "Called doStart when already started: " + this, e);
542            return;
543        }
544
545        mStarted = true;
546
547        // Call out to sub classes so they can start their loaders
548        // Let the existing loaders know that we want to be notified when a load is complete
549        for (int i = mLoaders.size()-1; i >= 0; i--) {
550            mLoaders.valueAt(i).start();
551        }
552    }
553
554    void doStop() {
555        if (DEBUG) Log.v(TAG, "Stopping in " + this);
556        if (!mStarted) {
557            RuntimeException e = new RuntimeException("here");
558            e.fillInStackTrace();
559            Log.w(TAG, "Called doStop when not started: " + this, e);
560            return;
561        }
562
563        for (int i = mLoaders.size()-1; i >= 0; i--) {
564            mLoaders.valueAt(i).stop();
565        }
566        mStarted = false;
567    }
568
569    void doRetain() {
570        if (DEBUG) Log.v(TAG, "Retaining in " + this);
571        if (!mStarted) {
572            RuntimeException e = new RuntimeException("here");
573            e.fillInStackTrace();
574            Log.w(TAG, "Called doRetain when not started: " + this, e);
575            return;
576        }
577
578        mRetaining = true;
579        mStarted = false;
580        for (int i = mLoaders.size()-1; i >= 0; i--) {
581            mLoaders.valueAt(i).retain();
582        }
583    }
584
585    void finishRetain() {
586        if (mRetaining) {
587            if (DEBUG) Log.v(TAG, "Finished Retaining in " + this);
588
589            mRetaining = false;
590            for (int i = mLoaders.size()-1; i >= 0; i--) {
591                mLoaders.valueAt(i).finishRetain();
592            }
593        }
594    }
595
596    void doDestroy() {
597        if (!mRetaining) {
598            if (DEBUG) Log.v(TAG, "Destroying Active in " + this);
599            for (int i = mLoaders.size()-1; i >= 0; i--) {
600                mLoaders.valueAt(i).destroy();
601            }
602        }
603
604        if (DEBUG) Log.v(TAG, "Destroying Inactive in " + this);
605        for (int i = mInactiveLoaders.size()-1; i >= 0; i--) {
606            mInactiveLoaders.valueAt(i).destroy();
607        }
608        mInactiveLoaders.clear();
609    }
610
611    @Override
612    public String toString() {
613        StringBuilder sb = new StringBuilder(128);
614        sb.append("LoaderManager{");
615        sb.append(Integer.toHexString(System.identityHashCode(this)));
616        sb.append(" in ");
617        DebugUtils.buildShortClassTag(mActivity, sb);
618        sb.append("}}");
619        return sb.toString();
620    }
621
622    @Override
623    public void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) {
624        if (mLoaders.size() > 0) {
625            writer.print(prefix); writer.println("Active Loaders:");
626            String innerPrefix = prefix + "    ";
627            for (int i=0; i < mLoaders.size(); i++) {
628                LoaderInfo li = mLoaders.valueAt(i);
629                writer.print(prefix); writer.print("  #"); writer.print(mLoaders.keyAt(i));
630                        writer.print(": "); writer.println(li.toString());
631                li.dump(innerPrefix, fd, writer, args);
632            }
633        }
634        if (mInactiveLoaders.size() > 0) {
635            writer.print(prefix); writer.println("Inactive Loaders:");
636            String innerPrefix = prefix + "    ";
637            for (int i=0; i < mInactiveLoaders.size(); i++) {
638                LoaderInfo li = mInactiveLoaders.valueAt(i);
639                writer.print(prefix); writer.print("  #"); writer.print(mInactiveLoaders.keyAt(i));
640                        writer.print(": "); writer.println(li.toString());
641                li.dump(innerPrefix, fd, writer, args);
642            }
643        }
644    }
645}
646