com.wit.android.support.fragment.manage.BaseFragmentFactory.java Source code

Java tutorial

Introduction

Here is the source code for com.wit.android.support.fragment.manage.BaseFragmentFactory.java

Source

/*
 * =================================================================================================
 *                Copyright (C) 2013 - 2014 Martin Albedinsky [Wolf-ITechnologies]
 * =================================================================================================
 *         Licensed under the Apache License, Version 2.0 or later (further "License" only).
 * -------------------------------------------------------------------------------------------------
 * You may use this file only in compliance with the License. More details and copy of this License
 * you may obtain at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 * You can redistribute, modify or publish any part of the code written within this file but as it
 * is described in the License, the software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES or CONDITIONS OF ANY KIND.
 *
 * See the License for the specific language governing permissions and limitations under the License.
 * =================================================================================================
 */
package com.wit.android.support.fragment.manage;

import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.text.TextUtils;
import android.util.Log;
import android.util.SparseArray;

import com.wit.android.support.fragment.annotation.FactoryFragment;
import com.wit.android.support.fragment.annotation.FactoryFragments;
import com.wit.android.support.fragment.annotation.FragmentFactories;
import com.wit.android.support.fragment.util.FragmentAnnotations;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * <h3>Class Overview</h3>
 * Fragment factory which provides base simple implementation of required {@link com.wit.android.support.fragment.manage.FragmentController.FragmentFactory}
 * interface for {@link com.wit.android.support.fragment.manage.FragmentController}.
 * <h3>Accepted annotations</h3>
 * <ul>
 * <li>
 * {@link com.wit.android.support.fragment.annotation.FactoryFragments @FactoryFragments} <b>[class]</b>
 * <p>
 * If this annotation is presented, all ids of fragments presented within this annotation will be
 * attached to an instance of annotated BaseFragmentFactory sub-class, so {@link #isFragmentProvided(int)}
 * will returns always {@code true} for each of these ids.
 * <p>
 * Also, there will be automatically created default tags for all such ids, so they can be obtained
 * by calling {@link #getFragmentTag(int)} with the specific fragment id.
 * </li>
 * <li>
 * {@link com.wit.android.support.fragment.annotation.FactoryFragment @FactoryFragment} <b>[member]</b>
 * <p>
 * This annotation provides same results as {@link com.wit.android.support.fragment.annotation.FactoryFragments @FactoryFragments}
 * annotation, but this annotation is meant to be used to mark directly constant fields which specifies
 * fragment ids and also provides more configuration options like the type of fragment which should
 * be instantiated for the specified id.
 * <p>
 * <b>Note</b>, that tag for fragment with the specified id will be automatically created.
 * </li>
 * <li>
 * {@link com.wit.android.support.fragment.annotation.FragmentFactories @FragmentFactories} <b>[class - inherited]</b>
 * <p>
 * If this annotation is presented, all classes of FragmentFactory provided by this annotation will
 * be instantiated and joined to an instance of annotated BaseFragmentFactory sub-class.
 * </li>
 * </ul>
 *
 * @author Martin Albedinsky
 */
public abstract class BaseFragmentFactory implements FragmentController.FragmentFactory {

    /**
     * Interface ===================================================================================
     */

    /**
     * Constants ===================================================================================
     */

    /**
     * Log TAG.
     */
    private static final String TAG = "BaseFragmentFactory";

    /**
     * Flag indicating whether the debug output trough log-cat is enabled or not.
     */
    // private static final boolean DEBUG_ENABLED = true;

    /**
     * Flag indicating whether the output trough log-cat is enabled or not.
     */
    // private static final boolean LOG_ENABLED = true;

    /**
     * Static members ==============================================================================
     */

    /**
     * Members =====================================================================================
     */

    /**
     * Set of fragment item holders created from annotated fields ({@link FactoryFragment @FactoryDialog})
     * of this factory instance.
     */
    private SparseArray<FragmentItem> mItems;

    /**
     * List with joined factories. Fragment instances and tags requested from this factory are firstly
     * obtained from these factories then from this one.
     */
    private List<FragmentController.FragmentFactory> mFactories;

    /**
     * Id of the fragment which was last checked by {@link #isFragmentProvided(int)}.
     */
    private int mLastCheckedFragmentId = -1;

    /**
     * Flag indicating whether an instance of fragment for {@link #mLastCheckedFragmentId} can be
     * provided by this factory or not.
     */
    private boolean mFragmentProvided = false;

    /**
     * Constructors ================================================================================
     */

    /**
     * Creates a new instance of BaseFragmentFactory. If {@link com.wit.android.support.fragment.annotation.FactoryFragments @FactoryFragments}
     * or {@link com.wit.android.support.fragment.annotation.FragmentFactories @FragmentFactories}
     * annotations are presented above a sub-class of this BaseFragmentFactory, they will be processed
     * here.
     */
    public BaseFragmentFactory() {
        final Class<?> classOfFactory = ((Object) this).getClass();
        /**
         * Process class annotations.
         */
        final SparseArray<FragmentItem> items = new SparseArray<>();
        // Obtain fragment ids.
        if (classOfFactory.isAnnotationPresent(FactoryFragments.class)) {
            final FactoryFragments fragments = classOfFactory.getAnnotation(FactoryFragments.class);

            final int[] ids = fragments.value();
            if (ids.length > 0) {
                for (int id : ids) {
                    items.put(id, new FragmentItem(id, getFragmentTag(id), null));
                }
            }
        }
        this.processAnnotatedFragments(classOfFactory, items);
        if (items.size() > 0) {
            this.mItems = items;
        }
        // Obtain joined factories.
        final List<Class<? extends FragmentController.FragmentFactory>> factories = this.gatherJoinedFactories(
                classOfFactory, new ArrayList<Class<? extends FragmentController.FragmentFactory>>());
        if (!factories.isEmpty()) {
            for (Class<? extends FragmentController.FragmentFactory> factory : factories) {
                FragmentController.FragmentFactory fragmentFactory = null;
                try {
                    fragmentFactory = factory.newInstance();
                } catch (InstantiationException | IllegalAccessException e) {
                    Log.e(TAG, "Failed to instantiate the fragment factory class of(" + factory.getSimpleName()
                            + ")." + "Make sure this fragment factory has public empty constructor.", e);
                }
                if (fragmentFactory != null) {
                    joinFactory(fragmentFactory);
                }
            }
        }
    }

    /**
     * Methods =====================================================================================
     */

    /**
     * Public --------------------------------------------------------------------------------------
     */

    /**
     * Creates a tag for fragment in the required format depends on a package name of the passed
     * <var>classOfFactory</var> and <var>fragmentName</var>.
     * <p>
     * Example format: <u>com.android.app.fragment.ProfileFragments.TAG.EditProfile</u>
     * <p>
     * - where <b>com.android.app.fragment</b> is name of the package where is the specified
     * <var>classOfFactory</var> placed, <b>ProfileFragments</b> is factory class name, <b>EditProfile</b>
     * is <var>fragmentName</var> and <b>TAG</b> is tag identifier.
     *
     * @param classOfFactory Class of factory which provides fragment with the given name.
     * @param fragmentName   Fragment name (can be fragment class name) for which tag should be created.
     * @return Fragment tag in required format, or {@code ""} if <var>fragmentName</var> is
     * {@code null} or empty.
     */
    @Nullable
    public static String createFragmentTag(
            @NonNull Class<? extends FragmentController.FragmentFactory> classOfFactory,
            @NonNull String fragmentName) {
        if (TextUtils.isEmpty(fragmentName)) {
            return null;
        }
        return classOfFactory.getPackage().getName() + "." + classOfFactory.getSimpleName() + ".TAG."
                + fragmentName;
    }

    /**
     */
    @Override
    public boolean isFragmentProvided(int fragmentId) {
        if (fragmentId == mLastCheckedFragmentId) {
            return mFragmentProvided;
        }
        // Store last checked fragment id.
        this.mLastCheckedFragmentId = fragmentId;
        // Check joined factories.
        if (hasJoinedFactories()) {
            for (FragmentController.FragmentFactory factory : mFactories) {
                if (factory.isFragmentProvided(fragmentId)) {
                    return mFragmentProvided = true;
                }
            }
        }
        return mFragmentProvided = providesFragment(fragmentId);
    }

    /**
     */
    @Nullable
    @Override
    public Fragment createFragmentInstance(int fragmentId, @Nullable Bundle params) {
        if (hasJoinedFactories()) {
            // Try to obtain dialog fragment from the current joined factories.
            for (FragmentController.FragmentFactory factory : mFactories) {
                if (factory.isFragmentProvided(fragmentId)) {
                    return factory.createFragmentInstance(fragmentId, params);
                }
            }
        }
        // Create fragment within this factory.
        return onCreateFragmentInstance(fragmentId, params);
    }

    /**
     */
    @Nullable
    @Override
    public String getFragmentTag(int fragmentId) {
        if (hasJoinedFactories()) {
            // Try to obtain tag from the joined factories.
            for (FragmentController.FragmentFactory factory : mFactories) {
                if (factory.isFragmentProvided(fragmentId)) {
                    return factory.getFragmentTag(fragmentId);
                }
            }
        }
        return onGetFragmentTag(fragmentId);
    }

    /**
     */
    @Nullable
    @Override
    public FragmentController.TransactionOptions getFragmentTransactionOptions(int fragmentId,
            @Nullable Bundle params) {
        if (hasJoinedFactories()) {
            // Try to obtain TransactionOptions from the joined factories.
            for (FragmentController.FragmentFactory factory : mFactories) {
                if (factory.isFragmentProvided(fragmentId)) {
                    return factory.getFragmentTransactionOptions(fragmentId, params);
                }
            }
        }
        return onGetFragmentTransactionOptions(fragmentId, params);
    }

    /**
     * Checks whether this factory instance has some joined factories or not.
     *
     * @return {@code True} if there are some joined factories, {@code false} otherwise.
     * @see #getJoinedFactories()
     */
    public boolean hasJoinedFactories() {
        return mFactories != null && !mFactories.isEmpty();
    }

    /**
     * Getters + Setters ---------------------------------------------------------------------------
     */

    /**
     * Joins the given fragment <var>factory</var> with this one.
     * <p>
     * <b>Note</b>, that fragment instances (and their tags) requested upon this factory are
     * obtained from the current joined factories in order as they were joined. If none of the current
     * joined factories provides requested fragment, this factory will handle such a request.
     *
     * @param factory Fragment factory to join with this one.
     * @see #getJoinedFactories()
     */
    public final void joinFactory(@NonNull FragmentController.FragmentFactory factory) {
        this.ensureFactories();
        if (!mFactories.contains(factory)) {
            mFactories.add(factory);
        }
    }

    /**
     * Returns the current joined factories.
     *
     * @return Set of dialog factories or {@code null} if there are not factories joined to this
     * one.
     * @see #hasJoinedFactories()
     * @see #joinFactory(FragmentController.FragmentFactory)
     */
    @Nullable
    public final List<FragmentController.FragmentFactory> getJoinedFactories() {
        return mFactories;
    }

    /**
     * Protected -----------------------------------------------------------------------------------
     */

    /**
     * Invoked whenever {@link #isFragmentProvided(int)} is called and none of the current joined
     * factories provides fragment for the specified <var>fragmentId</var>.
     * <p>
     * This implementation returns {@code true} if there is {@link FactoryFragments @FactoryFragments}
     * or {@link FactoryFragment @FactoryFragment} annotation presented for the specified <var>fragmentId</var>,
     * {@code false} otherwise.
     */
    protected boolean providesFragment(int fragmentId) {
        return (mItems != null) && mItems.indexOfKey(fragmentId) >= 0;
    }

    /**
     * Invoked whenever {@link #getFragmentTag(int)} is called and none of the current joined factories
     * provides fragment for the specified <var>fragmentId</var>.
     * <p>
     * This implementation returns requested tag if there is {@link FactoryFragments @FactoryFragments}
     * or {@link FactoryFragment @FactoryFragment} annotation presented for the specified <var>fragmentId</var>,
     * otherwise {@link #createFragmentTag(Class, String)} will be used to create requested fragment tag.
     */
    @Nullable
    protected String onGetFragmentTag(int fragmentId) {
        return providesFragment(fragmentId) ? mItems.get(fragmentId).tag
                : createFragmentTag(getClass(), Integer.toString(fragmentId));
    }

    /**
     * Invoked whenever {@link #createFragmentInstance(int, android.os.Bundle)} is called and none of
     * the current joined factories provides fragment for the specified <var>fragmentId</var>.
     * <p>
     * This implementation returns the requested fragment instance if there is {@link FactoryFragment @FactoryFragment}
     * annotation presented for the specified <var>fragmentId</var> with valid fragment class type
     * ({@link FactoryFragment#type() @FactoryFragment.type()}), {@code null} otherwise.
     */
    @Nullable
    protected Fragment onCreateFragmentInstance(int fragmentId, @Nullable Bundle params) {
        return providesFragment(fragmentId) ? mItems.get(fragmentId).newInstance(params) : null;
    }

    /**
     * Invoked whenever {@link #getFragmentTransactionOptions(int, android.os.Bundle)} is called and
     * none of the current joined factories provides fragment for the specified <var>fragmentId</var>.
     * <p>
     * This implementation returns default transaction options with {@link FragmentTransition#FADE_IN}
     * transition and tag for the specified <var>fragmentId</var> obtained by {@link #getFragmentTag(int)}.
     */
    @Nullable
    protected FragmentController.TransactionOptions onGetFragmentTransactionOptions(int fragmentId,
            @Nullable Bundle params) {
        return providesFragment(fragmentId) ? new FragmentController.TransactionOptions()
                .transition(FragmentTransition.FADE_IN).tag(getFragmentTag(fragmentId)) : null;
    }

    /**
     * Private -------------------------------------------------------------------------------------
     */

    /**
     * Ensures that the array of joined factories is initialized.
     */
    private void ensureFactories() {
        if (mFactories == null) {
            this.mFactories = new ArrayList<>();
        }
    }

    /**
     * Gathers all classes of fragment factories presented within FragmentFactories annotation. Note,
     * that this is recursive method, which will gather all classes from
     * {@link com.wit.android.support.fragment.annotation.FragmentFactories @FragmentFactories}
     * annotation presented above the given <var>classOfFragment</var>.
     *
     * @param classOfFactory Class of fragment where to check FragmentFactories annotation.
     * @param factories      List of already gathered factories.
     * @return List of all gathered classes of fragment factories.
     */
    private List<Class<? extends FragmentController.FragmentFactory>> gatherJoinedFactories(Class<?> classOfFactory,
            List<Class<? extends FragmentController.FragmentFactory>> factories) {
        if (classOfFactory.isAnnotationPresent(FragmentFactories.class)) {
            final FragmentFactories fragmentFactories = classOfFactory.getAnnotation(FragmentFactories.class);
            if (fragmentFactories.value().length > 0) {
                factories.addAll(Arrays.asList(fragmentFactories.value()));
            }
        }

        // Obtain also factories of super class, but only to this BaseFragmentFactory super.
        final Class<?> superOfFactory = classOfFactory.getSuperclass();
        if (superOfFactory != null && !classOfFactory.equals(BaseFragmentFactory.class)) {
            gatherJoinedFactories(superOfFactory, factories);
        }
        return factories;
    }

    /**
     * Processes all annotated fields marked with {@link com.wit.android.support.fragment.annotation.FactoryFragment @FactoryFragment}
     * annotation and puts them into the given <var>items</var> array.
     *
     * @param classOfFactory Class of this fragment factory.
     * @param items          Initial array of fragment items.
     */
    @SuppressWarnings("unchecked")
    private void processAnnotatedFragments(final Class<?> classOfFactory, final SparseArray<FragmentItem> items) {
        FragmentAnnotations.iterateFields(classOfFactory, new FragmentAnnotations.FieldProcessor() {

            /**
             */
            @Override
            public void onProcessField(@NonNull Field field, @NonNull String name) {
                if (field.isAnnotationPresent(FactoryFragment.class) && int.class.equals(field.getType())) {
                    final FactoryFragment factoryFragment = field.getAnnotation(FactoryFragment.class);
                    try {
                        final int id = (int) field.get(BaseFragmentFactory.this);
                        items.put(id, new FragmentItem(id, TextUtils.isEmpty(factoryFragment.taggedName())
                                ? getFragmentTag(id)
                                : createFragmentTag(
                                        (Class<? extends FragmentController.FragmentFactory>) classOfFactory,
                                        factoryFragment.taggedName()),
                                factoryFragment.type()));
                    } catch (IllegalAccessException e) {
                        e.printStackTrace();
                    }
                }
            }
        });
    }

    /**
     * Inner classes ===============================================================================
     */

    /**
     * Holder for fragment item configuration.
     */
    static class FragmentItem {

        /**
         * Members =================================================================================
         */

        /**
         * Fragment id specified for this item.
         */
        final int id;

        /**
         * Fragment tag specified for this item.
         */
        final String tag;

        /**
         * Fragment type specified for this item.
         */
        final Class<? extends Fragment> type;

        /**
         * Constructors ============================================================================
         */

        /**
         * Creates a new instance of FragmentItem with the given parameters.
         */
        FragmentItem(int id, String tag, Class<? extends Fragment> type) {
            this.id = id;
            this.tag = tag;
            this.type = type;
        }

        /**
         * Methods =================================================================================
         */

        /**
         * Creates a new instance of Fragment specified for this item.
         *
         * @param params Parameters for fragment to be set as its arguments.
         * @return New fragment instance or {@code null} if type of this item is {@link Fragment Fragment.class}
         * which is default and can not be instantiated or instantiation error occur.
         */
        public Fragment newInstance(Bundle params) {
            if (type == null || type.equals(Fragment.class)) {
                return null;
            }
            try {
                final Fragment fragment = type.newInstance();
                if (params != null) {
                    fragment.setArguments(params);
                }
                return fragment;
            } catch (InstantiationException | IllegalAccessException e) {
                Log.e(TAG, "Failed to instantiate fragment class of(" + type
                        + "). Make sure this fragment has public empty constructor.");
            }
            return null;
        }
    }
}