All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.gh.bmd.jrt.android.v11.routine.LoaderInvocation Maven / Gradle / Ivy

There is a newer version: 6.0.0
Show newest version
/*
 * 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 com.gh.bmd.jrt.android.v11.routine;

import android.annotation.TargetApi;
import android.app.Activity;
import android.app.Fragment;
import android.app.LoaderManager;
import android.app.LoaderManager.LoaderCallbacks;
import android.content.Context;
import android.content.Loader;
import android.os.Build.VERSION_CODES;
import android.os.Bundle;
import android.util.SparseArray;

import com.gh.bmd.jrt.android.builder.AndroidRoutineBuilder;
import com.gh.bmd.jrt.android.builder.AndroidRoutineBuilder.CacheStrategy;
import com.gh.bmd.jrt.android.builder.AndroidRoutineBuilder.ClashResolution;
import com.gh.bmd.jrt.android.builder.InputClashException;
import com.gh.bmd.jrt.android.builder.InvocationClashException;
import com.gh.bmd.jrt.android.invocation.AndroidInvocation;
import com.gh.bmd.jrt.builder.RoutineConfiguration;
import com.gh.bmd.jrt.builder.RoutineConfiguration.OrderType;
import com.gh.bmd.jrt.channel.InputChannel;
import com.gh.bmd.jrt.channel.OutputChannel;
import com.gh.bmd.jrt.channel.ResultChannel;
import com.gh.bmd.jrt.channel.StandaloneChannel;
import com.gh.bmd.jrt.channel.StandaloneChannel.StandaloneInput;
import com.gh.bmd.jrt.common.ClassToken;
import com.gh.bmd.jrt.common.InvocationException;
import com.gh.bmd.jrt.common.RoutineException;
import com.gh.bmd.jrt.common.WeakIdentityHashMap;
import com.gh.bmd.jrt.invocation.SingleCallInvocation;
import com.gh.bmd.jrt.log.Logger;
import com.gh.bmd.jrt.time.TimeDuration;

import java.lang.ref.WeakReference;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.List;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;

import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;

import static com.gh.bmd.jrt.builder.RoutineConfiguration.builder;

/**
 * Invocation implementation employing loaders to perform background operations.
 * 

* Created by davide on 12/11/14. * * @param the input data type. * @param the output data type. */ @TargetApi(VERSION_CODES.HONEYCOMB) class LoaderInvocation extends SingleCallInvocation { private static final WeakIdentityHashMap>>> sCallbackMap = new WeakIdentityHashMap>>>(); private final CacheStrategy mCacheStrategy; private final ClashResolution mClashResolution; private final Constructor> mConstructor; private final WeakReference mContext; private final int mLoaderId; private final Logger mLogger; private final OrderType mOrderType; /** * Constructor. * * @param context the context reference. * @param loaderId the loader ID. * @param resolution the clash resolution type. * @param cacheStrategy the result cache type. * @param constructor the invocation constructor. * @param order the input data order. * @param logger the logger instance. * @throws java.lang.NullPointerException if any of the specified non-null parameters is null. */ @SuppressWarnings("ConstantConditions") LoaderInvocation(@Nonnull final WeakReference context, final int loaderId, @Nullable final ClashResolution resolution, @Nullable final CacheStrategy cacheStrategy, @Nonnull final Constructor> constructor, @Nullable final OrderType order, @Nonnull final Logger logger) { if (context == null) { throw new NullPointerException("the context reference must not be null"); } if (constructor == null) { throw new NullPointerException("the invocation constructor must not be null"); } mContext = context; mLoaderId = loaderId; mClashResolution = (resolution == null) ? ClashResolution.ABORT_THAT_INPUT : resolution; mCacheStrategy = (cacheStrategy == null) ? CacheStrategy.CLEAR : cacheStrategy; mConstructor = constructor; mOrderType = order; mLogger = logger.subContextLogger(this); } /** * Destroys the loader with the specified ID. * * @param context the context. * @param loaderId the loader ID. */ static void purgeLoader(@Nonnull final Object context, final int loaderId) { final SparseArray>> callbackArray = sCallbackMap.get(context); if (callbackArray == null) { return; } final LoaderManager loaderManager; if (context instanceof Activity) { final Activity activity = (Activity) context; loaderManager = activity.getLoaderManager(); } else if (context instanceof Fragment) { final Fragment fragment = (Fragment) context; loaderManager = fragment.getLoaderManager(); } else { throw new IllegalArgumentException( "invalid context type: " + context.getClass().getCanonicalName()); } int i = 0; while (i < callbackArray.size()) { final RoutineLoaderCallbacks callbacks = callbackArray.valueAt(i).get(); if (callbacks == null) { callbackArray.removeAt(i); continue; } final RoutineLoader loader = callbacks.mLoader; if ((loaderId == callbackArray.keyAt(i)) && (loader.getInvocationCount() == 0)) { loaderManager.destroyLoader(loaderId); callbackArray.removeAt(i); continue; } ++i; } if (callbackArray.size() == 0) { sCallbackMap.remove(context); } } /** * Destroys all loaders with the specified invocation class and the specified inputs. * * @param context the context. * @param loaderId the loader ID. * @param invocationClass the invocation class. * @param inputs the invocation inputs. */ @SuppressWarnings("unchecked") static void purgeLoader(@Nonnull final Object context, final int loaderId, @Nonnull final Class invocationClass, @Nonnull final List inputs) { final SparseArray>> callbackArray = sCallbackMap.get(context); if (callbackArray == null) { return; } final LoaderManager loaderManager; if (context instanceof Activity) { final Activity activity = (Activity) context; loaderManager = activity.getLoaderManager(); } else if (context instanceof Fragment) { final Fragment fragment = (Fragment) context; loaderManager = fragment.getLoaderManager(); } else { throw new IllegalArgumentException( "invalid context type: " + context.getClass().getCanonicalName()); } int i = 0; while (i < callbackArray.size()) { final RoutineLoaderCallbacks callbacks = callbackArray.valueAt(i).get(); if (callbacks == null) { callbackArray.removeAt(i); continue; } final RoutineLoader loader = (RoutineLoader) callbacks.mLoader; if ((loader.getInvocationType() == invocationClass) && (loader.getInvocationCount() == 0)) { final int id = callbackArray.keyAt(i); if (((loaderId == AndroidRoutineBuilder.AUTO) || (loaderId == id)) && loader.areSameInputs(inputs)) { loaderManager.destroyLoader(id); callbackArray.removeAt(i); continue; } } ++i; } if (callbackArray.size() == 0) { sCallbackMap.remove(context); } } /** * Destroys the loader with the specified ID and the specified inputs. * * @param context the context. * @param loaderId the loader ID. * @param inputs the invocation inputs. */ @SuppressWarnings("unchecked") static void purgeLoader(@Nonnull final Object context, final int loaderId, @Nonnull final List inputs) { final SparseArray>> callbackArray = sCallbackMap.get(context); if (callbackArray == null) { return; } final LoaderManager loaderManager; if (context instanceof Activity) { final Activity activity = (Activity) context; loaderManager = activity.getLoaderManager(); } else if (context instanceof Fragment) { final Fragment fragment = (Fragment) context; loaderManager = fragment.getLoaderManager(); } else { throw new IllegalArgumentException( "invalid context type: " + context.getClass().getCanonicalName()); } int i = 0; while (i < callbackArray.size()) { final RoutineLoaderCallbacks callbacks = callbackArray.valueAt(i).get(); if (callbacks == null) { callbackArray.removeAt(i); continue; } final RoutineLoader loader = (RoutineLoader) callbacks.mLoader; if ((loader.getInvocationCount() == 0) && (loaderId == callbackArray.keyAt(i)) && loader .areSameInputs(inputs)) { loaderManager.destroyLoader(loaderId); callbackArray.removeAt(i); continue; } ++i; } if (callbackArray.size() == 0) { sCallbackMap.remove(context); } } /** * Destroys all loaders with the specified invocation class. * * @param context the context. * @param loaderId the loader ID. * @param invocationClass the invocation class. */ static void purgeLoaders(@Nonnull final Object context, final int loaderId, @Nonnull final Class invocationClass) { final SparseArray>> callbackArray = sCallbackMap.get(context); if (callbackArray == null) { return; } final LoaderManager loaderManager; if (context instanceof Activity) { final Activity activity = (Activity) context; loaderManager = activity.getLoaderManager(); } else if (context instanceof Fragment) { final Fragment fragment = (Fragment) context; loaderManager = fragment.getLoaderManager(); } else { throw new IllegalArgumentException( "invalid context type: " + context.getClass().getCanonicalName()); } int i = 0; while (i < callbackArray.size()) { final RoutineLoaderCallbacks callbacks = callbackArray.valueAt(i).get(); if (callbacks == null) { callbackArray.removeAt(i); continue; } final RoutineLoader loader = callbacks.mLoader; if ((loader.getInvocationType() == invocationClass) && (loader.getInvocationCount() == 0)) { final int id = callbackArray.keyAt(i); if ((loaderId == AndroidRoutineBuilder.AUTO) || (loaderId == id)) { loaderManager.destroyLoader(id); callbackArray.removeAt(i); continue; } } ++i; } if (callbackArray.size() == 0) { sCallbackMap.remove(context); } } @Override @SuppressWarnings("unchecked") @SuppressFBWarnings(value = "BC_UNCONFIRMED_CAST_OF_RETURN_VALUE", justification = "class comparison with == is done") public void onCall(@Nonnull final List inputs, @Nonnull final ResultChannel result) { final Logger logger = mLogger; final Object context = mContext.get(); if (context == null) { logger.dbg("avoiding running invocation since context is null"); return; } final Context loaderContext; final LoaderManager loaderManager; if (context instanceof Activity) { final Activity activity = (Activity) context; loaderContext = activity.getApplicationContext(); loaderManager = activity.getLoaderManager(); logger.dbg("running invocation bound to activity: %s", activity); } else if (context instanceof Fragment) { final Fragment fragment = (Fragment) context; loaderContext = fragment.getActivity().getApplicationContext(); loaderManager = fragment.getLoaderManager(); logger.dbg("running invocation bound to fragment: %s", fragment); } else { throw new IllegalArgumentException( "invalid context type: " + context.getClass().getCanonicalName()); } int loaderId = mLoaderId; if (loaderId == AndroidRoutineBuilder.AUTO) { loaderId = 31 * mConstructor.getDeclaringClass().hashCode() + inputs.hashCode(); logger.dbg("generating invocation ID: %d", loaderId); } final Loader> loader = loaderManager.getLoader(loaderId); final boolean isClash = isClash(loader, loaderId, inputs); final WeakIdentityHashMap>>> callbackMap = sCallbackMap; SparseArray>> callbackArray = callbackMap.get(context); if (callbackArray == null) { callbackArray = new SparseArray>>(); callbackMap.put(context, callbackArray); } final WeakReference> callbackReference = callbackArray.get(loaderId); RoutineLoaderCallbacks callbacks = (callbackReference != null) ? (RoutineLoaderCallbacks) callbackReference.get() : null; if ((callbacks == null) || (loader == null) || isClash) { final RoutineLoader routineLoader; if (!isClash && (loader != null) && (loader.getClass() == RoutineLoader.class)) { routineLoader = (RoutineLoader) loader; } else { routineLoader = null; } final RoutineLoaderCallbacks newCallbacks = createCallbacks(loaderContext, loaderManager, routineLoader, inputs, loaderId); if (callbacks != null) { logger.dbg("resetting existing callbacks [%d]", loaderId); callbacks.reset(); } callbackArray.put(loaderId, new WeakReference>(newCallbacks)); callbacks = newCallbacks; } logger.dbg("setting result cache type [%d]: %s", loaderId, mCacheStrategy); callbacks.setCacheStrategy(mCacheStrategy); final OutputChannel outputChannel = callbacks.newChannel(); if (isClash) { logger.dbg("restarting loader [%d]", loaderId); loaderManager.restartLoader(loaderId, Bundle.EMPTY, callbacks); } else { logger.dbg("initializing loader [%d]", loaderId); loaderManager.initLoader(loaderId, Bundle.EMPTY, callbacks); } result.pass(outputChannel); } private RoutineLoaderCallbacks createCallbacks(@Nonnull final Context loaderContext, @Nonnull final LoaderManager loaderManager, @Nullable final RoutineLoader loader, @Nonnull final List inputs, final int loaderId) { final Logger logger = mLogger; final Constructor> constructor = mConstructor; final AndroidInvocation invocation; try { logger.dbg("creating a new instance of class [%d]: %s", loaderId, constructor.getDeclaringClass()); invocation = constructor.newInstance(); invocation.onContext(loaderContext.getApplicationContext()); } catch (final InvocationTargetException e) { logger.err(e, "error creating the invocation instance [%d]", loaderId); throw new InvocationException(e.getCause()); } catch (final RoutineException e) { logger.err(e, "error creating the invocation instance [%d]", loaderId); throw e; } catch (final Throwable t) { logger.err(t, "error creating the invocation instance [%d]", loaderId); throw new InvocationException(t); } final RoutineLoader callbacksLoader = (loader != null) ? loader : new RoutineLoader(loaderContext, invocation, inputs, mOrderType, logger); return new RoutineLoaderCallbacks(loaderManager, callbacksLoader, logger); } @SuppressFBWarnings(value = "BC_UNCONFIRMED_CAST", justification = "class comparison with == is done") private boolean isClash(@Nullable final Loader> loader, final int loaderId, @Nonnull final List inputs) { if (loader == null) { return false; } final Logger logger = mLogger; if (loader.getClass() != RoutineLoader.class) { logger.err("clashing invocation ID [%d]: %s", loaderId, loader.getClass().getCanonicalName()); throw new InvocationClashException(loaderId); } final RoutineLoader routineLoader = (RoutineLoader) loader; final Class> invocationClass = mConstructor.getDeclaringClass(); if ((new ClassToken>() {}.getRawClass() != invocationClass) && (routineLoader.getInvocationType() != invocationClass)) { logger.wrn("clashing invocation ID [%d]: %s", loaderId, routineLoader.getInvocationType().getCanonicalName()); throw new InvocationClashException(loaderId); } final ClashResolution resolution = mClashResolution; if (resolution == ClashResolution.ABORT_THAT) { logger.dbg("restarting existing invocation [%d]", loaderId); return true; } else if (resolution == ClashResolution.ABORT_THIS) { logger.dbg("aborting invocation [%d]", loaderId); throw new InputClashException(loaderId); } else if ((resolution == ClashResolution.KEEP_THAT) || routineLoader.areSameInputs( inputs)) { logger.dbg("keeping existing invocation [%d]", loaderId); return false; } else if (resolution == ClashResolution.ABORT_THAT_INPUT) { logger.dbg("restarting existing invocation [%d]", loaderId); return true; } else if (resolution == ClashResolution.ABORT_THIS_INPUT) { logger.dbg("aborting invocation [%d]", loaderId); throw new InputClashException(loaderId); } return true; } /** * Loader callbacks implementation.
* The callbacks object will make sure that the loader results are passed to the returned output * channels. * * @param the output data type. */ private static class RoutineLoaderCallbacks implements LoaderCallbacks> { private final ArrayList> mChannels = new ArrayList>(); private final RoutineLoader mLoader; private final LoaderManager mLoaderManager; private final Logger mLogger; private final ArrayList> mNewChannels = new ArrayList>(); private CacheStrategy mCacheStrategy; private int mResultCount; /** * Constructor. * * @param loaderManager the loader manager. * @param loader the loader instance. * @param logger the logger instance. */ private RoutineLoaderCallbacks(@Nonnull final LoaderManager loaderManager, @Nonnull final RoutineLoader loader, @Nonnull final Logger logger) { mLoaderManager = loaderManager; mLoader = loader; mLogger = logger.subContextLogger(this); } /** * Creates and returns a new output channel.
* The channel will be used to deliver the loader results. * * @return the new output channel. */ @Nonnull public OutputChannel newChannel() { final Logger logger = mLogger; logger.dbg("creating new result channel"); final RoutineLoader internalLoader = mLoader; final ArrayList> channels = mNewChannels; final RoutineConfiguration configuration = builder().withOutputSize(Integer.MAX_VALUE) .withOutputTimeout( TimeDuration.ZERO) .withLog(logger.getLog()) .withLogLevel(logger.getLogLevel()) .buildConfiguration(); final StandaloneChannel channel = JRoutine.standalone().withConfiguration(configuration).buildChannel(); channels.add(channel.input()); internalLoader.setInvocationCount( Math.max(channels.size(), internalLoader.getInvocationCount())); return channel.output(); } public Loader> onCreateLoader(final int id, final Bundle args) { mLogger.dbg("creating Android loader: %d", id); return mLoader; } public void onLoadFinished(final Loader> loader, final InvocationResult data) { final Logger logger = mLogger; final ArrayList> channels = mChannels; final ArrayList> newChannels = mNewChannels; logger.dbg("dispatching invocation result: %s", data); if (data.passTo(newChannels, channels)) { final ArrayList> channelsToClose = new ArrayList>(channels); channelsToClose.addAll(newChannels); mResultCount += channels.size() + newChannels.size(); channels.clear(); newChannels.clear(); final RoutineLoader internalLoader = mLoader; if (mResultCount >= internalLoader.getInvocationCount()) { mResultCount = 0; internalLoader.setInvocationCount(0); final CacheStrategy cacheStrategy = mCacheStrategy; if ((cacheStrategy == CacheStrategy.CLEAR) || (data.isError() ? (cacheStrategy == CacheStrategy.CACHE_IF_SUCCESS) : (cacheStrategy == CacheStrategy.CACHE_IF_ERROR))) { final int id = internalLoader.getId(); logger.dbg("destroying Android loader: %d", id); mLoaderManager.destroyLoader(id); } } if (data.isError()) { final Throwable exception = data.getAbortException(); for (final StandaloneInput channel : channelsToClose) { channel.abort(exception); } } else { for (final StandaloneInput channel : channelsToClose) { channel.close(); } } } else { channels.addAll(newChannels); newChannels.clear(); } } public void onLoaderReset(final Loader> loader) { mLogger.dbg("resetting Android loader: %d", mLoader.getId()); reset(); } private void reset() { mLogger.dbg("aborting result channels"); final ArrayList> channels = mChannels; final ArrayList> newChannels = mNewChannels; final InvocationClashException reason = new InvocationClashException(mLoader.getId()); for (final InputChannel channel : channels) { channel.abort(reason); } channels.clear(); for (final InputChannel newChannel : newChannels) { newChannel.abort(reason); } newChannels.clear(); } private void setCacheStrategy(@Nonnull final CacheStrategy cacheStrategy) { mLogger.dbg("setting cache type: %s", cacheStrategy); mCacheStrategy = cacheStrategy; } } }