/* * Copyright 2015 OpenMarket Ltd * * 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 org.matrix.androidsdk.db; import android.os.AsyncTask; import android.os.Looper; import org.matrix.androidsdk.util.Log; import org.json.JSONException; import org.json.JSONObject; import org.matrix.androidsdk.listeners.IMXMediaUploadListener; import org.matrix.androidsdk.rest.callback.ApiCallback; import org.matrix.androidsdk.rest.model.ContentResponse; import org.matrix.androidsdk.rest.model.MatrixError; import org.matrix.androidsdk.ssl.CertUtil; import org.matrix.androidsdk.util.ContentManager; import org.matrix.androidsdk.util.JsonUtils; import java.io.DataOutputStream; import java.io.EOFException; import java.io.InputStream; import java.net.HttpURLConnection; import java.net.URL; import java.net.URLEncoder; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.Timer; import java.util.TimerTask; import javax.net.ssl.HttpsURLConnection; /** * Private AsyncTask used to upload files. */ public class MXMediaUploadWorkerTask extends AsyncTask<Void, IMXMediaUploadListener.UploadStats, String> { private static final String LOG_TAG = "MXMediaUploadWorkerTask"; // upload ID -> task private static final HashMap<String, MXMediaUploadWorkerTask> mPendingUploadByUploadId = new HashMap<>(); // progress listener private ArrayList<IMXMediaUploadListener> mUploadListeners = new ArrayList<>(); // the upload stats private IMXMediaUploadListener.UploadStats mUploadStats; // the media mimeType private final String mMimeType; // the media to upload private final InputStream mContentStream; // its unique identifier private final String mUploadId; // store the server response to provide it the listeners private String mResponseFromServer = null; // tells if the current upload has been cancelled. private boolean mIsCancelled = false; /** * Tells if the upload has been completed */ private boolean mIsDone = false; // upload const private static final int UPLOAD_BUFFER_READ_SIZE = 1024 * 32; // dummy ApiCallback uses to be warned when the upload must be declared as "undeliverable". private ApiCallback mApiCallback = new ApiCallback() { @Override public void onSuccess(Object info) { } @Override public void onNetworkError(Exception e) { } @Override public void onMatrixError(MatrixError e) { } @Override public void onUnexpectedError(Exception e) { dispatchResult(mResponseFromServer); } }; // the upload server HTTP response code private int mResponseCode = -1; // the media file name private String mFilename = null; // the content manager private final ContentManager mContentManager; /** * Check if there is a pending download for the url. * @param uploadId The id to check the existence * @return the dedicated BitmapWorkerTask if it exists. */ public static MXMediaUploadWorkerTask getMediaDUploadWorkerTask(String uploadId) { if ((uploadId != null) && mPendingUploadByUploadId.containsKey(uploadId)) { MXMediaUploadWorkerTask task; synchronized(mPendingUploadByUploadId) { task = mPendingUploadByUploadId.get(uploadId); } return task; } else { return null; } } /** * Cancel the pending uploads. */ public static void cancelPendingUploads() { Collection<MXMediaUploadWorkerTask> tasks = mPendingUploadByUploadId.values(); // cancels the running task for(MXMediaUploadWorkerTask task : tasks) { try { task.cancelUpload(); task.cancel(true); } catch (Exception e) { Log.e(LOG_TAG, "cancelPendingUploads " + e.getLocalizedMessage()); } } mPendingUploadByUploadId.clear(); } /** * Constructor * @param contentManager the content manager * @param contentStream the stream to upload * @param mimeType the mime type * @param uploadId the upload id * @param filename the dest filename * @param listener the upload listener */ public MXMediaUploadWorkerTask(ContentManager contentManager, InputStream contentStream, String mimeType, String uploadId, String filename, IMXMediaUploadListener listener) { try { contentStream.reset(); } catch (Exception e) { Log.e(LOG_TAG, "MXMediaUploadWorkerTask " + e.getLocalizedMessage()); } if ((null != listener) && (mUploadListeners.indexOf(listener) < 0)) { mUploadListeners.add(listener); } mMimeType = mimeType; mContentStream = contentStream; mUploadId = uploadId; mFilename = filename; mContentManager = contentManager; if (null != uploadId) { mPendingUploadByUploadId.put(uploadId, this); } } /** * Add an upload listener * @param aListener the listener to add. */ public void addListener(IMXMediaUploadListener aListener) { if ((null != aListener) && (mUploadListeners.indexOf(aListener) < 0)) { mUploadListeners.add(aListener); } } /** * @return the upload progress */ public int getProgress() { if (null != mUploadStats) { return mUploadStats.mProgress; } return -1; } /** * @return the upload stats */ public IMXMediaUploadListener.UploadStats getStats() { return mUploadStats; } /** * @return true if the current upload has been cancelled. */ private synchronized boolean isUploadCancelled() { return mIsCancelled; } /** * Cancel the current upload. */ public synchronized void cancelUpload() { mIsCancelled = true; } /** * refresh the progress info */ private void publishProgress(long startUploadTime) { mUploadStats.mElapsedTime = (int)((System.currentTimeMillis() - startUploadTime) / 1000); if (0 != mUploadStats.mFileSize) { // Uploading data is 90% of the job // the other 10s is the end of the connection related actions mUploadStats.mProgress = (int) (((long) mUploadStats.mUploadedSize) * 96 / mUploadStats.mFileSize); } // avoid zero div if (System.currentTimeMillis() != startUploadTime) { mUploadStats.mBitRate = (int)(((long)mUploadStats.mUploadedSize) * 1000 / (System.currentTimeMillis() - startUploadTime) / 1024); } else { mUploadStats.mBitRate = 0; } if (0 != mUploadStats.mBitRate) { mUploadStats.mEstimatedRemainingTime = (mUploadStats.mFileSize - mUploadStats.mUploadedSize) / 1024 / mUploadStats.mBitRate; } else { mUploadStats.mEstimatedRemainingTime = -1; } publishProgress(mUploadStats); } @Override protected String doInBackground(Void... params) { HttpURLConnection conn; DataOutputStream dos; mResponseCode = -1; int bytesRead, bytesAvailable; int totalWritten, totalSize; int bufferSize; byte[] buffer; String serverResponse = null; String urlString = mContentManager.getHsConfig().getHomeserverUri().toString() + ContentManager.URI_PREFIX_CONTENT_API + "/upload?access_token=" + mContentManager.getHsConfig().getCredentials().accessToken; if (null != mFilename) { try { String utf8Filename = URLEncoder.encode(mFilename, "utf-8"); urlString += "&filename=" + utf8Filename; } catch (Exception e) { Log.e(LOG_TAG, "doInBackground " + e.getLocalizedMessage()); } } try { URL url = new URL(urlString); conn = (HttpURLConnection) url.openConnection(); conn.setDoInput(true); conn.setDoOutput(true); conn.setUseCaches(false); conn.setRequestMethod("POST"); if (conn instanceof HttpsURLConnection) { // Add SSL Socket factory. HttpsURLConnection sslConn = (HttpsURLConnection) conn; try { sslConn.setSSLSocketFactory(CertUtil.newPinnedSSLSocketFactory(mContentManager.getHsConfig())); sslConn.setHostnameVerifier(CertUtil.newHostnameVerifier(mContentManager.getHsConfig())); } catch (Exception e) { Log.e(LOG_TAG, "sslConn " + e.getLocalizedMessage()); } } conn.setRequestProperty("Content-Type", mMimeType); conn.setRequestProperty("Content-Length", Integer.toString(mContentStream.available())); // avoid caching data before really sending them. conn.setFixedLengthStreamingMode(mContentStream.available()); conn.connect(); dos = new DataOutputStream(conn.getOutputStream()); // create a buffer of maximum size totalSize = bytesAvailable = mContentStream.available(); totalWritten = 0; bufferSize = Math.min(bytesAvailable, UPLOAD_BUFFER_READ_SIZE); buffer = new byte[bufferSize]; mUploadStats = new IMXMediaUploadListener.UploadStats(); mUploadStats.mUploadId = mUploadId; mUploadStats.mProgress = 0; mUploadStats.mUploadedSize = 0; mUploadStats.mFileSize = totalSize; mUploadStats.mElapsedTime = 0; mUploadStats.mEstimatedRemainingTime = -1; mUploadStats.mBitRate = 0; final long startUploadTime = System.currentTimeMillis(); Log.d(LOG_TAG, "doInBackground : start Upload (" + totalSize + " bytes)"); // read file and write it into form... bytesRead = mContentStream.read(buffer, 0, bufferSize); dispatchOnUploadStart(); final android.os.Handler uiHandler = new android.os.Handler(Looper.getMainLooper()); final Timer refreshTimer = new Timer(); uiHandler.post(new Runnable() { @Override public void run() { refreshTimer.scheduleAtFixedRate(new TimerTask() { @Override public void run() { uiHandler.post(new Runnable() { @Override public void run() { if (!mIsDone) { publishProgress(startUploadTime); } } }); } }, new java.util.Date(), 100); } }); while ((bytesRead > 0) && !isUploadCancelled()) { dos.write(buffer, 0, bytesRead); totalWritten += bytesRead; bytesAvailable = mContentStream.available(); bufferSize = Math.min(bytesAvailable, UPLOAD_BUFFER_READ_SIZE); Log.d(LOG_TAG, "doInBackground : totalWritten " + totalWritten + " / totalSize " + totalSize); mUploadStats.mUploadedSize = totalWritten; bytesRead = mContentStream.read(buffer, 0, bufferSize); } mIsDone = true; uiHandler.post(new Runnable() { @Override public void run() { refreshTimer.cancel(); } }); if (!isUploadCancelled()) { mUploadStats.mProgress = 96; publishProgress(startUploadTime); dos.flush(); mUploadStats.mProgress = 97; publishProgress(startUploadTime); dos.close(); mUploadStats.mProgress = 98; publishProgress(startUploadTime); try { // Read the SERVER RESPONSE mResponseCode = conn.getResponseCode(); } catch (EOFException eofEx) { mResponseCode = HttpURLConnection.HTTP_INTERNAL_ERROR; } mUploadStats.mProgress = 99; publishProgress(startUploadTime); Log.d(LOG_TAG, "doInBackground : Upload is done with response code " + mResponseCode); InputStream is; if (mResponseCode == HttpURLConnection.HTTP_OK) { is = conn.getInputStream(); } else { is = conn.getErrorStream(); } int ch; StringBuffer b = new StringBuffer(); while ((ch = is.read()) != -1) { b.append((char) ch); } serverResponse = b.toString(); is.close(); // the server should provide an error description if (mResponseCode != HttpURLConnection.HTTP_OK) { try { JSONObject responseJSON = new JSONObject(serverResponse); serverResponse = responseJSON.getString("error"); } catch (JSONException e) { Log.e(LOG_TAG, "doInBackground : Error parsing " + e.getLocalizedMessage()); } } } else { dos.flush(); dos.close(); } if (null != conn) { conn.disconnect(); } } catch (Exception e) { serverResponse = e.getLocalizedMessage(); Log.e(LOG_TAG, "doInBackground ; failed with error " + e.getClass() + " - " + e.getMessage()); } mResponseFromServer = serverResponse; return serverResponse; } @Override protected void onProgressUpdate(IMXMediaUploadListener.UploadStats ... progress) { super.onProgressUpdate(progress); Log.d(LOG_TAG, "Upload " + this + " : " + mUploadStats.mProgress); dispatchOnUploadProgress(mUploadStats); } /** * Dispatch the result to the callbacks * @param serverResponse the server response */ private void dispatchResult(final String serverResponse) { if (null != mUploadId) { mPendingUploadByUploadId.remove(mUploadId); } mContentManager.getUnsentEventsManager().onEventSent(mApiCallback); // close the source stream try { mContentStream.close(); } catch (Exception e) { Log.e(LOG_TAG, "dispatchResult " + e.getLocalizedMessage()); } if (isUploadCancelled()) { dispatchOnUploadCancel(); } else { ContentResponse uploadResponse = ((mResponseCode != 200) || (serverResponse == null)) ? null : JsonUtils.toContentResponse(serverResponse); if ((null == uploadResponse) || (null == uploadResponse.contentUri)) { dispatchOnUploadError(mResponseCode, serverResponse); } else { dispatchOnUploadComplete(uploadResponse.contentUri); } } } @Override protected void onPostExecute(final String serverResponseMessage) { // do not call the callback if cancelled. if (!isCancelled()) { dispatchResult(serverResponseMessage); } } //============================================================================================================== // Dispatchers //============================================================================================================== /** * Dispatch Upload start */ private void dispatchOnUploadStart() { for(IMXMediaUploadListener listener : mUploadListeners) { try { listener.onUploadStart(mUploadId); } catch (Exception e) { Log.e(LOG_TAG, "dispatchOnUploadStart failed " + e.getLocalizedMessage()); } } } /** * Dispatch Upload start * @param stats the upload stats */ private void dispatchOnUploadProgress(IMXMediaUploadListener.UploadStats stats) { for(IMXMediaUploadListener listener : mUploadListeners) { try { listener.onUploadProgress(mUploadId, stats); } catch (Exception e) { Log.e(LOG_TAG, "dispatchOnUploadProgress failed " + e.getLocalizedMessage()); } } } /** * Dispatch Upload cancel. */ private void dispatchOnUploadCancel() { for(IMXMediaUploadListener listener : mUploadListeners) { try { listener.onUploadCancel(mUploadId); } catch (Exception e) { Log.e(LOG_TAG, "listener failed " + e.getLocalizedMessage()); } } } /** * Dispatch Upload error. * @param serverResponseCode the server response code. * @param serverErrorMessage the server error message */ private void dispatchOnUploadError(int serverResponseCode, String serverErrorMessage) { for(IMXMediaUploadListener listener : mUploadListeners) { try { listener.onUploadError(mUploadId, serverResponseCode, serverErrorMessage); } catch (Exception e) { Log.e(LOG_TAG, "dispatchOnUploadError failed " + e.getLocalizedMessage()); } } } /** * Dispatch Upload complete. * @param contentUri the media uri. */ private void dispatchOnUploadComplete(String contentUri) { for(IMXMediaUploadListener listener : mUploadListeners) { try { listener.onUploadComplete(mUploadId, contentUri); } catch (Exception e) { Log.e(LOG_TAG, "dispatchOnUploadComplete failed " + e.getLocalizedMessage()); } } } }