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