package com.cloudinary.android; import android.content.Context; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.cloudinary.android.callback.ErrorInfo; import com.cloudinary.android.callback.UploadCallback; import com.cloudinary.android.payload.FilePayload; import com.cloudinary.android.payload.Payload; import com.cloudinary.android.payload.PayloadNotFoundException; import com.cloudinary.android.policy.TimeWindow; import com.cloudinary.android.policy.UploadPolicy; import com.cloudinary.android.preprocess.PayloadDecodeException; import com.cloudinary.android.preprocess.PreprocessChain; import com.cloudinary.android.preprocess.PreprocessException; import com.cloudinary.android.preprocess.ResourceCreationException; import com.cloudinary.utils.ObjectUtils; import java.io.IOException; import java.util.HashMap; import java.util.Map; import java.util.UUID; /** * A request to upload a single {@link Payload} to Cloudinary. Note: Once calling {@link #dispatch()} the request is sealed and any * attempt to modify it will produce an {@link IllegalStateException}. If there's a need to change a request after dispatching, * it needs to be cancelled ({@link MediaManager#cancelRequest(String)}) and a new request should be dispatched in it's place. * * @param <T> The payload type this request will upload */ public class UploadRequest<T extends Payload> { private static final String TAG = UploadRequest.class.getSimpleName(); private final UploadContext<T> uploadContext; private final Object optionsLockObject = new Object(); private PreprocessChain preprocessChain; private String requestId = UUID.randomUUID().toString(); private boolean dispatched = false; private UploadPolicy uploadPolicy = MediaManager.get().getGlobalUploadPolicy(); private TimeWindow timeWindow = TimeWindow.getDefault(); private UploadCallback callback; private Map<String, Object> options; private String optionsAsString = null; private Long maxFileSize; private boolean startNow = false; public UploadRequest(UploadContext<T> uploadContext) { this.uploadContext = uploadContext; } UploadRequest(UploadContext<T> uploadContext, @Nullable Map<String, Object> options) { this.uploadContext = uploadContext; this.options = options; } static String encodeOptions(Map<String, Object> options) throws IOException { return ObjectUtils.serialize(options); } @SuppressWarnings("unchecked") static Map<String, Object> decodeOptions(String encoded) throws IOException, ClassNotFoundException { return (Map<String, Object>) ObjectUtils.deserialize(encoded); } /** * Setup a callback to get notified on upload events. * * @return This request for chaining. */ public synchronized UploadRequest<T> callback(UploadCallback callback) { assertNotDispatched(); this.callback = new DelegateCallback(callback); return this; } /** * Make this an unsigned upload * * @param uploadPreset The name of the upload preset to use, as defined in your cloudinary console * @return This request for chaining. */ public synchronized UploadRequest<T> unsigned(String uploadPreset) { assertNotDispatched(); verifyOptionsExist(); options.put("unsigned", true); options.put("upload_preset", uploadPreset); return this; } /** * Fail the request is the file size is larger than this * * @param bytes Maximum allowed file size to upload * @return This request for chaining */ public synchronized UploadRequest<T> maxFileSize(long bytes) { assertNotDispatched(); this.maxFileSize = bytes; return this; } /** * Add a chain of preprocessing step to run on the resource before uploading * * @param preprocessChain Preprocess chain to run on the file before the upload * @return This request for chaining. */ public synchronized UploadRequest<T> preprocess(PreprocessChain preprocessChain) { assertNotDispatched(); this.preprocessChain = preprocessChain; return this; } /** * Constrain this request to run within a specific {@link TimeWindow}. * * @return This request for chaining. */ public synchronized UploadRequest<T> constrain(TimeWindow timeWindow) { assertNotDispatched(); this.timeWindow = timeWindow; return this; } /** * Set a map of options for this request. Note: This replaces any existing options. * * @return This request for chaining. */ public synchronized UploadRequest<T> options(Map<String, Object> options) { assertNotDispatched(); this.options = options; return this; } /** * Add an option to this request. * * @param name Option name. * @param value Option value. * @return This request for chaining. */ public synchronized UploadRequest<T> option(String name, Object value) { assertNotDispatched(); verifyOptionsExist(); options.put(name, value); return this; } /** * Set the upload uploadPolicy for the request * * @param policy The uploadPolicy to set. See {@link UploadPolicy.Builder} * @return This request for chaining. */ public synchronized UploadRequest<T> policy(UploadPolicy policy) { assertNotDispatched(); this.uploadPolicy = policy; return this; } /** * Dispatch the request * * @return The unique id of the request. */ public synchronized String dispatch() { return dispatch(null); } /** * Start the request immediately, ignoring all other constraints. * * @param context Android context * @return The started request id. */ public synchronized String startNow(@NonNull Context context) { startNow = true; return dispatch(context); } /** * Dispatch the request * * @param context Android context. Needed if using preprocessing. * Otherwise can be null. * @return The unique id of the request. */ public synchronized String dispatch(@Nullable final Context context) { assertNotDispatched(); verifyOptionsExist(); this.dispatched = true; serializeOptions(); MediaManager.get().registerCallback(requestId, callback); final RequestDispatcher dispatcher = uploadContext.getDispatcher(); boolean hasPreprocess = preprocessChain != null && !preprocessChain.isEmpty(); if (!hasPreprocess && maxFileSize == null) { doDispatch(dispatcher, context, UploadRequest.this); } else { if (context == null) { throw new IllegalArgumentException("A valid android context must be supplied to UploadRequest.dispatch() when using preprocessing or setting maxFileSize"); } MediaManager.get().execute(new Runnable() { @Override public void run() { try { final UploadRequest newRequest = preprocessChain != null ? preprocessAndClone(context) : UploadRequest.this; long length = newRequest.getPayload().getLength(context); if (maxFileSize != null && length > maxFileSize) { MediaManager.get().dispatchRequestError(context, requestId, new ErrorInfo(ErrorInfo.PREPROCESS_ERROR, String.format("Payload size is too large, %d, max is %d", length, maxFileSize))); } else { doDispatch(dispatcher, context, newRequest); } } catch (RuntimeException e) { Logger.e(TAG, "Error running preprocess for request", e); MediaManager.get().dispatchRequestError(context, requestId, new ErrorInfo(ErrorInfo.PREPROCESS_ERROR, e.getClass().getSimpleName() + ": " + e.getMessage())); } catch (PreprocessException e) { MediaManager.get().dispatchRequestError(context, requestId, new ErrorInfo(ErrorInfo.PREPROCESS_ERROR, e.getClass().getSimpleName() + ": " + e.getMessage())); } catch (PayloadNotFoundException e) { MediaManager.get().dispatchRequestError(context, requestId, new ErrorInfo(ErrorInfo.PREPROCESS_ERROR, e.getClass().getSimpleName() + ": " + e.getMessage())); } } }); } return requestId; } private void doDispatch(RequestDispatcher dispatcher, @Nullable Context context, UploadRequest<T> uploadRequest) { if (startNow) { if (context == null) { throw new IllegalArgumentException("Context cannot be null when calling startNow()"); } dispatcher.startNow(context, uploadRequest); } else { dispatcher.dispatch(uploadRequest); } } synchronized void serializeOptions() { try { optionsAsString = encodeOptions(options); } catch (IOException e) { throw new InvalidParamsException("Parameters must be serializable", e); } } /** * Run all the preprocessing steps on the request and replicate a new request, with a file payload * containing the processed resource. * * @param context Android context for preprocssing * @return A new request with the preprocessed resource * @throws PayloadNotFoundException * @throws PayloadDecodeException * @throws ResourceCreationException */ private UploadRequest preprocessAndClone(Context context) throws PayloadNotFoundException, PreprocessException { String newFile = preprocessChain.execute(context, getPayload()); UploadRequest<FilePayload> uploadRequest = new UploadRequest<>(new UploadContext<>(new FilePayload(newFile), getUploadContext().getDispatcher())); uploadRequest.uploadPolicy = uploadPolicy; uploadRequest.timeWindow = TimeWindow.getDefault(); uploadRequest.callback = callback; uploadRequest.options = options; uploadRequest.optionsAsString = optionsAsString; uploadRequest.requestId = requestId; uploadRequest.dispatched = dispatched; return uploadRequest; } private void verifyOptionsExist() { if (options == null) { synchronized (optionsLockObject) { if (options == null) { options = new HashMap<>(); } } } } public String getRequestId() { return requestId; } public T getPayload() { return uploadContext.getPayload(); } UploadCallback getCallback() { return callback; } UploadContext<T> getUploadContext() { return uploadContext; } TimeWindow getTimeWindow() { return timeWindow; } private void assertNotDispatched() { if (dispatched) { throw new IllegalStateException("Request already dispatched"); } } private String getOptionsString() { return optionsAsString; } UploadPolicy getUploadPolicy() { return uploadPolicy; } void defferByMinutes(int minutes) { timeWindow = timeWindow.newDeferredWindow(minutes); } void populateParamsFromFields(RequestParams target) { target.putString("uri", getPayload().toUri()); target.putString("requestId", getRequestId()); target.putInt("maxErrorRetries", getUploadPolicy().getMaxErrorRetries()); target.putString("options", getOptionsString()); } /** * Wraps the delegate and unregisters the callback once a request is finished. */ private static final class DelegateCallback implements UploadCallback { private final UploadCallback callback; DelegateCallback(UploadCallback callback) { this.callback = callback; } @Override public void onStart(String requestId) { callback.onStart(requestId); } @Override public void onProgress(String requestId, long bytes, long totalBytes) { callback.onProgress(requestId, bytes, totalBytes); } @Override public void onSuccess(String requestId, Map resultData) { callback.onSuccess(requestId, resultData); MediaManager.get().unregisterCallback(this); } @Override public void onError(String requestId, ErrorInfo error) { callback.onError(requestId, error); MediaManager.get().unregisterCallback(this); } @Override public void onReschedule(String requestId, ErrorInfo error) { callback.onReschedule(requestId, error); } } }