diff --git a/library/src/main/java/com/owncloud/android/lib/common/network/FileRequestEntity.java b/library/src/main/java/com/owncloud/android/lib/common/network/FileRequestEntity.java index 24584b050..44f204b46 100644 --- a/library/src/main/java/com/owncloud/android/lib/common/network/FileRequestEntity.java +++ b/library/src/main/java/com/owncloud/android/lib/common/network/FileRequestEntity.java @@ -16,14 +16,17 @@ import java.io.FileNotFoundException; import java.io.IOException; import java.io.OutputStream; -import java.io.RandomAccessFile; -import java.nio.ByteBuffer; -import java.nio.channels.FileChannel; import java.util.Collection; import java.util.HashSet; import java.util.Iterator; import java.util.Set; +import okio.Throttler; +import okio.Source; +import okio.Sink; +import okio.BufferedSink; +import okio.Okio; + /** * A RequestEntity that represents a File. */ @@ -32,6 +35,7 @@ public class FileRequestEntity implements RequestEntity, ProgressiveDataTransfer private final File file; private final String contentType; private final Set dataTransferListeners = new HashSet<>(); + private final Throttler throttler = new Throttler(); public FileRequestEntity(final File file, final String contentType) { super(); @@ -77,28 +81,45 @@ public void removeDataTransferProgressListener(OnDatatransferProgressListener li dataTransferListeners.remove(listener); } } - - + + /** + * @param limit Maximum upload speed in bytes per second. + * Disabled by default (limit 0). + */ + public void setBandwidthLimit(long limit) { + throttler.bytesPerSecond(limit); + } + @Override public void writeRequest(final OutputStream out) throws IOException { - ByteBuffer tmp = ByteBuffer.allocate(4096); - int readResult; - - RandomAccessFile raf = new RandomAccessFile(file, "r"); - FileChannel channel = raf.getChannel(); + long readResult; Iterator it; long transferred = 0; long size = file.length(); if (size == 0) size = -1; + + Source source = null; + Source bufferSource = null; + Sink sink = null; + Sink throttledSink = null; + BufferedSink bufferedThrottledSink = null; try { - while ((readResult = channel.read(tmp)) >= 0) { + source = Okio.source(file); + bufferSource = Okio.buffer(source); + + sink = Okio.sink(out); + throttledSink = throttler.sink(sink); + bufferedThrottledSink = Okio.buffer(throttledSink); + + while ((readResult = bufferSource.read(bufferedThrottledSink.getBuffer(), 4096)) >= 0) { try { - out.write(tmp.array(), 0, readResult); + bufferedThrottledSink.emitCompleteSegments(); + } catch (IOException io) { // work-around try catch to filter exception in writing throw new WriteException(io); } - tmp.clear(); + transferred += readResult; synchronized (dataTransferListeners) { it = dataTransferListeners.iterator(); @@ -107,6 +128,7 @@ public void writeRequest(final OutputStream out) throws IOException { } } } + bufferedThrottledSink.flush(); } catch (IOException io) { // any read problem will be handled as if the file is not there @@ -123,8 +145,12 @@ public void writeRequest(final OutputStream out) throws IOException { } finally { try { - channel.close(); - raf.close(); + // TODO Which of these are even necessary? (Been a while since I last dealt with buffers) + if (source != null) source.close(); + if (bufferSource != null) bufferSource.close(); + // if (sink != null) sink.close(); + // if (throttledSink != null) throttledSink.close(); + // if (bufferedThrottledSink != null) bufferedThrottledSink.close(); } catch (IOException io) { // ignore failures closing source file } diff --git a/library/src/main/java/com/owncloud/android/lib/resources/files/DownloadFileRemoteOperation.java b/library/src/main/java/com/owncloud/android/lib/resources/files/DownloadFileRemoteOperation.java index b5de6ebf9..55a66a45f 100644 --- a/library/src/main/java/com/owncloud/android/lib/resources/files/DownloadFileRemoteOperation.java +++ b/library/src/main/java/com/owncloud/android/lib/resources/files/DownloadFileRemoteOperation.java @@ -18,9 +18,12 @@ import org.apache.commons.httpclient.HttpStatus; import org.apache.commons.httpclient.methods.GetMethod; -import java.io.BufferedInputStream; +import okio.Throttler; +import okio.Source; +import okio.BufferedSink; +import okio.Okio; + import java.io.File; -import java.io.FileOutputStream; import java.io.IOException; import java.util.Date; import java.util.HashSet; @@ -44,6 +47,7 @@ public class DownloadFileRemoteOperation extends RemoteOperation { private long modificationTimestamp = 0; private String eTag = ""; private GetMethod getMethod; + private final Throttler throttler = new Throttler(); private String remotePath; private String temporalFolderPath; @@ -57,8 +61,16 @@ public DownloadFileRemoteOperation(String remotePath, String temporalFolderPath) this.temporalFolderPath = temporalFolderPath; } - @Override - protected RemoteOperationResult run(OwnCloudClient client) { + /** + * @param limit Maximum download speed in bytes per second. + * Disabled by default (limit 0). + */ + public void setBandwidthLimit(long limit) { + throttler.bytesPerSecond(limit); + } + + @Override + protected RemoteOperationResult run(OwnCloudClient client) { RemoteOperationResult result; /// download will be performed to a temporal file, then moved to the final location @@ -88,7 +100,11 @@ private int downloadFile(OwnCloudClient client, File targetFile) throws IOExcept getMethod = new GetMethod(client.getFilesDavUri(remotePath)); Iterator it; - FileOutputStream fos = null; + // TODO If the upload and download limits should be global then the same throttler can be used for + // all instances of this and the upload class. + Source bufferSource = null; + Source throttledBufferSource = null; + BufferedSink bufferSink = null; try { status = client.executeMethod(getMethod); if (isSuccess(status)) { @@ -98,25 +114,26 @@ private int downloadFile(OwnCloudClient client, File targetFile) throws IOExcept Log_OC.e(TAG, "Error creating file " + targetFile.getAbsolutePath(), ex); throw new CreateLocalFileException(targetFile.getPath(), ex); } - BufferedInputStream bis = new BufferedInputStream(getMethod.getResponseBodyAsStream()); - fos = new FileOutputStream(targetFile); + bufferSource = Okio.source(getMethod.getResponseBodyAsStream()); + throttledBufferSource = throttler.source(bufferSource); + bufferSink = Okio.buffer(Okio.sink(targetFile)); + long transferred = 0; Header contentLength = getMethod.getResponseHeader("Content-Length"); long totalToTransfer = (contentLength != null && - contentLength.getValue().length() > 0) ? - Long.parseLong(contentLength.getValue()) : 0; + contentLength.getValue().length() > 0) ? + Long.parseLong(contentLength.getValue()) : 0; - byte[] bytes = new byte[4096]; - int readResult; - while ((readResult = bis.read(bytes)) != -1) { + long readResult; + while ((readResult = throttledBufferSource.read(bufferSink.getBuffer(), 4096)) != -1) { + bufferSink.emitCompleteSegments(); synchronized (mCancellationRequested) { if (mCancellationRequested.get()) { getMethod.abort(); throw new OperationCancelledException(); } } - fos.write(bytes, 0, readResult); transferred += readResult; synchronized (mDataTransferListeners) { it = mDataTransferListeners.iterator(); @@ -126,6 +143,7 @@ private int downloadFile(OwnCloudClient client, File targetFile) throws IOExcept } } } + bufferSink.flush(); // Check if the file is completed // if transfer-encoding: chunked we cannot check if the file is complete Header transferEncodingHeader = getMethod.getResponseHeader("Transfer-Encoding"); @@ -163,7 +181,11 @@ private int downloadFile(OwnCloudClient client, File targetFile) throws IOExcept } } finally { - if (fos != null) fos.close(); + // TODO Any of these need try statements? Which of these are even necessary? (Been a while since I last dealt with buffers) + if (bufferSource != null) bufferSource.close(); + if (throttledBufferSource != null) throttledBufferSource.close(); + if (bufferSink != null) bufferSink.close(); + if (!savedFile && targetFile.exists()) { targetFile.delete(); } diff --git a/library/src/main/java/com/owncloud/android/lib/resources/files/UploadFileRemoteOperation.java b/library/src/main/java/com/owncloud/android/lib/resources/files/UploadFileRemoteOperation.java index 652e22f1f..8f869f83d 100644 --- a/library/src/main/java/com/owncloud/android/lib/resources/files/UploadFileRemoteOperation.java +++ b/library/src/main/java/com/owncloud/android/lib/resources/files/UploadFileRemoteOperation.java @@ -61,6 +61,7 @@ public class UploadFileRemoteOperation extends RemoteOperation { final Set dataTransferListeners = new HashSet<>(); protected RequestEntity entity = null; + private long bandwidthLimit = 0; @VisibleForTesting public UploadFileRemoteOperation() { @@ -134,6 +135,20 @@ public UploadFileRemoteOperation(String localPath, this.creationTimestamp = creationTimestamp; } + /** + * @param limit Maximum upload speed in bytes per second. + * Disabled by default (limit 0). + */ + public void setBandwidthLimit(long limit) { + bandwidthLimit = limit; + + // If already in progress then set the limit immediately + // Otherwise it will be saved and set when it's run. + if (entity != null) { + ((FileRequestEntity) entity).setBandwidthLimit(limit); + } + } + @Override protected RemoteOperationResult run(OwnCloudClient client) { RemoteOperationResult result; @@ -193,6 +208,7 @@ protected RemoteOperationResult uploadFile(OwnCloudClient client) throws try { File f = new File(localPath); entity = new FileRequestEntity(f, mimeType); + ((FileRequestEntity) entity).setBandwidthLimit(bandwidthLimit); synchronized (dataTransferListeners) { ((ProgressiveDataTransfer) entity) .addDataTransferProgressListeners(dataTransferListeners); diff --git a/sample_client/src/main/java/com/owncloud/android/lib/sampleclient/MainActivity.java b/sample_client/src/main/java/com/owncloud/android/lib/sampleclient/MainActivity.java index 6f2d36709..b1089ef89 100644 --- a/sample_client/src/main/java/com/owncloud/android/lib/sampleclient/MainActivity.java +++ b/sample_client/src/main/java/com/owncloud/android/lib/sampleclient/MainActivity.java @@ -39,9 +39,16 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; +import java.text.SimpleDateFormat; import java.util.ArrayList; +import java.util.Date; import java.util.Iterator; import java.util.List; +import java.util.Locale; +import java.util.Random; + +import okio.BufferedSink; +import okio.Okio; public class MainActivity extends Activity implements OnRemoteOperationListener, OnDatatransferProgressListener { @@ -130,6 +137,9 @@ public void onClickHandler(View button) { case R.id.button_delete_local: startLocalDeletion(); break; + case R.id.button_speed_test: + performSpeedTest(); + break; default: Toast.makeText(this, R.string.youre_doing_it_wrong, Toast.LENGTH_SHORT).show(); } @@ -179,6 +189,183 @@ private void startDownload() { downloadOperation.execute(mClient, this, mHandler); } + private void performSpeedTest() { + + // Size in MB of file to create, upload, then download + int sizeInMB = 100; + + // Delay to wait after upload test to allow server to process + // 200ms per MB seems to work well + int delayInMs = (200 * sizeInMB) + 1000; + + // Limits in bytes per second (0=off) + long uploadLimit = 5 * 1000 * 1000; + long downloadLimit = 3 * 1000 * 1000; + + + // Results + // 100MB randomly generated file + // Using ethernet connection on S10+ + + // Original before modifications + + // Upload : Download (kBps) + // 36273 29416 + // 45511 30649 + // 46929 31673 + + // Avg + // 42904 30579 + + + // Post modifications + + // Upload : Download (kBps) + // 41915 27743 + // 45430 29066 + // 46800 33160 + + // Avg + // 44715 29989 + + + // Create local file with random bytes to upload to the server + String date = new SimpleDateFormat("yyyy-MM-dd-HH:mm:ss", Locale.ENGLISH).format(new Date()); + final File file = new File(getCacheDir(), "speed_test_" + date + ".txt"); + + // Fill file with random bytes to limit + BufferedSink sink = null; + Random random = new Random(); + byte[] b = new byte[1000]; + try { + // Open sink + sink = Okio.buffer(Okio.sink(file)); + int targetSizeInKB = sizeInMB * 1000; + + // Write bytes to file + for(int i=0; i < targetSizeInKB; i++) { + random.nextBytes(b); + sink.write(b); + } + + } catch (IOException e) { + e.printStackTrace(); + + } finally { + if (sink != null) { + try { + sink.flush(); + sink.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + // Prepare to upload file to server + String remotePath = FileUtils.PATH_SEPARATOR + file.getName(); + String mimeType = "application/octet-stream"; + // Get the last modification date of the file from the file system + long timeStamp = file.lastModified() / 1000; + UploadFileRemoteOperation uploadOperation = new UploadFileRemoteOperation( + file.getAbsolutePath(), + remotePath, + mimeType, + timeStamp + ); + + // Set the limit + uploadOperation.setBandwidthLimit(uploadLimit); + + // Start the clock + long start = System.currentTimeMillis(); + + // Use custom listener to update the values in the UI and run the download test after + uploadOperation.addDataTransferProgressListener(new OnDatatransferProgressListener() { + @Override + public void onTransferProgress(long progressRate, long totalTransferredSoFar, long totalToTransfer, String fileAbsoluteName) { + + final long percentage = (totalToTransfer > 0 ? totalTransferredSoFar * 100 / totalToTransfer : 0); + final long elapsedTime = System.currentTimeMillis() - start; + final long estimatedSpeed = totalTransferredSoFar / Math.max(elapsedTime, 1); + Log.d(TAG, "Upload percentage: " + percentage); + + mHandler.post(() -> { + TextView uploadPercent = findViewById(R.id.text_upload_completion); + TextView uploadSpeed = findViewById(R.id.text_upload_speed); + TextView uploadElapsed = findViewById(R.id.text_upload_elapsed); + + uploadPercent.setText(percentage + " %"); + uploadSpeed.setText(estimatedSpeed + " kBps"); + uploadElapsed.setText(elapsedTime + " ms"); + + if (percentage == 100) { + + Log.i(TAG, "Will run download after delay to allow server to process."); + + // Then continue to the download test + Handler handler = new Handler(); + final Runnable r = () -> runDownloadTest(file, downloadLimit); + handler.postDelayed(r, delayInMs); + + Log.i(TAG, "Upload done!"); + } + }); + } + }); + + // Execute the upload! + uploadOperation.execute(mClient, this, mHandler); + + } + + private void runDownloadTest(File file, long downloadLimit) { + + try { + + // Download remote file + Log.i(TAG, "Starting download!"); + + // Setup for download + File downFolder = new File(getCacheDir(), getString(R.string.download_folder_path)); + downFolder.mkdir(); + String remotePath = FileUtils.PATH_SEPARATOR + file.getName(); + DownloadFileRemoteOperation downloadOperation = new DownloadFileRemoteOperation(remotePath, + downFolder.getAbsolutePath()); + downloadOperation.setBandwidthLimit(downloadLimit); + + // Start the clock + final long start = System.currentTimeMillis(); + + // Add the listening code to update the UI + downloadOperation.addDatatransferProgressListener(new OnDatatransferProgressListener() { + @Override + public void onTransferProgress(long progressRate, long totalTransferredSoFar, long totalToTransfer, String fileAbsoluteName) { + final long percentage = (totalToTransfer > 0 ? totalTransferredSoFar * 100 / totalToTransfer : 0); + final long elapsedTime = System.currentTimeMillis() - start; + final long estimatedSpeed = totalTransferredSoFar / Math.max(elapsedTime, 1); + Log.d(TAG, "Download percentage: " + percentage); + + mHandler.post(() -> { + TextView downloadPercent = findViewById(R.id.text_download_completion); + TextView downloadSpeed = findViewById(R.id.text_download_speed); + TextView downloadElapsed = findViewById(R.id.text_download_elapsed); + + downloadPercent.setText(percentage + " %"); + downloadSpeed.setText(estimatedSpeed + " kBps"); + downloadElapsed.setText(elapsedTime + " ms"); + }); + } + }); + + // Run the download + downloadOperation.execute(mClient, this, mHandler); + + } catch (Exception e) { + e.printStackTrace(); + } + } + @SuppressWarnings("deprecation") private void startLocalDeletion() { File downFolder = new File(getCacheDir(), getString(R.string.download_folder_path)); diff --git a/sample_client/src/main/res/layout/main.xml b/sample_client/src/main/res/layout/main.xml index 35c4651f8..4e5aa3b41 100644 --- a/sample_client/src/main/res/layout/main.xml +++ b/sample_client/src/main/res/layout/main.xml @@ -7,6 +7,8 @@ ~ SPDX-License-Identifier: MIT --> @@ -60,7 +62,130 @@ android:layout_height="@dimen/frame_height" android:layout_alignParentLeft="true" android:layout_alignParentRight="true" - android:layout_above="@+id/button_download"> + android:layout_above="@+id/button_download"> + + + + + +