Java tutorial
/* * Copyright (c) 2015 Magnet Systems, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.magnet.max.android; import android.content.Context; import android.graphics.Bitmap; import android.os.AsyncTask; import android.os.Parcel; import android.os.Parcelable; import android.util.Log; import android.webkit.MimeTypeMap; import com.google.gson.annotations.SerializedName; import com.magnet.max.android.util.EqualityUtil; import com.magnet.max.android.util.HashCodeBuilder; import com.magnet.max.android.util.ParcelableHelper; import com.magnet.max.android.util.StringUtil; import com.squareup.okhttp.MediaType; import com.squareup.okhttp.MultipartBuilder; import com.squareup.okhttp.RequestBody; import com.squareup.okhttp.ResponseBody; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.UnsupportedEncodingException; import java.util.HashMap; import java.util.Map; import java.util.UUID; import java.util.concurrent.atomic.AtomicReference; import retrofit.Callback; import retrofit.Response; /** * Attachment is used to save/download large content (such as file) to/from Max server. */ final public class Attachment implements Parcelable { private static final String CONTENT_SHOULD_NOT_BE_EMPTY = "content shouldn't be empty"; /** * Status of the attachment */ public enum Status { INIT, INLINE, TRANSFERING, COMPLETE, ERROR } /** * Type of the source content */ public enum ContentSourceType { TEXT, FILE, INPUT_STREAM, BYTE_ARRAY, BITMAP } /** * Listener for events uploading a attachment */ public interface UploadListener { /** * Start to upload the attachment * @param attachment */ void onStart(Attachment attachment); //void onProgress(Attachment attachment, long processedBytes); /** * The uploading is completed successfully * @param attachment */ void onComplete(Attachment attachment); /** * The uploading fails * @param attachment * @param error */ void onError(Attachment attachment, Throwable error); } protected static abstract class AbstractDownloadListener<T> { /** * Downloading starts */ public void onStart() { } //void onProgress(Attachment attachment, long processedBytes); /** * Downloading finished and return the content * @param content */ public abstract void onComplete(T content); /** * Downloading fails * @param error */ public abstract void onError(Throwable error); } /** * Download the content of attachment as byte array */ public static abstract class DownloadAsBytesListener extends AbstractDownloadListener<byte[]> { } /** * Download the content of attachment to a file */ public static abstract class DownloadAsFileListener extends AbstractDownloadListener<File> { } /** * Download the content of attachment as {@link InputStream} * The stream should be read in a background thread */ public static abstract class DownloadAsStreamListener extends AbstractDownloadListener<InputStream> { } /** * Download the content of attachment as String */ public static abstract class DownloadAsStringListener extends AbstractDownloadListener<String> { } private static final int DEFAULT_BUFFER_SIZE = 1024 * 4; private static final String TAG = Attachment.class.getSimpleName(); public static final String META_FILE_ID = "metadata_file_id"; public static final String MIME_TYPE_IMAGE = "image"; public static final String MIME_TYPE_VIDEO = "video"; public static final String TEXT_PLAIN = "text/plain"; public static final String TEXT_HTML = "text/html"; private static File defaultDownloadDir; protected transient Status mStatus = Status.INIT; @SerializedName("sourceType") protected ContentSourceType mSourceType; @SerializedName("name") protected String mName; @SerializedName("summary") protected String mSummary; @SerializedName("mimeType") protected String mMimeType; @SerializedName("length") protected long mLength = -1; private String charsetName; protected transient Object mContent; protected transient byte[] mData; /** The id to retrieve the attachment from server */ @SerializedName("attachmentId") protected String mAttachmentId; private String senderId; @SerializedName("metaData") private transient Map<String, String> mMetaData; /** * Create from a {@link File} * @param content * @param mimeType */ public Attachment(File content, String mimeType) { this(content, mimeType, null, null); } /** * Create from a file with name and summary * @param content * @param mimeType * @param name * @param summary */ public Attachment(File content, String mimeType, String name, String summary) { if (null == content) { throw new IllegalArgumentException(CONTENT_SHOULD_NOT_BE_EMPTY); } if (!content.exists()) { throw new IllegalArgumentException("content file doesn't exist"); } this.mLength = content.length(); mSourceType = ContentSourceType.FILE; create(content, mimeType, name, summary); } /** * Create from a byte array * @param content * @param mimeType */ public Attachment(byte[] content, String mimeType) { this(content, mimeType, null, null); } /** * Create from a byte array with name and summary * @param content * @param mimeType * @param name * @param summary */ public Attachment(byte[] content, String mimeType, String name, String summary) { if (null == content || content.length == 0) { throw new IllegalArgumentException(CONTENT_SHOULD_NOT_BE_EMPTY); } validateMimeType(mimeType); this.mLength = content.length; mSourceType = ContentSourceType.BYTE_ARRAY; mData = content; create(content, mimeType, name, summary); } /** * Create from a {@link InputStream} * @param content * @param mimeType */ public Attachment(InputStream content, String mimeType) { this(content, mimeType, null, null); } /** * Create from a {@link InputStream} with name and summary * @param content * @param mimeType * @param name * @param summary */ public Attachment(InputStream content, String mimeType, String name, String summary) { if (null == content) { throw new IllegalArgumentException(CONTENT_SHOULD_NOT_BE_EMPTY); } validateMimeType(mimeType); mSourceType = ContentSourceType.INPUT_STREAM; create(content, mimeType, name, summary); } /** * Create from a String * @param content * @param mimeType */ public Attachment(String content, String mimeType) { this(content, mimeType, null, null); } /** * Create from a String with name and summary * @param content * @param mimeType * @param name * @param description */ public Attachment(String content, String mimeType, /**String charsetName,**/ String name, String description) { if (StringUtil.isEmpty(content)) { throw new IllegalArgumentException(CONTENT_SHOULD_NOT_BE_EMPTY); } validateMimeType(mimeType); this.mLength = content.length(); mSourceType = ContentSourceType.TEXT; //this.charsetName = charsetName; create(content, mimeType, name, description); } public Attachment(Bitmap content, String mimeType) { if (null == content) { throw new IllegalArgumentException(CONTENT_SHOULD_NOT_BE_EMPTY); } validateMimeType(mimeType); this.mLength = content.getByteCount(); mSourceType = ContentSourceType.BITMAP; create(content, mimeType, null, null); } protected void create(Object content, String mimeType, String name, String description) { this.mContent = content; this.mName = name; this.mSummary = description; this.mMimeType = mimeType; this.mStatus = Status.INIT; this.senderId = User.getCurrentUserId(); } private byte[] getAsBytes() { if (null == mData && null != mContent) { mData = convertToBytes(); if (null != mData) { mLength = mData.length; } } return mData; } /** * The MimeType of the attachment * @return */ public String getMimeType() { return mMimeType; } private Object getContent() { return mContent; } /** * The the id (returned from Max Server) of the attachment * @return */ public String getAttachmentId() { return mAttachmentId; } /** * The URL to download the content directly * @return */ public String getDownloadUrl() { if (checkIfContentAvailable(null)) { return createDownloadUrl(mAttachmentId, getSenderId()); } else { return null; } } /** * Get the status of the attachment * @return */ public Status getStatus() { if (null == mStatus) { mStatus = Status.INIT; } return mStatus; } /** * Get the name of the attachment * @return */ public String getName() { return mName; } /** * Get the summary fo the attachment * @return */ public String getSummary() { return mSummary; } private ContentSourceType getSourceType() { return mSourceType; } private String getCharsetName() { return charsetName; } /** * Get the length of the content. Return -1 if the length it's not available * @return */ public long getLength() { return mLength; } private String getSenderId() { return senderId; } /** * Upload the attachment to Max Server * @param listener */ public void upload(final UploadListener listener) { if (StringUtil.isNotEmpty(mAttachmentId)) { // Already uploaded Log.d(TAG, "Already uploaded"); if (null != listener) { listener.onComplete(this); } return; } if (mStatus == Status.INIT || mStatus == Status.ERROR) { if (null != listener) { listener.onStart(this); } final AtomicReference<Long> startTime = new AtomicReference<>(); Callback<Map<String, String>> uploadCallback = new Callback<Map<String, String>>() { @Override public void onResponse(Response<Map<String, String>> response) { if (response.isSuccess()) { Map<String, String> result = response.body(); if (null != result && !result.isEmpty()) { mAttachmentId = result.values().iterator().next(); mStatus = Status.COMPLETE; Log.d(TAG, "It took " + (System.currentTimeMillis() - startTime.get()) / 1000 + " seconds to upload attachment " + mAttachmentId); if (null != listener) { listener.onComplete(Attachment.this); } } else { handleError(new Exception("Can't get attachmentId from response")); } } else { handleError(new Exception(response.message())); } } @Override public void onFailure(Throwable throwable) { handleError(throwable); } private void handleError(Throwable throwable) { mStatus = Status.ERROR; Log.d(TAG, "Failed to upload attachment " + mName, throwable); if (null != listener) { listener.onError(Attachment.this, throwable); } } }; RequestBody requestBody = null; if (mSourceType == ContentSourceType.FILE) { requestBody = RequestBody.create(MediaType.parse(getMimeType()), (File) mContent); } else { requestBody = RequestBody.create(MediaType.parse(getMimeType()), getAsBytes()); } startTime.set(System.currentTimeMillis()); String partName = StringUtil.isNotEmpty(mName) ? mName : "attachment"; getAttachmentService() .uploadMultiple(mMetaData, new MultipartBuilder().type(MultipartBuilder.FORM) .addFormDataPart(partName, partName, requestBody).build(), uploadCallback) .executeInBackground(); mStatus = Status.TRANSFERING; } else if (mStatus == Status.TRANSFERING) { if (null != listener) { listener.onError(this, new IllegalStateException("Attachment is being uploading")); } } } /** * Download the attachment as byte array * @param listener */ public void download(DownloadAsBytesListener listener) { if (checkIfContentAvailable(listener)) { AbstractDownloader downloader = new BytesDownloader(listener); downloader.download(); } } /** * Download the attachment to a specific file path * @param destinationFilePath * @param listener */ public void download(String destinationFilePath, DownloadAsFileListener listener) { if (StringUtil.isEmpty(destinationFilePath)) { if (null != listener) { listener.onError(new IllegalArgumentException("destinationFilePath shouldn't be null")); } return; } download(new File(destinationFilePath), listener); } /** * Download the attachment to a specific file * @param destinationFile * @param listener */ public void download(File destinationFile, DownloadAsFileListener listener) { if (null == destinationFile) { if (null != listener) { listener.onError(new IllegalArgumentException("destinationFile shouldn't be null")); } return; } if (checkIfContentAvailable(listener)) { AbstractDownloader downloader = new FileDownloader(destinationFile, listener); downloader.download(); } } /** * Download the attachment to a temp file under messageAttachments in app data directory * @param listener */ public void download(DownloadAsFileListener listener) { File dir = getDefaultDownloadDir(); if (null != dir) { File destinationFile = new File(dir, UUID.randomUUID().toString()); download(destinationFile, listener); } else { if (null != listener) { listener.onError(new IllegalStateException("Can't get local dir to download attachment")); } } } /** * Download the attachment as {@link InputStream} * @param listener */ public void download(DownloadAsStreamListener listener) { if (checkIfContentAvailable(listener)) { AbstractDownloader downloader = new StreamDownloader(listener); downloader.download(); } } /** * Added key-value pair meta data for the attachment which will be saved on server * @param key * @param value */ public void addMetaData(String key, String value) { if (StringUtil.isEmpty(key)) { Log.e(TAG, "addMetaData : key shouldn't be null"); return; } if (null == mMetaData) { mMetaData = new HashMap<>(); } mMetaData.put(key, value); } public void setMetaData(Map<String, String> metaData) { if (null != metaData) { metaData.clear(); } this.mMetaData = metaData; } /** * Compares this Attachment object with the specified object and indicates if they * are equal. Following properties are compared : * <p><ul> * <li>attachmentId * <li>name * <li>mimeType * <li>status * <li>length * <li>sourceType * </ul><p> * @param obj * @return */ @Override public boolean equals(Object obj) { if (!EqualityUtil.quickCheck(this, obj)) { return false; } Attachment theOther = (Attachment) obj; return StringUtil.isStringValueEqual(mAttachmentId, theOther.getAttachmentId()) && StringUtil.isStringValueEqual(mName, theOther.getName()) && StringUtil.isStringValueEqual(mMimeType, theOther.getMimeType()) && mStatus == theOther.getStatus() && mLength == theOther.getLength() && mSourceType == theOther.getSourceType(); } /** * Returns an integer hash code for this object. * @see #equals(Object) for the properties used for hash calculation. * @return */ @Override public int hashCode() { return new HashCodeBuilder().hash(mAttachmentId).hash(mName).hash(mMimeType).hash(mStatus).hash(mLength) .hash(mSourceType).hashCode(); } @Override public String toString() { return new StringBuilder().append("{").append("attachmentId = ").append(mAttachmentId).append(", ") .append("name = ").append(mName).append(", ").append("status = ").append(mStatus).append(", ") .append("sourceType = ").append(mSourceType).append(", ").append("mimeType = ").append(mMimeType) .append(", ").append("length = ").append(mLength).append(", ").append("metaData = ") .append(StringUtil.toString(mMetaData)).append("}").toString(); } public static String getMimeType(String fileName, String type) { if (StringUtil.isNotEmpty(fileName)) { int idx = fileName.lastIndexOf("."); if (idx >= 0 && idx < fileName.length() - 1) { String format = fileName.substring(idx + 1); return MimeTypeMap.getSingleton().getMimeTypeFromExtension(format); } } return type + "/*"; } public static String createDownloadUrl(String attachmentId, String ownerId) { if (null == ModuleManager.getUserToken()) { Log.e(TAG, "createDownloadUrl : User token is not available when generating download url. "); return null; } String baseUrl = MaxCore.getConfig().getBaseUrl(); StringBuilder urlBuilder = new StringBuilder(baseUrl); if (!baseUrl.endsWith("/")) { urlBuilder.append("/"); } urlBuilder.append("com.magnet.server/file/download/").append(attachmentId).append("?access_token=") .append(ModuleManager.getUserToken().getAccessToken()).append("&").append("user_id=") .append(ownerId); return urlBuilder.toString(); } protected AttachmentService getAttachmentService() { return MaxCore.create(AttachmentService.class); } protected byte[] convertToBytes() { if (mSourceType == ContentSourceType.TEXT) { if (null != charsetName) { try { return ((String) mContent).getBytes(charsetName); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } } else { return ((String) mContent).getBytes(); } } else if (mSourceType == ContentSourceType.INPUT_STREAM) { return convertInputStreamToBytes((InputStream) mContent); } else if (mSourceType == ContentSourceType.BITMAP) { Bitmap bitmap = (Bitmap) mContent; ByteArrayOutputStream stream = new ByteArrayOutputStream(); Bitmap.CompressFormat compressFormat = Bitmap.CompressFormat.PNG; String imageType = mMimeType.substring(mMimeType.lastIndexOf("/") + 1); if ("png".equalsIgnoreCase(imageType)) { compressFormat = Bitmap.CompressFormat.PNG; } else if ("webp".equalsIgnoreCase(imageType)) { compressFormat = Bitmap.CompressFormat.WEBP; } else if (imageType.toLowerCase().endsWith("jpg")) { compressFormat = Bitmap.CompressFormat.JPEG; } try { bitmap.compress(compressFormat, 100, stream); return stream.toByteArray(); } catch (Exception e) { Log.d(TAG, "Failed to convert bitmap to byte array"); } finally { try { stream.close(); } catch (IOException e) { } } } else if (mSourceType == ContentSourceType.FILE) { InputStream in = null; try { in = new FileInputStream((File) mContent); return convertInputStreamToBytes(new FileInputStream((File) mContent)); } catch (FileNotFoundException e) { Log.e(TAG, e.getLocalizedMessage()); } finally { if (null != in) { try { in.close(); } catch (IOException e) { } } } } return null; } private byte[] convertInputStreamToBytes(InputStream in) { try { ByteArrayOutputStream output = new ByteArrayOutputStream(); byte[] buffer = new byte[DEFAULT_BUFFER_SIZE]; long count = 0; int n = 0; while (-1 != (n = in.read(buffer))) { output.write(buffer, 0, n); count += n; } if (count <= Integer.MAX_VALUE) { return output.toByteArray(); } } catch (IOException e) { Log.d(TAG, "Failed to convert input stream to bytes", e); } return null; } private boolean checkIfContentAvailable(AbstractDownloadListener listener) { String errorMessage = null; if (StringUtil.isEmpty(mAttachmentId)) { errorMessage = "AttachmentId is not available"; } else if (StringUtil.isEmpty(senderId)) { errorMessage = "SenderId shouldn't be null"; } if (null != errorMessage) { Log.e(TAG, "checkIfContentAvailable : " + errorMessage); if (null != listener) { listener.onError(new IllegalStateException(errorMessage)); } return false; } return true; } private static File getDefaultDownloadDir() { if (null == defaultDownloadDir) { Context theContext = MaxCore.getApplicationContext(); if (null != theContext) { defaultDownloadDir = new File(theContext.getFilesDir() + "/messageAttachments/"); if (!defaultDownloadDir.exists()) { defaultDownloadDir.mkdirs(); } } } return defaultDownloadDir; } private void validateMimeType(String mimeType) { if (StringUtil.isEmpty(mimeType)) { throw new IllegalArgumentException("mimeType shouldn't be null"); } } private abstract class AbstractDownloader { protected final AbstractDownloadListener listener; protected final ContentSourceType sourceType; protected long startTime; abstract protected void doDownload(); public AbstractDownloader(AbstractDownloadListener listener, ContentSourceType sourceType) { this.listener = listener; this.sourceType = sourceType; } public void download() { Status currentStatus = getStatus(); startTime = System.currentTimeMillis(); if (currentStatus == Status.COMPLETE) { //TODO : content is not cached right now, always re-download //// Already downloaded //if (null != listener) { // listener.onComplete(this); //} mStatus = Status.INIT; if (null != listener) { listener.onStart(); } doDownload(); mStatus = Status.TRANSFERING; } else if (currentStatus == Status.INIT) { if (null != listener) { listener.onStart(); } doDownload(); mStatus = Status.TRANSFERING; } else if (currentStatus == Status.INLINE) { } else if (currentStatus == Status.TRANSFERING) { if (null != listener) { listener.onError(new IllegalStateException("Attachment is downloading")); } } } protected void logTime() { Log.d(TAG, "It took " + (System.currentTimeMillis() - startTime) / 1000 + " seconds to download attachment " + mAttachmentId); } } private class BytesDownloader extends AbstractDownloader { public BytesDownloader(AbstractDownloadListener listener) { super(listener, ContentSourceType.BYTE_ARRAY); } @Override protected void doDownload() { getAttachmentService().downloadAsBytes(mAttachmentId, senderId, new Callback<byte[]>() { @Override public void onResponse(Response<byte[]> response) { if (response.isSuccess()) { mData = response.body(); mLength = mData.length; mStatus = Status.COMPLETE; logTime(); if (null != listener) { listener.onComplete(mData); } } else { handleError(new Exception(response.message())); } } @Override public void onFailure(Throwable throwable) { handleError(throwable); } private void handleError(Throwable throwable) { mStatus = Status.ERROR; Log.d(TAG, "Failed to download attachment " + mName, throwable); if (null != listener) { listener.onError(throwable); } } }).executeInBackground(); } } private class FileDownloader extends AbstractDownloader { private final File destinationFile; public FileDownloader(File destinationFile, AbstractDownloadListener listener) { super(listener, ContentSourceType.FILE); this.destinationFile = destinationFile; } @Override protected void doDownload() { getAttachmentService().downloadAsStream(mAttachmentId, senderId, new Callback<ResponseBody>() { @Override public void onResponse(final Response<ResponseBody> response) { if (response.isSuccess()) { // Read the InputStream in AsyncTask new AsyncTask<Void, Void, Exception>() { @Override protected Exception doInBackground(Void... params) { try { writeInputStreamToFile(response.body().byteStream(), destinationFile); mStatus = Attachment.Status.COMPLETE; mLength = destinationFile.length(); logTime(); } catch (IOException e) { return e; } return null; } @Override protected void onPostExecute(Exception exception) { if (null == exception) { if (null != listener) { listener.onComplete(destinationFile); } } else { handleError(exception); } } }.execute(); } else { handleError(new Exception(response.message())); } } @Override public void onFailure(Throwable throwable) { handleError(throwable); } private void writeInputStreamToFile(InputStream is, File destinationFile) { OutputStream outputStream = null; try { outputStream = new FileOutputStream(destinationFile); int read = 0; byte[] bytes = new byte[DEFAULT_BUFFER_SIZE]; while ((read = is.read(bytes)) != -1) { outputStream.write(bytes, 0, read); } } catch (IOException e) { e.printStackTrace(); } finally { if (is != null) { try { is.close(); } catch (IOException e) { } } if (outputStream != null) { try { outputStream.close(); } catch (IOException e) { } } } } private void handleError(Throwable throwable) { mStatus = Status.ERROR; Log.d(TAG, "Failed to download attachment " + mName + " to file " + destinationFile.getAbsolutePath(), throwable); if (null != listener) { listener.onError(throwable); } } }).executeInBackground(); } } private class StreamDownloader extends AbstractDownloader { public StreamDownloader(AbstractDownloadListener listener) { super(listener, ContentSourceType.INPUT_STREAM); } @Override protected void doDownload() { getAttachmentService().downloadAsStream(mAttachmentId, senderId, new Callback<ResponseBody>() { @Override public void onResponse(Response<ResponseBody> response) { if (response.isSuccess()) { mStatus = Status.COMPLETE; try { mLength = response.body().contentLength(); logTime(); if (null != listener) { listener.onComplete(response.body().byteStream()); } } catch (IOException e) { handleError(e); } } else { handleError(new Exception(response.message())); } } @Override public void onFailure(Throwable throwable) { handleError(throwable); } private void handleError(Throwable throwable) { mStatus = Status.ERROR; Log.d(TAG, "Failed to download attachment " + mName + " to stream ", throwable); if (null != listener) { listener.onError(throwable); } } }).executeInBackground(); } } //----------------Parcelable Methods---------------- @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeInt(this.mStatus == null ? -1 : this.mStatus.ordinal()); dest.writeInt(this.mSourceType == null ? -1 : this.mSourceType.ordinal()); dest.writeString(this.mName); dest.writeString(this.mSummary); dest.writeString(this.mMimeType); dest.writeLong(this.mLength); dest.writeString(this.charsetName); dest.writeByteArray(this.mData); dest.writeString(this.mAttachmentId); dest.writeString(this.senderId); dest.writeBundle(ParcelableHelper.stringMapToBundle(this.mMetaData)); } protected Attachment(Parcel in) { int tmpStatus = in.readInt(); this.mStatus = tmpStatus == -1 ? null : Status.values()[tmpStatus]; int tmpSourceType = in.readInt(); this.mSourceType = tmpSourceType == -1 ? null : ContentSourceType.values()[tmpSourceType]; this.mName = in.readString(); this.mSummary = in.readString(); this.mMimeType = in.readString(); this.mLength = in.readLong(); this.charsetName = in.readString(); this.mContent = in.readParcelable(Object.class.getClassLoader()); this.mData = in.createByteArray(); this.mAttachmentId = in.readString(); this.senderId = in.readString(); this.mMetaData = ParcelableHelper.stringMapfromBundle(in.readBundle(getClass().getClassLoader())); } protected Attachment() { } public static final Parcelable.Creator<Attachment> CREATOR = new Parcelable.Creator<Attachment>() { public Attachment createFromParcel(Parcel source) { return new Attachment(source); } public Attachment[] newArray(int size) { return new Attachment[size]; } }; }