com.albedinsky.android.support.intent.BaseIntent.java Source code

Java tutorial

Introduction

Here is the source code for com.albedinsky.android.support.intent.BaseIntent.java

Source

/*
 * =================================================================================================
 *                             Copyright (C) 2014 Martin Albedinsky
 * =================================================================================================
 *         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.albedinsky.android.support.intent;

import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.support.annotation.AnimRes;
import android.support.annotation.CheckResult;
import android.support.annotation.IntRange;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.StringRes;
import android.support.v4.app.Fragment;
import android.util.AndroidRuntimeException;
import android.widget.Toast;

import com.albedinsky.android.support.intent.internal.IntentActivityWrapper;
import com.albedinsky.android.support.intent.internal.IntentFragmentWrapper;
import com.albedinsky.android.support.intent.internal.IntentContextWrapper;

/**
 * A BaseIntent specifies base API for intent builders. Some Android intents require a lots of data
 * to be supplied to them and the intent builders should provide API to simplify such a set up process.
 * <p>
 * A specific implementation of BaseIntent builder is designed primarily for usage within {@link Activity}
 * or {@link Fragment} context:
 * <ul>
 * <li>{@link #BaseIntent(Activity)}</li>
 * <li>{@link #BaseIntent(Fragment)}</li>
 * </ul>
 * Each of these contexts is wrapped into {@link IntentContextWrapper} that simplifies and unifies
 * accessing all necessary data and logic provided by both these contexts.
 *
 * <h3>Implementation</h3>
 * Implementations of the BaseIntent class need to implement these methods:
 * <ul>
 * <li>
 * {@link #build()}
 * <p>
 * This method is called whenever {@link #start()} is invoked and a specific BaseIntent builder
 * implementation should build here an intent which is for that type of intent builder specific
 * and populate it with a data supplied from outside through its setters API.
 * </li>
 * <li>
 * {@link #onStart(Intent)}
 * <p>
 * It is not required to override this method. Default implementation of this method only starts
 * the intent obtained via {@link #build()} method using {@link #startActivity(Intent)}.
 * If a specific BaseIntent implementation requires for example starting intent as activity for result,
 * than it should be done within this method.
 * </li>
 * </ul>
 *
 * <h3>Activity transitions</h3>
 * BaseIntent class supports also API for supplying of activity transitions via {@link #enterTransition(int)}
 * and {@link #exitTransition(int)}. These transitions will be used whenever {@link #startActivity(Intent)}
 * or {@link #startActivityForResult(Intent, int)} is called.
 *
 * @param <I> A type of the BaseIntent builder implementation to allow proper setter methods chaining.
 * @author Martin Albedinsky
 */
public abstract class BaseIntent<I extends BaseIntent> {

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

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

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

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

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

    /**
     * Title text for the activity chooser dialog.
     */
    CharSequence mDialogTitle = "Choose";

    /**
     * Wrapper for an intent's context.
     */
    IntentContextWrapper mContextWrapper;

    /**
     * Message text for the toast, in case, when there is no activity to process requested intent.
     */
    private CharSequence mActivityNotFoundMessage = "No application found to handle this action";

    /**
     * Window transition resource id.
     */
    private int mEnterTransition, mExitTransition;

    /**
     * Flag indicating whether to apply window transition or not.
     */
    private boolean mApplyTransitions;

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

    /**
     * Creates a new instance of BaseIntent representation for the given activity context. Note, that
     * this activity receives result in the {@link Activity#onActivityResult(int, int, Intent)}
     * in case, that this intent was started for some result.
     *
     * @param activity Activity context.
     */
    public BaseIntent(@NonNull Activity activity) {
        this(new IntentActivityWrapper(activity));
    }

    /**
     * Creates a new instance of BaseIntent representation for the given fragment context. Note, that
     * this fragment receives result in the {@link android.support.v4.app.Fragment#onActivityResult(int, int, Intent)}
     * in case, that this intent was started for some result.
     *
     * @param fragment Fragment context.
     */
    public BaseIntent(@NonNull Fragment fragment) {
        this(new IntentFragmentWrapper(fragment));
    }

    /**
     * Creates a new instance of BaseIntent representation for the given context wrapper.
     *
     * @param contextWrapper Context wrapper used to access the current context data.
     */
    private BaseIntent(IntentContextWrapper contextWrapper) {
        this.mContextWrapper = contextWrapper;
        this.mDialogTitle = contextWrapper.getString(R.string.intent_chooser_dialog_title);
    }

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

    /**
     * Same as {@link #dialogTitle(CharSequence)} for resource id.
     *
     * @param resId The resource if of desired title text.
     */
    public I dialogTitle(@StringRes int resId) {
        return dialogTitle(obtainText(resId));
    }

    /**
     * Sets a title for the chooser dialog.
     * <p>
     * Default value: <b>Choose</b>
     *
     * @param title The desired title text.
     * @return This intent builder to allow methods chaining.
     * @see #dialogTitle()
     */
    @SuppressWarnings("unchecked")
    public I dialogTitle(@Nullable CharSequence title) {
        this.mDialogTitle = title != null ? title : "";
        return (I) this;
    }

    /**
     * Returns the title text for the chooser dialog.
     * <p>
     * Default value: <b>Choose</b>
     *
     * @return Title text.
     */
    @NonNull
    public CharSequence dialogTitle() {
        return mDialogTitle != null ? mDialogTitle : "";
    }

    /**
     * Same as {@link #activityNotFoundMessage(CharSequence)} for resource id.
     *
     * @param resId Resource id of the desired message text.
     */
    public I activityNotFoundMessage(@StringRes int resId) {
        return activityNotFoundMessage(obtainText(resId));
    }

    /**
     * Sets a message for the toast that is showed in case, when there wasn't found any activity
     * to handle started intent.
     *
     * @param message The desired message text.
     * @return This intent builder to allow methods chaining.
     * @see #activityNotFoundMessage()
     */
    @SuppressWarnings("unchecked")
    public I activityNotFoundMessage(@Nullable CharSequence message) {
        this.mActivityNotFoundMessage = message;
        return (I) this;
    }

    /**
     * Returns the message text for the toast that is showed in case, when there wasn't found any
     * activity to handle started intent.
     *
     * @return Message text.
     * @see #activityNotFoundMessage(CharSequence)
     */
    @Nullable
    public CharSequence activityNotFoundMessage() {
        return mActivityNotFoundMessage;
    }

    /**
     * Returns the context wrapper of this intent builder.
     *
     * @return One of {@link IntentActivityWrapper},
     * {@link IntentFragmentWrapper}.
     */
    @NonNull
    public final IntentContextWrapper contextWrapper() {
        return mContextWrapper;
    }

    /**
     * Sets a transition to be used when starting intent to override default pending window enter
     * transition. See {@link Activity#overridePendingTransition(int, int)} for more info.
     *
     * @param transition Resource id of the desired window transition (animation).
     * @return This intent builder to allow methods chaining.
     */
    public I enterTransition(@AnimRes int transition) {
        return transitions(transition, mExitTransition);
    }

    /**
     * Returns the enter transition that should be used when starting intent to override default
     * pending window enter transition.
     *
     * @return Resource id of the enter transition or {@code 0} if there was no transition set yet.
     * @see #enterTransition(int)
     */
    @AnimRes
    public int enterTransition() {
        return mEnterTransition;
    }

    /**
     * Sets a transition to be used when starting intent to override default pending window exit
     * transition. See {@link Activity#overridePendingTransition(int, int)} for more info.
     *
     * @param transition Resource id of the desired window transition (animation).
     * @return This intent builder to allow methods chaining.
     */
    public I exitTransition(@AnimRes int transition) {
        return transitions(mEnterTransition, transition);
    }

    /**
     * Returns the exit transition that should be used when starting intent to override default
     * pending window exit transition.
     *
     * @return Resource id of the exit transition or {@code 0} if there was no transition set yet.
     * @see #exitTransition(int) (int)
     */
    @AnimRes
    public int exitTransition() {
        return mExitTransition;
    }

    /**
     * Sets both, enter + exit, transitions to be used when starting intent to override default pending
     * window transitions. See {@link Activity#overridePendingTransition(int, int)} for more info.
     *
     * @param enterTransition Resource id of the desired window enter transition (animation).
     * @param exitTransition  Resource id of the desired window exit transition (animation).
     * @return This intent builder to allow methods chaining.
     */
    @SuppressWarnings("unchecked")
    public I transitions(@AnimRes int enterTransition, @AnimRes int exitTransition) {
        if (enterTransition >= 0 || exitTransition >= 0) {
            this.mEnterTransition = enterTransition;
            this.mExitTransition = exitTransition;
            this.mApplyTransitions = true;
        } else {
            this.mApplyTransitions = false;
        }
        return (I) this;
    }

    /**
     * Obtains a string from the attached context.
     *
     * @param resId Resource id of the desired string.
     * @return Obtained string from application resources.
     */
    @NonNull
    protected final String obtainString(@StringRes int resId) {
        return resId != 0 ? mContextWrapper.getString(resId) : "";
    }

    /**
     * Obtains a text from the attached context.
     *
     * @param resId Resource id of the desired text.
     * @return Obtained text from application resources.
     */
    @NonNull
    protected final CharSequence obtainText(@StringRes int resId) {
        return resId != 0 ? mContextWrapper.getText(resId) : "";
    }

    /**
     * Starts an instance of intent created by {@link #build()}.
     * <p>
     * <b>Note</b>, that if there is no activity found to handle created intent, {@link #notifyActivityNotFound()}
     * will be invoked from here and {@code false} will be returned.
     *
     * @return {@code True} if intent was started, {@code false} otherwise.
     * @throws NullPointerException If the attached context is already invalid.
     */
    public boolean start() {
        if (mContextWrapper.getContext() == null) {
            throw new NullPointerException("Context is already invalid.");
        }
        final Intent intent = build();
        if (hasActivityForIntent(intent)) {
            return onStart(intent);
        }
        notifyActivityNotFound();
        return false;
    }

    /**
     * Called to create an instance of {@link Intent} from the current data of this
     * intent builder implementation.
     *
     * @return An instance of Intent specific for this intent builder.
     * @throws AndroidRuntimeException If this builder does not have all necessary data to build the
     * requested intent.
     */
    @NonNull
    public Intent build() {
        ensureCanBuildOrThrow();
        return onBuild();
    }

    /**
     * Called to ensure that this builder can build its Intent from its current data. If not an
     * exception create via {@link #cannotBuildIntentException(String)} can be thrown.
     */
    protected void ensureCanBuildOrThrow() {
    }

    /**
     * Invoked from {@link #build()} after {@link #ensureCanBuildOrThrow()} to build
     * intent specific for this intent builder from the current data.
     *
     * @return Intent instance with current data.
     */
    protected abstract Intent onBuild();

    /**
     * Checks whether there is any activity that can handle the specified <var>intent</var>.
     *
     * @param intent The intent for which to resolve activity.
     * @return {@code True} if activity for the intent has been resolved/found so we can call
     * {@link #startActivity(Intent)} or {@link #startActivityForResult(Intent, int)} with that
     * intent, {@code false} otherwise, that means that there is no activity currently installed on
     * actual Android device that can handle that intent.
     */
    @CheckResult
    @SuppressWarnings("ConstantConditions")
    protected boolean hasActivityForIntent(@NonNull Intent intent) {
        final Context context = mContextWrapper.getContext();
        return intent.resolveActivity(context.getPackageManager()) != null;
    }

    /**
     * Creates an instance of {@link AndroidRuntimeException} indicating that one of required parameters
     * for this intent builder has not been specified, but {@link #build()} has been invoked.
     * <p>
     * This can be invoked from {@link #build()} to handle such situation.
     *
     * @param message Message to be included into exception.
     */
    protected final AndroidRuntimeException cannotBuildIntentException(@NonNull String message) {
        final String intentName = getClass().getSimpleName();
        return new AndroidRuntimeException("Cannot build " + intentName + ". " + message);
    }

    /**
     * Invoked whenever {@link #start()} is called and there is an activity available that can handle
     * intent created via {@link #build()}.
     *
     * @param intent Instance of the Intent created via {@link #build()}.
     * @return {@code True} to indicate that the intent has been started, {@code false} otherwise.
     */
    @CheckResult
    protected boolean onStart(@NonNull Intent intent) {
        startActivity(intent);
        return true;
    }

    /**
     * Same as {@link IntentContextWrapper#startIntent(Intent)}.
     *
     * @param intent The instance of Intent to start.
     */
    protected final void startActivity(@NonNull Intent intent) {
        mContextWrapper.startIntent(intent);
        if (mApplyTransitions) {
            mContextWrapper.overridePendingTransition(mEnterTransition, mExitTransition);
        }
    }

    /**
     * Same as {@link IntentContextWrapper#startIntentForResult(Intent, int)}.
     * <p>
     * Note, that here is also handled {@link android.content.ActivityNotFoundException} exception.
     *
     * @param intent      The instance of Intent to start.
     * @param requestCode Request code for the intent.
     */
    protected final void startActivityForResult(@NonNull Intent intent,
            @IntRange(from = 0, to = Integer.MAX_VALUE) int requestCode) {
        mContextWrapper.startIntentForResult(intent, requestCode);
        if (mApplyTransitions) {
            mContextWrapper.overridePendingTransition(mEnterTransition, mExitTransition);
        }
    }

    /**
     * Notifies message for situation, when there wasn't found any activity to handle started intent.
     * <p>
     * Actually, this shows a toast with the message passed to {@link #activityNotFoundMessage(CharSequence)}.
     *
     * @see #activityNotFoundMessage(CharSequence)
     */
    protected void notifyActivityNotFound() {
        final Context context = mContextWrapper.getContext();
        if (context != null) {
            Toast.makeText(context, mActivityNotFoundMessage, Toast.LENGTH_LONG).show();
        }
    }

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