LoaderManagerImpl.java revision b31c3281d870e9abb673db239234d580dcc4feff
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 androidx.loader.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 androidx.annotation.MainThread;
28import androidx.annotation.NonNull;
29import androidx.annotation.Nullable;
30import androidx.loader.content.Loader;
31import androidx.core.util.DebugUtils;
32import androidx.collection.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                // Use super.removeObserver to avoid nulling out mLifecycleOwner & mObserver
114                super.removeObserver(observer);
115                observe(lifecycleOwner, observer);
116            }
117        }
118
119        boolean isCallbackWaitingForData() {
120            //noinspection SimplifiableIfStatement
121            if (!hasActiveObservers()) {
122                // No active observers means no one is waiting for data
123                return false;
124            }
125            return mObserver != null && !mObserver.hasDeliveredData();
126        }
127
128        @Override
129        public void removeObserver(@NonNull Observer<D> observer) {
130            super.removeObserver(observer);
131            // Clear out our references when the observer is removed to avoid leaking
132            mLifecycleOwner = null;
133            mObserver = null;
134        }
135
136        @MainThread
137        void destroy() {
138            if (DEBUG) Log.v(TAG, "  Destroying: " + this);
139            // First tell the Loader that we don't need it anymore
140            mLoader.cancelLoad();
141            mLoader.abandon();
142            // Then clean up the LoaderObserver
143            LoaderObserver<D> observer = mObserver;
144            if (observer != null) {
145                removeObserver(observer);
146                observer.reset();
147            }
148            // Finally, send the reset to the Loader
149            mLoader.unregisterListener(this);
150            mLoader.reset();
151        }
152
153        @Override
154        public void onLoadComplete(@NonNull Loader<D> loader, @Nullable D data) {
155            if (DEBUG) Log.v(TAG, "onLoadComplete: " + this);
156            if (Looper.myLooper() == Looper.getMainLooper()) {
157                setValue(data);
158            } else {
159                // The Loader#deliverResult method that calls this should
160                // only be called on the main thread, so this should never
161                // happen, but we don't want to lose the data
162                if (DEBUG) {
163                    Log.w(TAG, "onLoadComplete was incorrectly called on a "
164                            + "background thread");
165                }
166                postValue(data);
167            }
168        }
169
170        @Override
171        public String toString() {
172            StringBuilder sb = new StringBuilder(64);
173            sb.append("LoaderInfo{");
174            sb.append(Integer.toHexString(System.identityHashCode(this)));
175            sb.append(" #");
176            sb.append(mId);
177            sb.append(" : ");
178            DebugUtils.buildShortClassTag(mLoader, sb);
179            sb.append("}}");
180            return sb.toString();
181        }
182
183        public void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) {
184            writer.print(prefix); writer.print("mId="); writer.print(mId);
185            writer.print(" mArgs="); writer.println(mArgs);
186            writer.print(prefix); writer.print("mLoader="); writer.println(mLoader);
187            mLoader.dump(prefix + "  ", fd, writer, args);
188            if (mObserver != null) {
189                writer.print(prefix); writer.print("mCallbacks="); writer.println(mObserver);
190                mObserver.dump(prefix + "  ", writer);
191            }
192            writer.print(prefix); writer.print("mData="); writer.println(
193                    getLoader().dataToString(getValue()));
194            writer.print(prefix); writer.print("mStarted="); writer.println(
195                    hasActiveObservers());
196        }
197    }
198
199    /**
200     * Encapsulates the {@link LoaderCallbacks} as a {@link Observer}.
201     *
202     * @param <D> Type of data the LoaderCallbacks handles
203     */
204    static class LoaderObserver<D> implements Observer<D> {
205
206        private final @NonNull Loader<D> mLoader;
207        private final @NonNull LoaderCallbacks<D> mCallback;
208
209        private boolean mDeliveredData = false;
210
211        LoaderObserver(@NonNull Loader<D> loader, @NonNull LoaderCallbacks<D> callback) {
212            mLoader = loader;
213            mCallback = callback;
214        }
215
216        @Override
217        public void onChanged(@Nullable D data) {
218            if (DEBUG) {
219                Log.v(TAG, "  onLoadFinished in " + mLoader + ": "
220                        + mLoader.dataToString(data));
221            }
222            mCallback.onLoadFinished(mLoader, data);
223            mDeliveredData = true;
224        }
225
226        boolean hasDeliveredData() {
227            return mDeliveredData;
228        }
229
230        @MainThread
231        void reset() {
232            if (mDeliveredData) {
233                if (DEBUG) Log.v(TAG, "  Resetting: " + mLoader);
234                mCallback.onLoaderReset(mLoader);
235            }
236        }
237
238        @Override
239        public String toString() {
240            return mCallback.toString();
241        }
242
243        public void dump(String prefix, PrintWriter writer) {
244            writer.print(prefix); writer.print("mDeliveredData="); writer.println(
245                    mDeliveredData);
246        }
247    }
248
249    /**
250     * ViewModel responsible for retaining {@link LoaderInfo} instances across configuration changes
251     */
252    static class LoaderViewModel extends ViewModel {
253        private static final ViewModelProvider.Factory FACTORY = new ViewModelProvider.Factory() {
254            @NonNull
255            @Override
256            @SuppressWarnings("unchecked")
257            public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
258                return (T) new LoaderViewModel();
259            }
260        };
261
262        @NonNull
263        static LoaderViewModel getInstance(ViewModelStore viewModelStore) {
264            return new ViewModelProvider(viewModelStore, FACTORY).get(LoaderViewModel.class);
265        }
266
267        private SparseArrayCompat<LoaderInfo> mLoaders = new SparseArrayCompat<>();
268        private boolean mCreatingLoader = false;
269
270        void startCreatingLoader() {
271            mCreatingLoader = true;
272        }
273
274        boolean isCreatingLoader() {
275            return mCreatingLoader;
276        }
277
278        void finishCreatingLoader() {
279            mCreatingLoader = false;
280        }
281
282        void putLoader(int id, @NonNull LoaderInfo info) {
283            mLoaders.put(id, info);
284        }
285
286        @SuppressWarnings("unchecked")
287        <D> LoaderInfo<D> getLoader(int id) {
288            return mLoaders.get(id);
289        }
290
291        void removeLoader(int id) {
292            mLoaders.remove(id);
293        }
294
295        boolean hasRunningLoaders() {
296            int size = mLoaders.size();
297            for (int index = 0; index < size; index++) {
298                LoaderInfo info = mLoaders.valueAt(index);
299                if (info.isCallbackWaitingForData()) {
300                    return true;
301                }
302            }
303            return false;
304        }
305
306        void markForRedelivery() {
307            int size = mLoaders.size();
308            for (int index = 0; index < size; index++) {
309                LoaderInfo info = mLoaders.valueAt(index);
310                info.markForRedelivery();
311            }
312        }
313
314        @Override
315        protected void onCleared() {
316            super.onCleared();
317            int size = mLoaders.size();
318            for (int index = 0; index < size; index++) {
319                LoaderInfo info = mLoaders.valueAt(index);
320                info.destroy();
321            }
322            mLoaders.clear();
323        }
324
325        public void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) {
326            if (mLoaders.size() > 0) {
327                writer.print(prefix); writer.println("Loaders:");
328                String innerPrefix = prefix + "    ";
329                for (int i = 0; i < mLoaders.size(); i++) {
330                    LoaderInfo info = mLoaders.valueAt(i);
331                    writer.print(prefix); writer.print("  #"); writer.print(mLoaders.keyAt(i));
332                    writer.print(": "); writer.println(info.toString());
333                    info.dump(innerPrefix, fd, writer, args);
334                }
335            }
336        }
337    }
338
339    private final @NonNull LifecycleOwner mLifecycleOwner;
340    private final @NonNull LoaderViewModel mLoaderViewModel;
341
342    LoaderManagerImpl(@NonNull LifecycleOwner lifecycleOwner,
343            @NonNull ViewModelStore viewModelStore) {
344        mLifecycleOwner = lifecycleOwner;
345        mLoaderViewModel = LoaderViewModel.getInstance(viewModelStore);
346    }
347
348    @MainThread
349    @NonNull
350    private <D> Loader<D> createAndInstallLoader(int id, @Nullable Bundle args,
351            @NonNull LoaderCallbacks<D> callback) {
352        LoaderInfo<D> info;
353        try {
354            mLoaderViewModel.startCreatingLoader();
355            Loader<D> loader = callback.onCreateLoader(id, args);
356            if (loader.getClass().isMemberClass()
357                    && !Modifier.isStatic(loader.getClass().getModifiers())) {
358                throw new IllegalArgumentException("Object returned from onCreateLoader "
359                        + "must not be a non-static inner member class: "
360                        + loader);
361            }
362            info = new LoaderInfo<>(id, args, loader);
363            if (DEBUG) Log.v(TAG, "  Created new loader " + info);
364            mLoaderViewModel.putLoader(id, info);
365        } finally {
366            mLoaderViewModel.finishCreatingLoader();
367        }
368        return info.setCallback(mLifecycleOwner, callback);
369    }
370
371    @MainThread
372    @NonNull
373    @Override
374    public <D> Loader<D> initLoader(int id, @Nullable Bundle args,
375            @NonNull LoaderCallbacks<D> callback) {
376        if (mLoaderViewModel.isCreatingLoader()) {
377            throw new IllegalStateException("Called while creating a loader");
378        }
379        if (Looper.getMainLooper() != Looper.myLooper()) {
380            throw new IllegalStateException("initLoader must be called on the main thread");
381        }
382
383        LoaderInfo<D> info = mLoaderViewModel.getLoader(id);
384
385        if (DEBUG) Log.v(TAG, "initLoader in " + this + ": args=" + args);
386
387        if (info == null) {
388            // Loader doesn't already exist; create.
389            return createAndInstallLoader(id, args, callback);
390        } else {
391            if (DEBUG) Log.v(TAG, "  Re-using existing loader " + info);
392            return info.setCallback(mLifecycleOwner, callback);
393        }
394    }
395
396    @MainThread
397    @NonNull
398    @Override
399    public <D> Loader<D> restartLoader(int id, @Nullable Bundle args,
400            @NonNull LoaderCallbacks<D> callback) {
401        if (mLoaderViewModel.isCreatingLoader()) {
402            throw new IllegalStateException("Called while creating a loader");
403        }
404        if (Looper.getMainLooper() != Looper.myLooper()) {
405            throw new IllegalStateException("restartLoader must be called on the main thread");
406        }
407
408        if (DEBUG) Log.v(TAG, "restartLoader in " + this + ": args=" + args);
409        // Destroy any existing Loader
410        destroyLoader(id);
411        // And create a new Loader
412        return createAndInstallLoader(id, args, callback);
413    }
414
415    @MainThread
416    @Override
417    public void destroyLoader(int id) {
418        if (mLoaderViewModel.isCreatingLoader()) {
419            throw new IllegalStateException("Called while creating a loader");
420        }
421        if (Looper.getMainLooper() != Looper.myLooper()) {
422            throw new IllegalStateException("destroyLoader must be called on the main thread");
423        }
424
425        if (DEBUG) Log.v(TAG, "destroyLoader in " + this + " of " + id);
426        LoaderInfo info = mLoaderViewModel.getLoader(id);
427        if (info != null) {
428            info.destroy();
429            mLoaderViewModel.removeLoader(id);
430        }
431    }
432
433    @Nullable
434    @Override
435    public <D> Loader<D> getLoader(int id) {
436        if (mLoaderViewModel.isCreatingLoader()) {
437            throw new IllegalStateException("Called while creating a loader");
438        }
439
440        LoaderInfo<D> info = mLoaderViewModel.getLoader(id);
441        return info != null ? info.getLoader() : null;
442    }
443
444    @Override
445    public void markForRedelivery() {
446        mLoaderViewModel.markForRedelivery();
447    }
448
449    @Override
450    public String toString() {
451        StringBuilder sb = new StringBuilder(128);
452        sb.append("LoaderManager{");
453        sb.append(Integer.toHexString(System.identityHashCode(this)));
454        sb.append(" in ");
455        DebugUtils.buildShortClassTag(mLifecycleOwner, sb);
456        sb.append("}}");
457        return sb.toString();
458    }
459
460    @Override
461    public void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) {
462        mLoaderViewModel.dump(prefix, fd, writer, args);
463    }
464
465    @Override
466    public boolean hasRunningLoaders() {
467        return mLoaderViewModel.hasRunningLoaders();
468    }
469}
470