LoaderManagerImpl.java

/*
 * Copyright 2018 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package androidx.loader.app;

import android.os.Bundle;
import android.os.Looper;
import android.util.Log;

import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.collection.SparseArrayCompat;
import androidx.core.util.DebugUtils;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.Observer;
import androidx.lifecycle.ViewModel;
import androidx.lifecycle.ViewModelProvider;
import androidx.lifecycle.ViewModelStore;
import androidx.loader.content.Loader;

import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.lang.reflect.Modifier;

class LoaderManagerImpl extends LoaderManager {
    static final String TAG = "LoaderManager";
    static boolean DEBUG = false;

    /**
     * Class which manages the state of a {@link Loader} and its associated
     * {@link LoaderCallbacks}
     *
     * @param <D> Type of data the Loader handles
     */
    public static class LoaderInfo<D> extends MutableLiveData<D>
            implements Loader.OnLoadCompleteListener<D> {

        private final int mId;
        private final @Nullable Bundle mArgs;
        private final @NonNull Loader<D> mLoader;
        private LifecycleOwner mLifecycleOwner;
        private LoaderObserver<D> mObserver;
        private Loader<D> mPriorLoader;

        LoaderInfo(int id, @Nullable Bundle args, @NonNull Loader<D> loader,
                @Nullable Loader<D> priorLoader) {
            mId = id;
            mArgs = args;
            mLoader = loader;
            mPriorLoader = priorLoader;
            mLoader.registerListener(id, this);
        }

        @NonNull
        Loader<D> getLoader() {
            return mLoader;
        }

        @Override
        protected void onActive() {
            if (DEBUG) Log.v(TAG, "  Starting: " + LoaderInfo.this);
            mLoader.startLoading();
        }

        @Override
        protected void onInactive() {
            if (DEBUG) Log.v(TAG, "  Stopping: " + LoaderInfo.this);
            mLoader.stopLoading();
        }

        /**
         * Set the {@link LoaderCallbacks} to associate with this {@link Loader}. This
         * removes any existing {@link LoaderCallbacks}.
         *
         * @param owner The lifecycle that should be used to start and stop the {@link Loader}
         * @param callback The new {@link LoaderCallbacks} to use
         * @return The {@link Loader} associated with this LoaderInfo
         */
        @MainThread
        @NonNull
        Loader<D> setCallback(@NonNull LifecycleOwner owner,
                @NonNull LoaderCallbacks<D> callback) {
            LoaderObserver<D> observer = new LoaderObserver<>(mLoader, callback);
            // Add the new observer
            observe(owner, observer);
            // Loaders only support one observer at a time, so remove the current observer, if any
            if (mObserver != null) {
                removeObserver(mObserver);
            }
            mLifecycleOwner = owner;
            mObserver = observer;
            return mLoader;
        }

        void markForRedelivery() {
            LifecycleOwner lifecycleOwner = mLifecycleOwner;
            LoaderObserver<D> observer = mObserver;
            if (lifecycleOwner != null && observer != null) {
                // Removing and re-adding the observer ensures that the
                // observer is called again, even if they had already
                // received the current data
                // Use super.removeObserver to avoid nulling out mLifecycleOwner & mObserver
                super.removeObserver(observer);
                observe(lifecycleOwner, observer);
            }
        }

        boolean isCallbackWaitingForData() {
            //noinspection SimplifiableIfStatement
            if (!hasActiveObservers()) {
                // No active observers means no one is waiting for data
                return false;
            }
            return mObserver != null && !mObserver.hasDeliveredData();
        }

        @Override
        public void removeObserver(@NonNull Observer<? super D> observer) {
            super.removeObserver(observer);
            // Clear out our references when the observer is removed to avoid leaking
            mLifecycleOwner = null;
            mObserver = null;
        }

        /**
         * Destroys this LoaderInfo, its underlying {@link #getLoader() Loader}, and removes any
         * existing {@link androidx.loader.app.LoaderManager.LoaderCallbacks}.
         *
         * @param reset Whether the LoaderCallbacks and Loader should be reset.
         * @return When reset is false, returns any Loader that still needs to be reset
         */
        @MainThread
        Loader<D> destroy(boolean reset) {
            if (DEBUG) Log.v(TAG, "  Destroying: " + this);
            // First tell the Loader that we don't need it anymore
            mLoader.cancelLoad();
            mLoader.abandon();
            // Then clean up the LoaderObserver
            LoaderObserver<D> observer = mObserver;
            if (observer != null) {
                removeObserver(observer);
                if (reset) {
                    observer.reset();
                }
            }
            // Finally, clean up the Loader
            mLoader.unregisterListener(this);
            if ((observer != null && !observer.hasDeliveredData()) || reset) {
                mLoader.reset();
                return mPriorLoader;
            }
            return mLoader;
        }

        @Override
        public void onLoadComplete(@NonNull Loader<D> loader, @Nullable D data) {
            if (DEBUG) Log.v(TAG, "onLoadComplete: " + this);
            if (Looper.myLooper() == Looper.getMainLooper()) {
                setValue(data);
            } else {
                // The Loader#deliverResult method that calls this should
                // only be called on the main thread, so this should never
                // happen, but we don't want to lose the data
                if (DEBUG) {
                    Log.w(TAG, "onLoadComplete was incorrectly called on a "
                            + "background thread");
                }
                postValue(data);
            }
        }

        @Override
        public void setValue(D value) {
            super.setValue(value);
            // Now that the new data has arrived, we can reset any prior Loader
            if (mPriorLoader != null) {
                mPriorLoader.reset();
                mPriorLoader = null;
            }
        }

        @Override
        public String toString() {
            StringBuilder sb = new StringBuilder(64);
            sb.append("LoaderInfo{");
            sb.append(Integer.toHexString(System.identityHashCode(this)));
            sb.append(" #");
            sb.append(mId);
            sb.append(" : ");
            DebugUtils.buildShortClassTag(mLoader, sb);
            sb.append("}}");
            return sb.toString();
        }

        @SuppressWarnings("deprecation")
        public void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) {
            writer.print(prefix); writer.print("mId="); writer.print(mId);
            writer.print(" mArgs="); writer.println(mArgs);
            writer.print(prefix); writer.print("mLoader="); writer.println(mLoader);
            mLoader.dump(prefix + "  ", fd, writer, args);
            if (mObserver != null) {
                writer.print(prefix); writer.print("mCallbacks="); writer.println(mObserver);
                mObserver.dump(prefix + "  ", writer);
            }
            writer.print(prefix); writer.print("mData="); writer.println(
                    getLoader().dataToString(getValue()));
            writer.print(prefix); writer.print("mStarted="); writer.println(
                    hasActiveObservers());
        }
    }

    /**
     * Encapsulates the {@link LoaderCallbacks} as a {@link Observer}.
     *
     * @param <D> Type of data the LoaderCallbacks handles
     */
    static class LoaderObserver<D> implements Observer<D> {

        private final @NonNull Loader<D> mLoader;
        private final @NonNull LoaderCallbacks<D> mCallback;

        private boolean mDeliveredData = false;

        LoaderObserver(@NonNull Loader<D> loader, @NonNull LoaderCallbacks<D> callback) {
            mLoader = loader;
            mCallback = callback;
        }

        @Override
        public void onChanged(@Nullable D data) {
            if (DEBUG) {
                Log.v(TAG, "  onLoadFinished in " + mLoader + ": "
                        + mLoader.dataToString(data));
            }
            mCallback.onLoadFinished(mLoader, data);
            mDeliveredData = true;
        }

        boolean hasDeliveredData() {
            return mDeliveredData;
        }

        @MainThread
        void reset() {
            if (mDeliveredData) {
                if (DEBUG) Log.v(TAG, "  Resetting: " + mLoader);
                mCallback.onLoaderReset(mLoader);
            }
        }

        @Override
        public String toString() {
            return mCallback.toString();
        }

        public void dump(String prefix, PrintWriter writer) {
            writer.print(prefix); writer.print("mDeliveredData="); writer.println(
                    mDeliveredData);
        }
    }

    /**
     * ViewModel responsible for retaining {@link LoaderInfo} instances across configuration changes
     */
    static class LoaderViewModel extends ViewModel {
        private static final ViewModelProvider.Factory FACTORY = new ViewModelProvider.Factory() {
            @NonNull
            @Override
            @SuppressWarnings("unchecked")
            public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
                return (T) new LoaderViewModel();
            }
        };

        @NonNull
        static LoaderViewModel getInstance(ViewModelStore viewModelStore) {
            return new ViewModelProvider(viewModelStore, FACTORY).get(LoaderViewModel.class);
        }

        private SparseArrayCompat<LoaderInfo> mLoaders = new SparseArrayCompat<>();
        private boolean mCreatingLoader = false;

        void startCreatingLoader() {
            mCreatingLoader = true;
        }

        boolean isCreatingLoader() {
            return mCreatingLoader;
        }

        void finishCreatingLoader() {
            mCreatingLoader = false;
        }

        void putLoader(int id, @NonNull LoaderInfo info) {
            mLoaders.put(id, info);
        }

        @SuppressWarnings("unchecked")
        <D> LoaderInfo<D> getLoader(int id) {
            return mLoaders.get(id);
        }

        void removeLoader(int id) {
            mLoaders.remove(id);
        }

        boolean hasRunningLoaders() {
            int size = mLoaders.size();
            for (int index = 0; index < size; index++) {
                LoaderInfo info = mLoaders.valueAt(index);
                if (info.isCallbackWaitingForData()) {
                    return true;
                }
            }
            return false;
        }

        void markForRedelivery() {
            int size = mLoaders.size();
            for (int index = 0; index < size; index++) {
                LoaderInfo info = mLoaders.valueAt(index);
                info.markForRedelivery();
            }
        }

        @Override
        protected void onCleared() {
            super.onCleared();
            int size = mLoaders.size();
            for (int index = 0; index < size; index++) {
                LoaderInfo info = mLoaders.valueAt(index);
                info.destroy(true);
            }
            mLoaders.clear();
        }

        public void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) {
            if (mLoaders.size() > 0) {
                writer.print(prefix); writer.println("Loaders:");
                String innerPrefix = prefix + "    ";
                for (int i = 0; i < mLoaders.size(); i++) {
                    LoaderInfo info = mLoaders.valueAt(i);
                    writer.print(prefix); writer.print("  #"); writer.print(mLoaders.keyAt(i));
                    writer.print(": "); writer.println(info.toString());
                    info.dump(innerPrefix, fd, writer, args);
                }
            }
        }
    }

    private final @NonNull LifecycleOwner mLifecycleOwner;
    private final @NonNull LoaderViewModel mLoaderViewModel;

    LoaderManagerImpl(@NonNull LifecycleOwner lifecycleOwner,
            @NonNull ViewModelStore viewModelStore) {
        mLifecycleOwner = lifecycleOwner;
        mLoaderViewModel = LoaderViewModel.getInstance(viewModelStore);
    }

    @MainThread
    @NonNull
    private <D> Loader<D> createAndInstallLoader(int id, @Nullable Bundle args,
            @NonNull LoaderCallbacks<D> callback, @Nullable Loader<D> priorLoader) {
        LoaderInfo<D> info;
        try {
            mLoaderViewModel.startCreatingLoader();
            Loader<D> loader = callback.onCreateLoader(id, args);
            if (loader == null) {
                throw new IllegalArgumentException("Object returned from onCreateLoader "
                        + "must not be null");
            }
            if (loader.getClass().isMemberClass()
                    && !Modifier.isStatic(loader.getClass().getModifiers())) {
                throw new IllegalArgumentException("Object returned from onCreateLoader "
                        + "must not be a non-static inner member class: "
                        + loader);
            }
            info = new LoaderInfo<>(id, args, loader, priorLoader);
            if (DEBUG) Log.v(TAG, "  Created new loader " + info);
            mLoaderViewModel.putLoader(id, info);
        } finally {
            mLoaderViewModel.finishCreatingLoader();
        }
        return info.setCallback(mLifecycleOwner, callback);
    }

    @MainThread
    @NonNull
    @Override
    public <D> Loader<D> initLoader(int id, @Nullable Bundle args,
            @NonNull LoaderCallbacks<D> callback) {
        if (mLoaderViewModel.isCreatingLoader()) {
            throw new IllegalStateException("Called while creating a loader");
        }
        if (Looper.getMainLooper() != Looper.myLooper()) {
            throw new IllegalStateException("initLoader must be called on the main thread");
        }

        LoaderInfo<D> info = mLoaderViewModel.getLoader(id);

        if (DEBUG) Log.v(TAG, "initLoader in " + this + ": args=" + args);

        if (info == null) {
            // Loader doesn't already exist; create.
            return createAndInstallLoader(id, args, callback, null);
        } else {
            if (DEBUG) Log.v(TAG, "  Re-using existing loader " + info);
            return info.setCallback(mLifecycleOwner, callback);
        }
    }

    @MainThread
    @NonNull
    @Override
    public <D> Loader<D> restartLoader(int id, @Nullable Bundle args,
            @NonNull LoaderCallbacks<D> callback) {
        if (mLoaderViewModel.isCreatingLoader()) {
            throw new IllegalStateException("Called while creating a loader");
        }
        if (Looper.getMainLooper() != Looper.myLooper()) {
            throw new IllegalStateException("restartLoader must be called on the main thread");
        }

        if (DEBUG) Log.v(TAG, "restartLoader in " + this + ": args=" + args);
        LoaderInfo<D> info = mLoaderViewModel.getLoader(id);
        Loader<D> priorLoader = null;
        if (info != null) {
            priorLoader = info.destroy(false);
        }
        // And create a new Loader
        return createAndInstallLoader(id, args, callback, priorLoader);
    }

    @MainThread
    @Override
    public void destroyLoader(int id) {
        if (mLoaderViewModel.isCreatingLoader()) {
            throw new IllegalStateException("Called while creating a loader");
        }
        if (Looper.getMainLooper() != Looper.myLooper()) {
            throw new IllegalStateException("destroyLoader must be called on the main thread");
        }

        if (DEBUG) Log.v(TAG, "destroyLoader in " + this + " of " + id);
        LoaderInfo info = mLoaderViewModel.getLoader(id);
        if (info != null) {
            info.destroy(true);
            mLoaderViewModel.removeLoader(id);
        }
    }

    @Nullable
    @Override
    public <D> Loader<D> getLoader(int id) {
        if (mLoaderViewModel.isCreatingLoader()) {
            throw new IllegalStateException("Called while creating a loader");
        }

        LoaderInfo<D> info = mLoaderViewModel.getLoader(id);
        return info != null ? info.getLoader() : null;
    }

    @Override
    public void markForRedelivery() {
        mLoaderViewModel.markForRedelivery();
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder(128);
        sb.append("LoaderManager{");
        sb.append(Integer.toHexString(System.identityHashCode(this)));
        sb.append(" in ");
        DebugUtils.buildShortClassTag(mLifecycleOwner, sb);
        sb.append("}}");
        return sb.toString();
    }

    @Deprecated
    @Override
    public void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) {
        mLoaderViewModel.dump(prefix, fd, writer, args);
    }

    @Override
    public boolean hasRunningLoaders() {
        return mLoaderViewModel.hasRunningLoaders();
    }
}