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

Java tutorial

Introduction

Here is the source code for com.albedinsky.android.support.intent.ContentIntent.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.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.net.Uri;
import android.os.Environment;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.StringDef;
import android.support.v4.app.Fragment;
import android.text.TextUtils;

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

import java.io.File;
import java.io.IOException;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Locale;

/**
 * A {@link BaseIntent} builder implementation providing base API for building of intents targeting
 * a <b>content previewing/editing/obtaining</b> related applications.
 * <p>
 * The content intent can be intent to obtain or preview specific type of content, like <b>image,
 * audio, video, ...</b>. To decide which type of intent (obtain/preview) should be started, the
 * current set of {@link ContentHandler} is checked, if it isn't empty the
 * <b>OBTAIN</b> intent will be started, so <b>chooser</b> dialog ill be showed with a list item
 * specific for each of current providers. If there are no providers assigned to this intent builder
 * and there was passed valid {@link Uri} to {@link #input(Uri)}, he <b>PREVIEW</b>
 * intent will be started.
 *
 * @author Martin Albedinsky
 */
public abstract class ContentIntent<I extends ContentIntent<I>> extends BaseIntent<I> {

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

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

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

    /**
     * Defines an annotation for determining set of allowed mime types for {@link #dataType(String)}
     * method.
     */
    @Retention(RetentionPolicy.SOURCE)
    @StringDef({ MimeType.TEXT, MimeType.TEXT_PLAIN, MimeType.TEXT_HTML, MimeType.IMAGE, MimeType.IMAGE_JPEG,
            MimeType.IMAGE_PNG, MimeType.IMAGE_BITMAP, MimeType.AUDIO, MimeType.AUDIO_MP3, MimeType.AUDIO_MP4,
            MimeType.AUDIO_MPEG, MimeType.VIDEO, MimeType.VIDEO_MP4, MimeType.VIDEO_MPEG, MimeType.VIDEO_JPEG,
            MimeType.VIDEO_3GP, MimeType.VIDEO_3GP2 })
    public @interface DataType {
    }

    /**
     * Name format for files created by this type of intent.
     * <p>
     * Constant Value: <b>yyyyMMdd_HHmmss</b>
     */
    public static final String CONTENT_FILE_TIME_STAMP_FORMAT = "yyyyMMdd_HHmmss";

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

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

    /**
     * Uri to content.
     */
    Uri mUri;

    /**
     * Data type of content pointed by {@link #mUri}.
     */
    String mDataType;

    /**
     * Flag indicating whether {@link #mUri} has been specified as input uri via {@link #input(Uri)}
     * or not.
     */
    private boolean mHasInputUri;

    /**
     * Set of content handlers that will be used to create a chooser dialog (if there are any).
     */
    private List<ContentHandler> mHandlers;

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

    /**
     * Creates a new instance of ContentIntent for the given <var>activity</var> context.
     * See {@link com.albedinsky.android.support.intent.BaseIntent#BaseIntent(Activity)}
     * for additional info.
     */
    public ContentIntent(@NonNull Activity activity) {
        super(activity);
    }

    /**
     * Creates a new instance of ContentIntent for the given <var>fragment</var> context.
     * See {@link com.albedinsky.android.support.intent.BaseIntent#BaseIntent(android.support.v4.app.Fragment)}
     * for additional info.
     */
    public ContentIntent(@NonNull Fragment fragment) {
        super(fragment);
    }

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

    /**
     * Creates a time stamp in the {@link #CONTENT_FILE_TIME_STAMP_FORMAT} format for the current
     * {@link Date} that can be used as name for a content file.
     *
     * @return String representation of the current time stamp.
     */
    @NonNull
    public static String createContentFileTimeStamp() {
        return new SimpleDateFormat(CONTENT_FILE_TIME_STAMP_FORMAT, Locale.getDefault()).format(new Date());
    }

    /**
     * Same as {@link #createContentFile(String, File)} with <b>directory</b> obtained
     * via {@link Environment#getExternalStoragePublicDirectory(String)} with the specified
     * <var>externalDirectoryType</var> as <var>type</var>.
     *
     * @param fileName              The desired name for the requested file.
     * @param externalDirectoryType One of {@link Environment#DIRECTORY_PICTURES}, {@link Environment#DIRECTORY_MOVIES}, ....
     */
    @Nullable
    public static File createContentFile(@NonNull String fileName, @NonNull String externalDirectoryType) {
        return createContentFile(fileName, Environment.getExternalStoragePublicDirectory(externalDirectoryType));
    }

    /**
     * Creates a new file with the given parameters within the specified <var>directory</var>.
     *
     * @param fileName  The desired name for the requested file. Must also contain a suffix for the
     *                  file.
     * @param directory The directory within which should be the requested file created.
     * @return New instance of the desired file or {@code null} if some IO error occurs during its
     * creation process.
     * @see #createContentFile(String,  String)
     */
    @Nullable
    public static File createContentFile(@NonNull String fileName, @NonNull File directory) {
        try {
            final File file = new File(directory.getPath() + File.separator + fileName);
            return file.createNewFile() ? file : null;
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * Appends the specified <var>suffix</var> to the specified <var>fileName</var> if there is not
     * presented any yet.
     * <p>
     * <b>Note</b>, that this will append the given suffix only in case that the specified file name
     * does not contain any "." char.
     *
     * @param fileName The file name where to append the suffix.
     * @param defaultSuffix The suffix to append if necessary.
     * @return The given file name with the appended suffix if necessary.
     */
    static String appendDefaultFileSuffixIfNotPresented(String fileName, String defaultSuffix) {
        return fileName.contains(".") ? fileName : fileName + defaultSuffix;
    }

    /**
     * Populates this intent builder with default handler ({@link ContentHandler}) items.
     * <p>
     * Type and count of default handlers may differ depending on a specific ContentIntent implementation.
     *
     * @return This intent builder to allow methods chaining.
     */
    public abstract ContentIntent withDefaultHandlers();

    /**
     * Same as {@link #withHandlers(List)} for var-ContentHandlers.
     *
     * @param handlers The desired set of handlers to add.
     */
    public I withHandlers(@NonNull ContentHandler... handlers) {
        return withHandlers(Arrays.asList(handlers));
    }

    /**
     * Same as {@link #withHandler(ContentHandler)} for list of handler items.
     *
     * @param handlers The desired list of handlers items to add.
     * @return This intent builder to allow methods chaining.
     */
    @SuppressWarnings("unchecked")
    public I withHandlers(@NonNull List<ContentHandler> handlers) {
        if (handlers.size() > 0) {
            ensureContentHandlers(handlers.size());
            mHandlers.addAll(handlers);
        }
        return (I) this;
    }

    /**
     * Adds the specified <var>handler</var> item into the set of handlers. These handler items
     * will be used to build a chooser dialog with list of these items (if any) so a user can choose
     * one of them to handle a specific content intent. Such dialog is created and showed whenever
     * content intent is started via {@link #start()} and there is at least one handler item.
     * <p>
     * All assigned handlers can be accessed via {@link #handlers()}.
     *
     * @param handler The desired handler item to add.
     * @return This intent builder to allow methods chaining.
     * @see #withHandlers(ContentHandler...)
     * @see #withDefaultHandlers()
     * @see #clearHandlers()
     */
    @SuppressWarnings("unchecked")
    public I withHandler(@NonNull ContentHandler handler) {
        this.ensureContentHandlers(1);
        mHandlers.add(handler);
        return (I) this;
    }

    /**
     * Returns the set of content handlers to be displayed in a chooser dialog.
     *
     * @return List of handlers or {@link Collections#EMPTY_LIST} if no content handlers has been
     * added yet.
     * @see #withHandler(ContentHandler)
     * @see #withHandlers(List)
     * @see #withDefaultHandlers()
     * @see #clearHandlers()
     */
    @NonNull
    @SuppressWarnings("unchecked")
    public List<ContentHandler> handlers() {
        return mHandlers != null ? mHandlers : Collections.EMPTY_LIST;
    }

    /**
     * Clears the current set of content handlers. New handlers can be added via
     * {@link #withHandler(ContentHandler)} or {@link #withHandlers(ContentHandler...)}.
     *
     * @return This intent builder to allow methods chaining.
     */
    public ContentIntent clearHandlers() {
        if (mHandlers != null) {
            mHandlers.clear();
            this.mHandlers = null;
        }
        return this;
    }

    /**
     * Same as {@link #input(Uri)} with <var>uri</var> created from the given <var>file</var>
     * if not {@code null}.
     *
     * @param file The desired file to be used to crate an Uri. Can be {@code null}.
     */
    @SuppressWarnings("unchecked")
    public I input(@Nullable File file) {
        if (file != null)
            input(Uri.fromFile(file));
        else
            input((Uri) null);
        return (I) this;
    }

    /**
     * Sets an uri to a content that should be previewed by an activity that can handle/preview the
     * content of the data type specified via {@link #dataType(String)}. The specified uri will be
     * attached to an intent build via {@link #build()} if there are no content handlers
     * associated with this intent builder.
     * <p>
     * <b>Note</b>, that the current <b>data type</b> will be set to {@code null}, so {@link #dataType(String)}
     * should be called immediately after a new uri is set. A specific implementations of this
     * ContentIntent builder can here specify a default data type.
     *
     * @param uri The desired uri, which should be delivered to handling activity.
     * @return This intent builder to allow methods chaining.
     * @see #uri()
     */
    @SuppressWarnings("unchecked")
    public I input(@Nullable Uri uri) {
        this.mUri = uri;
        this.mDataType = null;
        this.mHasInputUri = uri != null;
        return (I) this;
    }

    /**
     * Same as {@link #output(Uri)} with <var>uri</var> created from the given <var>file</var>
     * if not {@code null}.
     *
     * @param file The desired file to be used to crate an Uri. Can be {@code null}.
     */
    @SuppressWarnings("unchecked")
    public I output(@Nullable File file) {
        if (file != null)
            output(Uri.fromFile(file));
        else
            output((Uri) null);
        return (I) this;
    }

    /**
     * Sets an uri where should be stored a content provided by an activity that can handle/provide
     * the content of the data type specified via {@link #dataType(String)}. The specified uri will
     * be attached to an intent of one of content handlers associated with this intent builder.
     *
     * @param uri The desired uri.
     * @return This intent builder to allow methods chaining.
     * @see #uri()
     */
    @SuppressWarnings("unchecked")
    public I output(@Nullable Uri uri) {
        this.mUri = uri;
        this.mDataType = null;
        this.mHasInputUri = false;
        return (I) this;
    }

    /**
     * Returns the uri passed to {@link #input(Uri)} or {@link #output(Uri)}.
     *
     * @return Current uri or {@code null} if there was no uri set yet.
     * @see #input(Uri)
     * @see #output(Uri)
     */
    @Nullable
    public Uri uri() {
        return mUri;
    }

    /**
     * Sets a data (MIME) type for the content uri.
     *
     * @param type The desired MIME type for the uri specified via {@link #input(Uri)}.
     * @return This intent builder to allow methods chaining.
     * @see #dataType()
     */
    @SuppressWarnings("unchecked")
    public I dataType(@NonNull @DataType String type) {
        this.mDataType = type;
        return (I) this;
    }

    /**
     * Returns the content's data (MIME) type.
     *
     * @return MIME type for the uri specified via {@link #input(Uri)} or {@code null} if no data
     * type has been specified yet.
     * @see #dataType(String)
     */
    @Nullable
    @DataType
    public String dataType() {
        return mDataType;
    }

    /**
     */
    @Override
    public boolean start() {
        if (mContextWrapper.getContext() == null) {
            throw new NullPointerException("Context is already invalid.");
        }
        if (hasContentHandlers()) {
            onShowDialogWithHandlers(mHandlers, mDialogTitle);
            return true;
        }
        final Intent intent = build();
        if (hasActivityForIntent(intent)) {
            return onStart(build());
        }
        notifyActivityNotFound();
        return false;
    }

    /**
     * Checks whether there are some content handlers or not.
     *
     * @return {@code True} if this builder has some content handler items, {@code false} otherwise.
     */
    private boolean hasContentHandlers() {
        return mHandlers != null && !mHandlers.isEmpty();
    }

    /**
     * Invoked from {@link #start()} if there is at least one {@link ContentHandler} assigned to this
     * intent builder to show a chooser dialog.
     *
     * @param handlers    Set of handlers assigned to this intent builder.
     * @param dialogTitle The title for the chooser dialog.
     */
    protected void onShowDialogWithHandlers(@NonNull final List<ContentHandler> handlers,
            @NonNull CharSequence dialogTitle) {
        final Context context = mContextWrapper.getContext();
        if (context == null) {
            throw new NullPointerException("Context is already invalid.");
        }
        final int n = handlers.size();
        final CharSequence[] providerNames = new CharSequence[n];
        for (int i = 0; i < n; i++) {
            providerNames[i] = handlers.get(i).name;
        }
        final AlertDialog.Builder builder = new AlertDialog.Builder(context);
        builder.setTitle(dialogTitle);
        builder.setItems(providerNames, new DialogInterface.OnClickListener() {

            /**
            */
            @Override
            @SuppressWarnings("ResourceType")
            public void onClick(DialogInterface dialog, int which) {
                final ContentHandler provider = handlers.get(which);
                if (provider.requestCode >= 0) {
                    mContextWrapper.startIntentForResult(provider.intent, provider.requestCode);
                } else {
                    mContextWrapper.startIntent(provider.intent);
                }
            }
        });
        builder.show();
    }

    /**
     * @throws IllegalStateException If there is at least one {@link ContentHandler} assigned to
     *                               this intent builder.
     */
    @NonNull
    @Override
    public Intent build() {
        if (hasContentHandlers()) {
            throw new IllegalStateException("Cannot build intent for set of ContentHandlers.");
        }
        return super.build();
    }

    /**
     */
    @Override
    protected void ensureCanBuildOrThrow() {
        super.ensureCanBuildOrThrow();
        if (!mHasInputUri) {
            throw cannotBuildIntentException("No input Uri specified.");
        }
        if (TextUtils.isEmpty(mDataType)) {
            throw cannotBuildIntentException("No MIME type specified for input Uri.");
        }
    }

    /**
     * Will be invoked only if there are no content handlers assigned to this intent builder.
     */
    @NonNull
    @Override
    protected Intent onBuild() {
        return new Intent(Intent.ACTION_VIEW).setDataAndType(mUri, mDataType);
    }

    /**
     */
    @Override
    protected boolean onStart(@NonNull Intent intent) {
        startActivity(Intent.createChooser(intent, mDialogTitle));
        return true;
    }

    /**
     * Ensures that list of content handlers is initialized.
     *
     * @param initialSize Initial capacity for the list.
     */
    private void ensureContentHandlers(int initialSize) {
        if (mHandlers == null)
            this.mHandlers = new ArrayList<>(initialSize);
    }

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

    /**
     * A ContentHandler is a simple class that can be used to add one item into {@link ContentIntent}
     * builder. Such an item will than displayed in a chooser dialog with all added handler items.
     * Each ContentHandler must have its name specified that will be displayed in a list item's view.
     * There must be also specified an intent that will be started whenever an item of a particular
     * handler is clicked.
     *
     * @author Martin Albedinsky
     * @see ContentHandler#ContentHandler(CharSequence, Intent)
     */
    public static class ContentHandler {

        /**
         * Name of this "content handler" to be displayed within chooser dialog list.
         */
        final CharSequence name;

        /**
         * Intent to be started when an item addressed to this handler has been clicked.
         */
        final Intent intent;

        /**
         * Request code used for {@link IntentContextWrapper#startIntentForResult(Intent, int) SimpleContextWrapper#startIntentForResult(android.content.Intent, int)}.
         */
        int requestCode = -1;

        /**
         * Creates a new instance of ContentHandler with the specified <var>name</var> and <var>intent</var>.
         * <p>
         * <b>Note</b>, that the given <var>intent</var> will be started via
         * {@link IntentContextWrapper#startIntent(Intent)} or via {@link IntentContextWrapper#startIntentForResult(Intent, int)}
         * if the request code specified via {@link #requestCode(int)} is {@code none-negative} number.
         *
         * @param name   Name of the new "content handler" to be displayed within <b>chooser dialog</b>
         *               list.
         * @param intent Intent to be started when an item addressed to this handler within
         *               <b>chooser dialog</b> is clicked.
         */
        public ContentHandler(@NonNull CharSequence name, @NonNull Intent intent) {
            this.name = name;
            this.intent = intent;
        }

        /**
         * Returns the name of this handler.
         *
         * @return This handlers's name.
         */
        @NonNull
        public CharSequence name() {
            return name;
        }

        /**
         * Returns the intent attached to this handler.
         *
         * @return This handler's intent.
         */
        @NonNull
        public Intent intent() {
            return intent;
        }

        /**
         * Sets a request code used when starting the intent specified during initialization.
         * <p>
         * If {@code none-negative}, {@link IntentContextWrapper#startIntentForResult(Intent, int)}
         * will be used to start that intent.
         *
         * @param code The desired request code. This code should be used to identify result from
         *             started intent in {@link Activity#onActivityResult(int, int, Intent)}
         *             or {@link android.support.v4.app.Fragment#onActivityResult(int, int, Intent)},
         *             depends on from within which context has been {@link ContentIntent} started.
         * @return This handler to allow methods chaining.
         * @see #requestCode()
         */
        public ContentHandler requestCode(int code) {
            this.requestCode = code;
            return this;
        }

        /**
         * Returns the request code assigned to this handler.
         *
         * @return This handler's request code.
         * @see #requestCode(int)
         */
        public int requestCode() {
            return requestCode;
        }
    }
}