com.wit.android.support.fragment.BaseFragment.java Source code

Java tutorial

Introduction

Here is the source code for com.wit.android.support.fragment.BaseFragment.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;

import android.app.Activity;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.StringRes;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentActivity;
import android.support.v4.app.LoaderManager;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

import com.wit.android.support.fragment.annotation.ClickableViews;
import com.wit.android.support.fragment.annotation.ContentView;
import com.wit.android.support.fragment.annotation.InjectView;
import com.wit.android.support.fragment.annotation.InjectViews;
import com.wit.android.support.fragment.util.FragmentAnnotations;

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

/**
 * <h3>Class Overview</h3>
 * todo: description
 * <h3>Accepted annotations</h3>
 * <ul>
 * <li>
 * {@link com.wit.android.support.fragment.annotation.ContentView @ContentView} <b>[class - inherited]</b>
 * <p>
 * If this annotation is presented, the layout id presented within this annotation will be used to
 * inflate the root view for an instance of annotated BaseFragment sub-class in
 * {@link #onCreateView(android.view.LayoutInflater, android.view.ViewGroup, android.os.Bundle)}.
 * </li>
 * <li>
 * {@link com.wit.android.support.fragment.annotation.ClickableViews @ClickableViews} <b>[class - inherited]</b>
 * <p>
 * If this annotation is presented, an inner {@link android.view.View.OnClickListener} will be attached
 * to all views found by ids presented within this annotation. If any of these views is clicked,
 * {@link #onViewClick(android.view.View, int)} will be invoked with that particular view and its id.
 * <p>
 * OnClickListener is attached to these views whenever {@link #onViewCreated(android.view.View, android.os.Bundle)}
 * is called.
 * </li>
 * <li>{@link com.wit.android.support.fragment.annotation.InjectView @InjectView} <b>[member - recursive]</b></li>
 * <li>
 * {@link com.wit.android.support.fragment.annotation.InjectView.Last @InjectView.Last} <b>[member - recursive]</b>
 * <p>
 * All fields marked with this annotation will be automatically injected (by {@link android.view.View#findViewById(int)})
 * using the root view passed to {@link #onViewCreated(android.view.View, android.os.Bundle)}.
 * <b>Note that {@link com.wit.android.support.fragment.annotation.InjectViews @InjectViews}
 * annotation is required above each sub-class of BaseFragment to run injecting process, otherwise
 * all marked fields (views) of such a sub-class will be ignored.</b>
 * <p>
 * All marked view fields are injected whenever {@link #onViewCreated(android.view.View, android.os.Bundle)}
 * is called.
 * </li>
 * </ul>
 *
 * @author Martin Albedinsky
 * @see com.wit.android.support.fragment.ActionBarFragment
 */
public abstract class BaseFragment extends Fragment implements BackPressWatcher, ViewClickWatcher {

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

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

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

    /**
     * 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;

    /**
     * Flag indicating whether this instance of fragment is restored (like after orientation change)
     * or not.
     */
    private static final int PFLAG_RESTORED = 0x01;

    /**
     * Flag indicating whether the view of this instance of fragment is restored or not.
     */
    private static final int PFLAG_VIEW_RESTORED = 0x02;

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

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

    /**
     * Activity to which is this instance of fragment currently attached, {@code null} if this
     * fragment is not attached to any activity.
     */
    Activity mActivity;

    /**
     * Stores all private flags for this object.
     */
    int mPrivateFlags;

    /**
     * Content view annotation holding configuration for the root view of this fragment.
     */
    private ContentView mContentView;

    /**
     * Array with ids of views to which should be attached instance of {@link android.view.View.OnClickListener}.
     * When one of these views is clicked, {@link #onViewClick(android.view.View, int)} is invoked to
     * handle click event.
     */
    private List<Integer> mClickableViewIds;

    /**
     * Array with gathered view fields which should be injected to this fragment whenever the new
     * root view hierarchy is created for this fragment instance.
     */
    private List<Field> mViewsToInject;

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

    /**
     * Creates a new instance of BaseFragment. If {@link com.wit.android.support.fragment.annotation.ContentView @ContentView}
     * or {@link com.wit.android.support.fragment.annotation.ClickableViews @ClickableViews} annotations
     * are presented above a sub-class of BaseFragment, they will be processed here. Also all declared
     * fields marked by annotation {@link com.wit.android.support.fragment.annotation.InjectView @InjectView}
     * or {@link com.wit.android.support.fragment.annotation.InjectView.Last @InjectView.Last} will
     * be recursively gathered and stored to be later injected.
     */
    public BaseFragment() {
        this.processClassAnnotations(((Object) this).getClass());
    }

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

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

    /**
     * Creates a new instance of the given <var>classOfFragment</var> with the given <var>args</var>.
     *
     * @param classOfFragment Class of the desired fragment to instantiate.
     * @param args            Arguments to set to new instance of fragment by {@link Fragment#setArguments(android.os.Bundle)}.
     * @param <F>             Type of the desired fragment.
     * @return New instance of fragment with the given arguments or {@code null} if some instantiation
     * error occurs.
     */
    @Nullable
    public static <F extends Fragment> F newInstanceWithArguments(@NonNull Class<F> classOfFragment,
            @Nullable Bundle args) {
        try {
            final F fragment = classOfFragment.newInstance();
            fragment.setArguments(args);
            return fragment;
        } catch (java.lang.InstantiationException | IllegalAccessException e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     */
    @Override
    public void onAttach(Activity activity) {
        super.onAttach(activity);
        this.mActivity = activity;
    }

    /**
     */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        this.updatePrivateFlags(PFLAG_VIEW_RESTORED, false);
        this.updatePrivateFlags(PFLAG_RESTORED, savedInstanceState != null);
    }

    /**
     */
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        if (mContentView != null) {
            if (mContentView.attachToRoot()) {
                inflater.inflate(mContentView.value(), container, true);
                return null;
            }
            return inflater.inflate(mContentView.value(), container, false);
        }
        return super.onCreateView(inflater, container, savedInstanceState);
    }

    /**
     */
    @Override
    public void onViewCreated(View view, Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        // Resolve view background.
        if (mContentView != null) {
            if (mContentView.backgroundRes() >= 0) {
                view.setBackgroundResource(mContentView.backgroundRes());
            }
        }
        // Set up clickable views.
        final ClickListener clickListener = new ClickListener();
        if (mClickableViewIds != null) {
            for (int id : mClickableViewIds) {
                View child = view.findViewById(id);
                if (child == null) {
                    throw new NullPointerException("Clickable view with id(" + id + ") not found.");
                }
                child.setOnClickListener(clickListener);
            }
        }

        if (mViewsToInject != null) {
            for (Field field : mViewsToInject) {
                FragmentAnnotations.injectView(field, this, view, clickListener);
            }
        }
    }

    /**
     */
    @Override
    public void onDestroyView() {
        super.onDestroyView();
        this.updatePrivateFlags(PFLAG_VIEW_RESTORED, true);
    }

    /**
     */
    @Override
    public void onDestroy() {
        super.onDestroy();
        this.updatePrivateFlags(PFLAG_VIEW_RESTORED, false);
    }

    /**
     */
    @Override
    public void onDetach() {
        super.onDetach();
        this.mActivity = null;
    }

    /**
     */
    @Override
    public boolean dispatchBackPressed() {
        return onBackPressed();
    }

    /**
     */
    @Override
    public boolean dispatchViewClick(View view) {
        return onViewClick(view, view.getId());
    }

    /**
     * Returns flag indicating whether this fragment instance was restored or not.
     *
     * @return {@code True} if this fragment was restored (<i>like, after orientation change</i>),
     * {@code false} otherwise.
     */
    public boolean isRestored() {
        return hasPrivateFlag(PFLAG_RESTORED);
    }

    /**
     * Returns flag indicating whether the view was restored or not.
     *
     * @return {@code True} if the view of this fragment was restored (<i>like, when the fragment
     * was showed from the back stack</i>), {@code false} otherwise.
     */
    public boolean isViewRestored() {
        return hasPrivateFlag(PFLAG_VIEW_RESTORED);
    }

    /**
     * Returns flag indicating whether the view is already created or not.
     *
     * @return {@code True} if the view of this fragment is already created, {@code false}
     * otherwise.
     */
    public boolean isViewCreated() {
        return getView() != null;
    }

    /**
     * Same as {@link #getString(int)}, but first is performed check if the parent activity of this
     * fragment instance is available to prevent illegal state exceptions.
     */
    @NonNull
    public String obtainString(@StringRes int resId) {
        return isActivityAvailable() ? getString(resId) : "";
    }

    /**
     * Same as  {@link #getString(int, Object...)}, but first is performed check if the parent activity
     * of this fragment instance is available to prevent illegal state exceptions.
     */
    @NonNull
    public String obtainString(@StringRes int resId, @Nullable Object... args) {
        return isActivityAvailable() ? getString(resId, args) : "";
    }

    /**
     * Same as {@link #getText(int)}, but first is performed check if the parent activity of this
     * fragment instance is available to prevent illegal state exceptions.
     */
    @NonNull
    public CharSequence obtainText(@StringRes int resId) {
        return isActivityAvailable() ? getText(resId) : "";
    }

    /**
     * Wrapped {@link android.app.Activity#runOnUiThread(Runnable)} on this fragment's parent activity.
     *
     * @return {@code True} if parent activity is available and action was posted, {@code false}
     * otherwise.
     */
    public final boolean runOnUiThread(@NonNull Runnable action) {
        if (isActivityAvailable()) {
            getActivity().runOnUiThread(action);
            return true;
        }
        return false;
    }

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

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

    /**
     * Invoked immediately after {@link #dispatchBackPressed()} was called to process back press event.
     *
     * @return {@code True} if this instance of fragment processes dispatched back press event,
     * {@code false} otherwise.
     */
    protected boolean onBackPressed() {
        return false;
    }

    /**
     * Invoked immediately after {@link #dispatchViewClick(android.view.View)} was called to process
     * click event on the given <var>view</var>.
     *
     * @param view The view which was clicked.
     * @param id   The id of the clicked view.
     * @return {@code True} if this fragment handles dispatched click event for the given
     * <var>view</var>, {@code false} otherwise.
     */
    protected boolean onViewClick(@NonNull View view, int id) {
        return false;
    }

    /**
     * Returns flag indicating whether the parent Activity of this fragment instance is available or not.
     * <p>
     * Parent activity is always available between {@link #onAttach(android.app.Activity)} and
     * {@link #onDetach()} life cycle calls.
     *
     * @return {@code True} if activity is available, {@code false} otherwise.
     */
    protected boolean isActivityAvailable() {
        return mActivity != null;
    }

    /**
     * Starts a loader with the specified <var>id</var>. If there was already started loader with the
     * same id before, such a loader will be <b>re-started</b>, otherwise new loader will be <b>initialized</b>.
     * <p>
     * See {@link android.support.v4.app.LoaderManager#restartLoader(int, Bundle, android.support.v4.app.LoaderManager.LoaderCallbacks)} and
     * {@link android.support.v4.app.LoaderManager#initLoader(int, Bundle, android.support.v4.app.LoaderManager.LoaderCallbacks)} for more info.
     *
     * @param id        Id of the desired loader to start.
     * @param params    Params for loader.
     * @param callbacks Callbacks for loader.
     * @return {@code True} if loader with the specified id was <b>initialized</b> or <b>re-started</b>,
     * {@code false} if the current activity is already invalid or {@link android.support.v4.app.LoaderManager} is not available.
     */
    protected boolean startLoader(int id, @Nullable Bundle params,
            @NonNull LoaderManager.LoaderCallbacks callbacks) {
        if (mActivity != null) {
            final LoaderManager loaderManager = ((FragmentActivity) mActivity).getSupportLoaderManager();
            if (loaderManager != null) {
                if (loaderManager.getLoader(id) != null) {
                    loaderManager.restartLoader(id, params, callbacks);
                } else {
                    loaderManager.initLoader(id, params, callbacks);
                }
                return true;
            }
        }
        return false;
    }

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

    /**
     * Called to process all annotations of the specified <var>classOfFragment</var>.
     *
     * @param classOfFragment The class of which annotations to process.
     */
    private void processClassAnnotations(Class<?> classOfFragment) {
        // Obtain content view.
        this.mContentView = FragmentAnnotations.obtainAnnotationFrom(classOfFragment, ContentView.class,
                BaseFragment.class);
        // Obtain clickable view ids.
        // Note, that we will gather ids from all annotated class to this parent.
        this.mClickableViewIds = this.gatherClickableViewIds(classOfFragment, new ArrayList<Integer>());
        if (mClickableViewIds.isEmpty()) {
            this.mClickableViewIds = null;
        }
        // Store all fields to inject as views.
        this.mViewsToInject = new ArrayList<>();
        this.iterateInjectableViewFields(classOfFragment, new FragmentAnnotations.FieldProcessor() {

            /**
             */
            @Override
            public void onProcessField(@NonNull Field field, @NonNull String name) {
                if (field.isAnnotationPresent(InjectView.class)
                        || field.isAnnotationPresent(InjectView.Last.class)) {
                    mViewsToInject.add(field);
                }
            }
        });
        if (mViewsToInject.isEmpty()) {
            this.mViewsToInject = null;
        }
    }

    /**
     * Gathers all ids presented within ClickableViews annotation. Note, that this is recursive method,
     * which will gather all ids from {@link com.wit.android.support.fragment.annotation.ClickableViews}
     * annotation presented above the given <var>classOfFragment</var>.
     *
     * @param classOfFragment Class of fragment where to check ClickableViews annotation.
     * @param ids             List of already gathered ids.
     * @return List of all gathered clickable view's ids.
     */
    private List<Integer> gatherClickableViewIds(Class<?> classOfFragment, List<Integer> ids) {
        if (classOfFragment.isAnnotationPresent(ClickableViews.class)) {
            final ClickableViews clickableViews = classOfFragment.getAnnotation(ClickableViews.class);
            if (clickableViews.value().length > 0) {
                ids.addAll(idsToList(clickableViews.value()));
            }
        }
        // Obtain also ids of super class, but only to this BaseFragment super.
        final Class<?> superOfFragment = classOfFragment.getSuperclass();
        if (superOfFragment != null && !superOfFragment.equals(BaseFragment.class)) {
            gatherClickableViewIds(superOfFragment, ids);
        }
        return ids;
    }

    /**
     * Iterates all declared fields of the given <var>classOfFragment</var> using
     * {@link FragmentAnnotations#iterateFields(Class, FragmentAnnotations.FieldProcessor)} utility
     * method passing it the given <var>fieldProcessor</var> and classOfFragment.
     *
     * @param classOfFragment Class of which fields to iterate.
     * @param fieldProcessor  Field processor to handle fields iteration.
     */
    private void iterateInjectableViewFields(Class<?> classOfFragment,
            FragmentAnnotations.FieldProcessor fieldProcessor) {
        if (classOfFragment.isAnnotationPresent(InjectViews.class)) {
            FragmentAnnotations.iterateFields(classOfFragment, fieldProcessor);
        }
        // Iterate also fields of super class, but only to this BaseFragment super.
        final Class<?> superOfFragment = classOfFragment.getSuperclass();
        if (superOfFragment != null && !superOfFragment.equals(BaseFragment.class)) {
            iterateInjectableViewFields(superOfFragment, fieldProcessor);
        }
    }

    /**
     * Converts the given <var>ids</var> array to list.
     *
     * @param ids The array of ids to convert.
     * @return Converted list of ids.
     */
    private List<Integer> idsToList(int[] ids) {
        final List<Integer> idsList = new ArrayList<>(ids.length);
        for (int id : ids) {
            idsList.add(id);
        }
        return idsList;
    }

    /**
     * Updates the current private flags.
     *
     * @param flag Value of the desired flag to add/remove to/from the current private flags.
     * @param add  Boolean flag indicating whether to add or remove the specified <var>flag</var>.
     */
    void updatePrivateFlags(int flag, boolean add) {
        if (add) {
            this.mPrivateFlags |= flag;
        } else {
            this.mPrivateFlags &= ~flag;
        }
    }

    /**
     * Returns a boolean flag indicating whether the specified <var>flag</var> is contained within
     * the current private flags or not.
     *
     * @param flag Value of the flag to check.
     * @return {@code True} if the requested flag is contained, {@code false} otherwise.
     */
    boolean hasPrivateFlag(int flag) {
        return (mPrivateFlags & flag) != 0;
    }

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

    /**
     * Simple on view click listener to attach to clickable views of this fragment's class.
     */
    private final class ClickListener implements View.OnClickListener {

        /**
         */
        @Override
        public void onClick(View view) {
            dispatchViewClick(view);
        }
    }
}