/** * Copyright 2015-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"). * You may not use this file except in compliance with the License. * A copy of the License is located at * * http://aws.amazon.com/apache2.0 * * or in the "license" file accompanying this file. This file 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.amazonaws.mobileconnectors.s3.transferutility; import static com.amazonaws.services.s3.internal.Constants.MAXIMUM_UPLOAD_PARTS; import static com.amazonaws.services.s3.internal.Constants.MB; import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.database.Cursor; import android.net.Uri; import android.util.Log; import com.amazonaws.AmazonWebServiceRequest; import com.amazonaws.services.s3.AmazonS3; import com.amazonaws.services.s3.model.CannedAccessControlList; import com.amazonaws.services.s3.model.ObjectMetadata; import com.amazonaws.util.VersionInfoUtils; import java.io.File; import java.util.ArrayList; import java.util.List; import java.util.UUID; /** * The transfer utility is a high-level class for applications to upload and * download files. It inserts upload and download records into the database and * starts a Service to execute the tasks in the background. Here is the usage: * * <pre> * // Initializes TransferUtility * TransferUtility transferUtility = new TransferUtility(s3, getApplicationContext()); * // Starts a download * TransferObserver observer = transferUtility.download("bucket_name", "key", file); * observer.setTransferListener(new TransferListener() { * public void onStateChanged(int id, String newState) { * // Do something in the callback. * } * * public void onProgressChanged(int id, long bytesCurrent, long bytesTotal) { * // Do something in the callback. * } * * public void onError(int id, Exception e) { * // Do something in the callback. * } * }); * </pre> * * For pausing and resuming tasks: * * <pre> * // Gets id of the transfer. * int id = observer.getId(); * * // Pauses the transfer. * transferUtility.pause(id); * * // Resumes the transfer. * transferUtility.resume(id); * </pre> * * For cancelling and deleting tasks: * * <pre> * // Cancels the transfer. * transferUtility.cancel(id); * * // Deletes the transfer. * transferUtility.delete(id); * </pre> */ public class TransferUtility { private static final String TAG = "TransferUtility"; /** * Default minimum part size for upload parts. Anything below this will use * a single upload */ static final int MINIMUM_UPLOAD_PART_SIZE = 5 * MB; private final AmazonS3 s3; private final Context appContext; private final TransferDBUtil dbUtil; /** * Constructs a new TransferUtility specifying the client to use and * initializes configuration of TransferUtility and a key for S3 client weak * reference. * * @param s3 The client to use when making requests to Amazon S3 * @param context The current context * @param configuration Configuration parameters for this TransferUtility */ public TransferUtility(AmazonS3 s3, Context context) { this.s3 = s3; this.appContext = context.getApplicationContext(); this.dbUtil = new TransferDBUtil(appContext); } /** * Starts downloading the S3 object specified by the bucket and the key to * the given file. The file must be a valid file. Directory isn't supported. * Note that if the given file exists, it'll be overwritten. * * @param bucket The name of the bucket containing the object to download. * @param key The key under which the object to download is stored. * @param file The file to download the object's data to. * @return A TransferObserver used to track download progress and state */ public TransferObserver download(String bucket, String key, File file) { if (file == null || file.isDirectory()) { throw new IllegalArgumentException("Invalid file: " + file); } final Uri uri = dbUtil.insertSingleTransferRecord(TransferType.DOWNLOAD, bucket, key, file); final int recordId = Integer.parseInt(uri.getLastPathSegment()); if (file.isFile()) { Log.w(TAG, "Overwrite existing file: " + file); file.delete(); } sendIntent(TransferService.INTENT_ACTION_TRANSFER_ADD, recordId); return new TransferObserver(recordId, dbUtil, bucket, key, file); } /** * Starts uploading the file to the given bucket, using the given key. The * file must be a valid file. Directory isn't supported. * * @param bucket The name of the bucket to upload the new object to. * @param key The key in the specified bucket by which to store the new * object. * @param file The file to upload. * @return A TransferObserver used to track upload progress and state */ public TransferObserver upload(String bucket, String key, File file) { return upload(bucket, key, file, new ObjectMetadata()); } /** * Starts uploading the file to the given bucket, using the given key. The * file must be a valid file. Directory isn't supported. * * @param bucket The name of the bucket to upload the new object to. * @param key The key in the specified bucket by which to store the new * object. * @param file The file to upload. * @param cannedAcl The canned ACL to associate with this object * @return A TransferObserver used to track upload progress and state */ public TransferObserver upload(String bucket, String key, File file, CannedAccessControlList cannedAcl) { return upload(bucket, key, file, new ObjectMetadata(), cannedAcl); } /** * Starts uploading the file to the given bucket, using the given key. The * file must be a valid file. Directory isn't supported. * * @param bucket The name of the bucket to upload the new object to. * @param key The key in the specified bucket by which to store the new * object. * @param file The file to upload. * @param metadata The S3 metadata to associate with this object * @return A TransferObserver used to track upload progress and state */ public TransferObserver upload(String bucket, String key, File file, ObjectMetadata metadata) { return upload(bucket, key, file, metadata, null); } /** * Starts uploading the file to the given bucket, using the given key. The * file must be a valid file. Directory isn't supported. * * @param bucket The name of the bucket to upload the new object to. * @param key The key in the specified bucket by which to store the new * object. * @param file The file to upload. * @param metadata The S3 metadata to associate with this object * @param cannedAcl The canned ACL to associate with this object * @return A TransferObserver used to track upload progress and state */ public TransferObserver upload(String bucket, String key, File file, ObjectMetadata metadata, CannedAccessControlList cannedAcl) { if (file == null || file.isDirectory() || !file.exists()) { throw new IllegalArgumentException("Invalid file: " + file); } int recordId = 0; if (shouldUploadInMultipart(file)) { recordId = createMultipartUploadRecords(bucket, key, file, metadata, cannedAcl); } else { final Uri uri = dbUtil.insertSingleTransferRecord(TransferType.UPLOAD, bucket, key, file, metadata, cannedAcl); recordId = Integer.parseInt(uri.getLastPathSegment()); } sendIntent(TransferService.INTENT_ACTION_TRANSFER_ADD, recordId); return new TransferObserver(recordId, dbUtil, bucket, key, file); } /** * Gets a TransferObserver instance to track the record with the given id. * * @param id A transfer id. * @return The TransferObserver instance which is observing the record. */ public TransferObserver getTransferById(int id) { final Cursor c = dbUtil.queryTransferById(id); try { if (c.moveToFirst()) { return new TransferObserver(id, dbUtil, c); } else { return null; } } finally { c.close(); } } /** * Gets a list of TransferObserver instances which are observing records * with the given type. * * @param type The type of the transfer "any". * @return A list of TransferObserver instances. */ public List<TransferObserver> getTransfersWithType(TransferType type) { final List<TransferObserver> transferObservers = new ArrayList<TransferObserver>(); final Cursor c = dbUtil.queryAllTransfersWithType(type); try { while (c.moveToNext()) { final int id = c.getInt(c.getColumnIndexOrThrow(TransferTable.COLUMN_ID)); transferObservers.add(new TransferObserver(id, dbUtil, c)); } } finally { c.close(); } return transferObservers; } /** * Gets a list of TransferObserver instances which are observing records * with the given type. * * @param type The type of the transfer. * @param state The state of the transfer. * @return A list of TransferObserver of transfer records with the given * type and state. */ public List<TransferObserver> getTransfersWithTypeAndState(TransferType type, TransferState state) { final List<TransferObserver> transferObservers = new ArrayList<TransferObserver>(); final Cursor c = dbUtil.queryTransfersWithTypeAndState(type, state); try { while (c.moveToNext()) { final int partNum = c.getInt(c.getColumnIndexOrThrow(TransferTable.COLUMN_PART_NUM)); if (partNum != 0) { // skip parts of a multipart upload continue; } final int id = c.getInt(c.getColumnIndexOrThrow(TransferTable.COLUMN_ID)); transferObservers.add(new TransferObserver(id, dbUtil, c)); } } finally { c.close(); } return transferObservers; } /** * Inserts a multipart summary record and actual part records into database * * @param bucket The name of the bucket to upload the new object to. * @param key The key in the specified bucket by which to store the new * object. * @param file The file to upload. * @param isUsingEncryption Whether the upload is encrypted. * @return Number of records created in database */ private int createMultipartUploadRecords(String bucket, String key, File file, ObjectMetadata metadata, CannedAccessControlList cannedAcl) { long remainingLenth = file.length(); double partSize = (double) remainingLenth / (double) MAXIMUM_UPLOAD_PARTS; partSize = Math.ceil(partSize); final long optimalPartSize = (long) Math.max(partSize, MINIMUM_UPLOAD_PART_SIZE); long fileOffset = 0; int partNumber = 1; // the number of parts final int partCount = (int) Math.ceil((double) remainingLenth / (double) optimalPartSize); /* * the size of valuesArray is partCount + 1, one for a multipart upload * summary, others are actual parts to be uploaded */ final ContentValues[] valuesArray = new ContentValues[partCount + 1]; valuesArray[0] = dbUtil.generateContentValuesForMultiPartUpload(bucket, key, file, fileOffset, 0, "", file.length(), 0, metadata, cannedAcl); for (int i = 1; i < partCount + 1; i++) { final long bytesForPart = Math.min(optimalPartSize, remainingLenth); valuesArray[i] = dbUtil.generateContentValuesForMultiPartUpload(bucket, key, file, fileOffset, partNumber, "", bytesForPart, remainingLenth - optimalPartSize <= 0 ? 1 : 0, metadata, cannedAcl); fileOffset += optimalPartSize; remainingLenth -= optimalPartSize; partNumber++; } return dbUtil.bulkInsertTransferRecords(valuesArray); } /** * Pauses a transfer task with the given id. * * @param id A transfer id specifying the transfer to be paused * @return Whether successfully paused */ public boolean pause(int id) { sendIntent(TransferService.INTENT_ACTION_TRANSFER_PAUSE, id); return true; } /** * Pauses all transfers which have the given type. * * @param type The type of transfers */ public void pauseAllWithType(TransferType type) { final Cursor c = dbUtil.queryAllTransfersWithType(type); try { while (c.moveToNext()) { final int id = c.getInt(c.getColumnIndexOrThrow(TransferTable.COLUMN_ID)); pause(id); } } finally { c.close(); } } /** * Resumes the transfer task with the given id. You can resume a transfer in * paused, canceled or failed state. If a transfer is in waiting or in * progress state but it isn't actually running, this operation will force * it to run. * * @param id A transfer id specifying the transfer to be resumed * @return A TransferObserver of the resumed upload/download or null if the * ID does not represent a paused transfer */ public TransferObserver resume(int id) { sendIntent(TransferService.INTENT_ACTION_TRANSFER_RESUME, id); return getTransferById(id); } /** * Sets a transfer to be canceled. Note the TransferState must be * TransferState.CANCELED before the transfer is guaranteed to have stopped, * and can be safely deleted * * @param id A transfer id specifying the transfer to be canceled * @return Whether the transfer was set to be canceled. */ public boolean cancel(int id) { sendIntent(TransferService.INTENT_ACTION_TRANSFER_CANCEL, id); return true; } /** * Sets all transfers which have the given type to be canceled. Note the * TransferState must be TransferState.CANCELED before the transfer is * guaranteed to have stopped, and can be safely deleted * * @param type The type of transfers */ public void cancelAllWithType(TransferType type) { final Cursor c = dbUtil.queryAllTransfersWithType(type); try { while (c.moveToNext()) { final int id = c.getInt(c.getColumnIndexOrThrow(TransferTable.COLUMN_ID)); cancel(id); } } finally { c.close(); } } /** * Deletes a transfer record with the given id. It just deletes the record * but does not stop the running thread, so you must cancel the task before * deleting the record. * * @param id A transfer id specifying the transfer to be deleted. * @return true if at least one record was deleted */ public boolean deleteTransferRecord(int id) { cancel(id); return dbUtil.deleteTransferRecords(id) > 0; } /** * Send an intent to {@link TransferService} * * @param action action to perform * @param id id of the transfer */ private synchronized void sendIntent(String action, int id) { final String s3Key = UUID.randomUUID().toString(); S3ClientReference.put(s3Key, s3); final Intent intent = new Intent(appContext, TransferService.class); intent.setAction(action); intent.putExtra(TransferService.INTENT_BUNDLE_TRANSFER_ID, id); intent.putExtra(TransferService.INTENT_BUNDLE_S3_REFERENCE_KEY, s3Key); appContext.startService(intent); } private boolean shouldUploadInMultipart(File file) { if (file != null && file.length() > MINIMUM_UPLOAD_PART_SIZE) { return true; } else { return false; } } static <X extends AmazonWebServiceRequest> X appendTransferServiceUserAgentString( final X request) { request.getRequestClientOptions().appendUserAgent("TransferService/" + VersionInfoUtils.getVersion()); return request; } static <X extends AmazonWebServiceRequest> X appendMultipartTransferServiceUserAgentString( final X request) { request.getRequestClientOptions().appendUserAgent("TransferService_multipart/" + VersionInfoUtils.getVersion()); return request; } }