Java tutorial
/* * ================================================================================================= * 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; } } }