LoaderManagerImpl.java revision 0f4d973e9bb5a00727068f6a76338908c21902fd
1/*
2 * Copyright 2018 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.arch.lifecycle.LifecycleOwner;
20import android.arch.lifecycle.MutableLiveData;
21import android.arch.lifecycle.Observer;
22import android.arch.lifecycle.ViewModel;
23import android.arch.lifecycle.ViewModelProvider;
24import android.arch.lifecycle.ViewModelStore;
25import android.os.Bundle;
26import android.os.Looper;
27import android.support.annotation.MainThread;
28import android.support.annotation.NonNull;
29import android.support.annotation.Nullable;
30import android.support.v4.content.Loader;
31import android.support.v4.util.DebugUtils;
32import android.support.v4.util.SparseArrayCompat;
33import android.util.Log;
34
35import java.io.FileDescriptor;
36import java.io.PrintWriter;
37import java.lang.reflect.Modifier;
38
39class LoaderManagerImpl extends LoaderManager {
40    static final String TAG = "LoaderManager";
41    static boolean DEBUG = false;
42
43    /**
44     * Class which manages the state of a {@link Loader} and its associated
45     * {@link LoaderCallbacks}
46     *
47     * @param <D> Type of data the Loader handles
48     */
49    public static class LoaderInfo<D> extends MutableLiveData<D>
50            implements Loader.OnLoadCompleteListener<D> {
51
52        private final int mId;
53        private final @Nullable Bundle mArgs;
54        private final @NonNull Loader<D> mLoader;
55        private LifecycleOwner mLifecycleOwner;
56        private LoaderObserver<D> mObserver;
57
58        LoaderInfo(int id, @Nullable Bundle args, @NonNull Loader<D> loader) {
59            mId = id;
60            mArgs = args;
61            mLoader = loader;
62            mLoader.registerListener(id, this);
63        }
64
65        @NonNull
66        Loader<D> getLoader() {
67            return mLoader;
68        }
69
70        @Override
71        protected void onActive() {
72            if (DEBUG) Log.v(TAG, "  Starting: " + LoaderInfo.this);
73            mLoader.startLoading();
74        }
75
76        @Override
77        protected void onInactive() {
78            if (DEBUG) Log.v(TAG, "  Stopping: " + LoaderInfo.this);
79            mLoader.stopLoading();
80        }
81
82        /**
83         * Set the {@link LoaderCallbacks} to associate with this {@link Loader}. This
84         * removes any existing {@link LoaderCallbacks}.
85         *
86         * @param owner The lifecycle that should be used to start and stop the {@link Loader}
87         * @param callback The new {@link LoaderCallbacks} to use
88         * @return The {@link Loader} associated with this LoaderInfo
89         */
90        @MainThread
91        @NonNull
92        Loader<D> setCallback(@NonNull LifecycleOwner owner,
93                @NonNull LoaderCallbacks<D> callback) {
94            LoaderObserver<D> observer = new LoaderObserver<>(mLoader, callback);
95            // Add the new observer
96            observe(owner, observer);
97            // Loaders only support one observer at a time, so remove the current observer, if any
98            if (mObserver != null) {
99                removeObserver(mObserver);
100            }
101            mLifecycleOwner = owner;
102            mObserver = observer;
103            return mLoader;
104        }
105
106        void markForRedelivery() {
107            LifecycleOwner lifecycleOwner = mLifecycleOwner;
108            LoaderObserver<D> observer = mObserver;
109            if (lifecycleOwner != null && observer != null) {
110                // Removing and re-adding the observer ensures that the
111                // observer is called again, even if they had already
112                // received the current data
113                removeObserver(observer);
114                observe(lifecycleOwner, observer);
115            }
116        }
117
118        boolean isCallbackWaitingForData() {
119            //noinspection SimplifiableIfStatement
120            if (!hasActiveObservers()) {
121                // No active observers means no one is waiting for data
122                return false;
123            }
124            return mObserver != null && !mObserver.hasDeliveredData();
125        }
126
127        @Override
128        public void removeObserver(@NonNull Observer<D> observer) {
129            super.removeObserver(observer);
130            // Clear out our references when the observer is removed to avoid leaking
131            mLifecycleOwner = null;
132            mObserver = null;
133        }
134
135        @MainThread
136        void destroy() {
137            if (DEBUG) Log.v(TAG, "  Destroying: " + this);
138            // First tell the Loader that we don't need it anymore
139            mLoader.cancelLoad();
140            mLoader.abandon();
141            // Then clean up the LoaderObserver
142            LoaderObserver<D> observer = mObserver;
143            if (observer != null) {
144                removeObserver(observer);
145                observer.reset();
146            }
147            // Finally, send the reset to the Loader
148            mLoader.unregisterListener(this);
149            mLoader.reset();
150        }
151
152        @Override
153        public void onLoadComplete(@NonNull Loader<D> loader, @Nullable D data) {
154            if (DEBUG) Log.v(TAG, "onLoadComplete: " + this);
155            postValue(data);
156        }
157
158        @Override
159        public String toString() {
160            StringBuilder sb = new StringBuilder(64);
161            sb.append("LoaderInfo{");
162            sb.append(Integer.toHexString(System.identityHashCode(this)));
163            sb.append(" #");
164            sb.append(mId);
165            sb.append(" : ");
166            DebugUtils.buildShortClassTag(mLoader, sb);
167            sb.append("}}");
168            return sb.toString();
169        }
170
171        public void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) {
172            writer.print(prefix); writer.print("mId="); writer.print(mId);
173            writer.print(" mArgs="); writer.println(mArgs);
174            writer.print(prefix); writer.print("mLoader="); writer.println(mLoader);
175            mLoader.dump(prefix + "  ", fd, writer, args);
176            if (mObserver != null) {
177                writer.print(prefix); writer.print("mCallbacks="); writer.println(mObserver);
178                mObserver.dump(prefix + "  ", writer);
179            }
180            writer.print(prefix); writer.print("mData="); writer.println(
181                    getLoader().dataToString(getValue()));
182            writer.print(prefix); writer.print("mStarted="); writer.println(
183                    hasActiveObservers());
184        }
185    }
186
187    /**
188     * Encapsulates the {@link LoaderCallbacks} as a {@link Observer}.
189     *
190     * @param <D> Type of data the LoaderCallbacks handles
191     */
192    static class LoaderObserver<D> implements Observer<D> {
193
194        private final @NonNull Loader<D> mLoader;
195        private final @NonNull LoaderCallbacks<D> mCallback;
196
197        private boolean mDeliveredData = false;
198
199        LoaderObserver(@NonNull Loader<D> loader, @NonNull LoaderCallbacks<D> callback) {
200            mLoader = loader;
201            mCallback = callback;
202        }
203
204        @Override
205        public void onChanged(@Nullable D data) {
206            if (DEBUG) {
207                Log.v(TAG, "  onLoadFinished in " + mLoader + ": "
208                        + mLoader.dataToString(data));
209            }
210            mCallback.onLoadFinished(mLoader, data);
211            mDeliveredData = true;
212        }
213
214        boolean hasDeliveredData() {
215            return mDeliveredData;
216        }
217
218        @MainThread
219        void reset() {
220            if (mDeliveredData) {
221                if (DEBUG) Log.v(TAG, "  Resetting: " + mLoader);
222                mCallback.onLoaderReset(mLoader);
223            }
224        }
225
226        @Override
227        public String toString() {
228            return mCallback.toString();
229        }
230
231        public void dump(String prefix, PrintWriter writer) {
232            writer.print(prefix); writer.print("mDeliveredData="); writer.println(
233                    mDeliveredData);
234        }
235    }
236
237    /**
238     * ViewModel responsible for retaining {@link LoaderInfo} instances across configuration changes
239     */
240    static class LoaderViewModel extends ViewModel {
241        private static final ViewModelProvider.Factory FACTORY = new ViewModelProvider.Factory() {
242            @NonNull
243            @Override
244            @SuppressWarnings("unchecked")
245            public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
246                return (T) new LoaderViewModel();
247            }
248        };
249
250        @NonNull
251        static LoaderViewModel getInstance(ViewModelStore viewModelStore) {
252            return new ViewModelProvider(viewModelStore, FACTORY).get(LoaderViewModel.class);
253        }
254
255        private SparseArrayCompat<LoaderInfo> mLoaders = new SparseArrayCompat<>();
256
257        void putLoader(int id, @NonNull LoaderInfo info) {
258            mLoaders.put(id, info);
259        }
260
261        @SuppressWarnings("unchecked")
262        <D> LoaderInfo<D> getLoader(int id) {
263            return mLoaders.get(id);
264        }
265
266        void removeLoader(int id) {
267            mLoaders.remove(id);
268        }
269
270        boolean hasRunningLoaders() {
271            int size = mLoaders.size();
272            for (int index = 0; index < size; index++) {
273                LoaderInfo info = mLoaders.valueAt(index);
274                if (info.isCallbackWaitingForData()) {
275                    return true;
276                }
277            }
278            return false;
279        }
280
281        void markForRedelivery() {
282            int size = mLoaders.size();
283            for (int index = 0; index < size; index++) {
284                LoaderInfo info = mLoaders.valueAt(index);
285                info.markForRedelivery();
286            }
287        }
288
289        @Override
290        protected void onCleared() {
291            super.onCleared();
292            int size = mLoaders.size();
293            for (int index = 0; index < size; index++) {
294                LoaderInfo info = mLoaders.valueAt(index);
295                info.destroy();
296            }
297            mLoaders.clear();
298        }
299
300        public void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) {
301            if (mLoaders.size() > 0) {
302                writer.print(prefix); writer.println("Loaders:");
303                String innerPrefix = prefix + "    ";
304                for (int i = 0; i < mLoaders.size(); i++) {
305                    LoaderInfo info = mLoaders.valueAt(i);
306                    writer.print(prefix); writer.print("  #"); writer.print(mLoaders.keyAt(i));
307                    writer.print(": "); writer.println(info.toString());
308                    info.dump(innerPrefix, fd, writer, args);
309                }
310            }
311        }
312    }
313
314    private final @NonNull LifecycleOwner mLifecycleOwner;
315    private final @NonNull LoaderViewModel mLoaderViewModel;
316
317    private boolean mCreatingLoader;
318
319    LoaderManagerImpl(@NonNull LifecycleOwner lifecycleOwner,
320            @NonNull ViewModelStore viewModelStore) {
321        mLifecycleOwner = lifecycleOwner;
322        mLoaderViewModel = LoaderViewModel.getInstance(viewModelStore);
323    }
324
325    @MainThread
326    @NonNull
327    private <D> Loader<D> createAndInstallLoader(int id, @Nullable Bundle args,
328            @NonNull LoaderCallbacks<D> callback) {
329        try {
330            mCreatingLoader = true;
331            Loader<D> loader = callback.onCreateLoader(id, args);
332            if (loader.getClass().isMemberClass()
333                    && !Modifier.isStatic(loader.getClass().getModifiers())) {
334                throw new IllegalArgumentException("Object returned from onCreateLoader "
335                        + "must not be a non-static inner member class: "
336                        + loader);
337            }
338            LoaderInfo<D> info = new LoaderInfo<>(id, args, loader);
339            if (DEBUG) Log.v(TAG, "  Created new loader " + info);
340            mLoaderViewModel.putLoader(id, info);
341            return info.setCallback(mLifecycleOwner, callback);
342        } finally {
343            mCreatingLoader = false;
344        }
345    }
346
347    @MainThread
348    @NonNull
349    @Override
350    public <D> Loader<D> initLoader(int id, @Nullable Bundle args,
351            @NonNull LoaderCallbacks<D> callback) {
352        if (mCreatingLoader) {
353            throw new IllegalStateException("Called while creating a loader");
354        }
355        if (Looper.getMainLooper() != Looper.myLooper()) {
356            throw new IllegalStateException("initLoader must be called on the main thread");
357        }
358
359        LoaderInfo<D> info = mLoaderViewModel.getLoader(id);
360
361        if (DEBUG) Log.v(TAG, "initLoader in " + this + ": args=" + args);
362
363        if (info == null) {
364            // Loader doesn't already exist; create.
365            return createAndInstallLoader(id, args, callback);
366        } else {
367            if (DEBUG) Log.v(TAG, "  Re-using existing loader " + info);
368            return info.setCallback(mLifecycleOwner, callback);
369        }
370    }
371
372    @MainThread
373    @NonNull
374    @Override
375    public <D> Loader<D> restartLoader(int id, @Nullable Bundle args,
376            @NonNull LoaderCallbacks<D> callback) {
377        if (mCreatingLoader) {
378            throw new IllegalStateException("Called while creating a loader");
379        }
380        if (Looper.getMainLooper() != Looper.myLooper()) {
381            throw new IllegalStateException("restartLoader must be called on the main thread");
382        }
383
384        if (DEBUG) Log.v(TAG, "restartLoader in " + this + ": args=" + args);
385        // Destroy any existing Loader
386        destroyLoader(id);
387        // And create a new Loader
388        return createAndInstallLoader(id, args, callback);
389    }
390
391    @MainThread
392    @Override
393    public void destroyLoader(int id) {
394        if (mCreatingLoader) {
395            throw new IllegalStateException("Called while creating a loader");
396        }
397        if (Looper.getMainLooper() != Looper.myLooper()) {
398            throw new IllegalStateException("destroyLoader must be called on the main thread");
399        }
400
401        if (DEBUG) Log.v(TAG, "destroyLoader in " + this + " of " + id);
402        LoaderInfo info = mLoaderViewModel.getLoader(id);
403        if (info != null) {
404            info.destroy();
405            mLoaderViewModel.removeLoader(id);
406        }
407    }
408
409    @Nullable
410    @Override
411    public <D> Loader<D> getLoader(int id) {
412        if (mCreatingLoader) {
413            throw new IllegalStateException("Called while creating a loader");
414        }
415
416        LoaderInfo<D> info = mLoaderViewModel.getLoader(id);
417        return info != null ? info.getLoader() : null;
418    }
419
420    /**
421     * Mark all Loaders associated with this LoaderManager for redelivery of their current
422     * data (if any) the next time the LifecycleOwner is started. In cases where no data has
423     * yet been delivered, this is effectively a no-op. In cases where data has already been
424     * delivered via {@link LoaderCallbacks#onLoadFinished(Loader, Object)}, this will ensure
425     * that {@link LoaderCallbacks#onLoadFinished(Loader, Object)} is called again with the
426     * same data.
427     */
428    void markForRedelivery() {
429        mLoaderViewModel.markForRedelivery();
430    }
431
432    @Override
433    public String toString() {
434        StringBuilder sb = new StringBuilder(128);
435        sb.append("LoaderManager{");
436        sb.append(Integer.toHexString(System.identityHashCode(this)));
437        sb.append(" in ");
438        DebugUtils.buildShortClassTag(mLifecycleOwner, sb);
439        sb.append("}}");
440        return sb.toString();
441    }
442
443    @Override
444    public void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) {
445        mLoaderViewModel.dump(prefix, fd, writer, args);
446    }
447
448    @Override
449    public boolean hasRunningLoaders() {
450        return mLoaderViewModel.hasRunningLoaders();
451    }
452}
453