Android: 在 Cloudinary 中上传照片,并在 HttpURLConnection 中进行进度回调

Android: Uploading a photo in Cloudinary with progress callback in HttpURLConnection

我正在尝试修改cloudinary的开源库,以便可以监听我上传照片的进度。库 class 包含我修改的 MultipartUtility java class 以监听上传进度。

修改前的原始代码可以在github上找到:https://github.com/cloudinary/cloudinary_java/blob/master/cloudinary-android/src/main/java/com/cloudinary/android/MultipartUtility.java

我从字面上修改了它以类似于另一个支持上传文件/图像等的进度的云服务 CloudFS 的代码:

https://github.com/bitcasa/CloudFS-Android/blob/master/app/src/main/java/com/bitcasa/cloudfs/api/MultipartUpload.java

package com.cloudinary.android;

import com.cloudinary.Cloudinary;

import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Map;

/**
 * This utility class provides an abstraction layer for sending multipart HTTP
 * POST requests to a web server.
 *
 * @author www.codejava.net
 * @author Cloudinary
 */
public class MultipartUtility {
    private final String boundary;
    private static final String LINE_FEED = "\r\n";
    private static final String APPLICATION_OCTET_STREAM = "application/octet-stream";
    private HttpURLConnection httpConn;
    private String charset;
    private OutputStream outputStream;
    private PrintWriter writer;
    UploadingCallback uploadingCallback;
    public final static String USER_AGENT = "CloudinaryAndroid/" + Cloudinary.VERSION;
    Long filesize;

    public void setUploadingCallback(UploadingCallback uploadingCallback) {
        this.uploadingCallback = uploadingCallback;
    }

    /**
     * This constructor initializes a new HTTP POST request with content type is
     * set to multipart/form-data
     *
     * @param requestURL
     * @param charset
     * @throws IOException
     */
    public MultipartUtility(String requestURL, String charset, String boundary, Map<String, String> headers, Long filesize) throws IOException {
        this.charset = charset;
        this.boundary = boundary;
        this.filesize = filesize;
        URL url = new URL(requestURL);
        httpConn = (HttpURLConnection) url.openConnection();
        httpConn.setDoOutput(true); // indicates POST method
        httpConn.setDoInput(true);
        httpConn.setFixedLengthStreamingMode(filesize); //added this in

        if (headers != null) {
            for (Map.Entry<String, String> header : headers.entrySet()) {
                httpConn.setRequestProperty(header.getKey(), header.getValue());
            }
        }
        httpConn.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + boundary);
        httpConn.setRequestProperty("User-Agent", USER_AGENT);
        outputStream = httpConn.getOutputStream();
        writer = new PrintWriter(new OutputStreamWriter(outputStream, charset), true);
    }

    public MultipartUtility(String requestURL, String charset, String boundary) throws IOException {
        this(requestURL, charset, boundary, null, 0L);
    }

    /**
     * Adds a form field to the request
     *
     * @param name  field name
     * @param value field value
     */
    public void addFormField(String name, String value) {
        writer.append("--" + boundary).append(LINE_FEED);
        writer.append("Content-Disposition: form-data; name=\"" + name + "\"").append(LINE_FEED);
        writer.append("Content-Type: text/plain; charset=" + charset).append(LINE_FEED);
        writer.append(LINE_FEED);
        writer.append(value).append(LINE_FEED);
        writer.flush();
    }

    /**
     * Adds a upload file section to the request
     *
     * @param fieldName  name attribute in {@code <input type="file" name="..." />}
     * @param uploadFile a File to be uploaded
     * @throws IOException
     */
    public void addFilePart(String fieldName, File uploadFile, String fileName) throws IOException {
        if (fileName == null) fileName = uploadFile.getName();
        FileInputStream inputStream = new FileInputStream(uploadFile);
        addFilePart(fieldName, inputStream, fileName);
    }

    public void addFilePart(String fieldName, File uploadFile) throws IOException {
        addFilePart(fieldName, uploadFile, "file");
    }

    public void addFilePart(String fieldName, InputStream inputStream, String fileName) throws IOException {
        if (fileName == null) fileName = "file";
        writer.append("--" + boundary).append(LINE_FEED);
        writer.append("Content-Disposition: form-data; name=\"" + fieldName + "\"; filename=\"" + fileName + "\"").append(LINE_FEED);
        writer.append("Content-Type: ").append(APPLICATION_OCTET_STREAM).append(LINE_FEED);
        writer.append("Content-Transfer-Encoding: binary").append(LINE_FEED);
        writer.append(LINE_FEED);
        writer.flush();

        int progress = 0;
        byte[] buffer = new byte[4096];
        int bytesRead = -1;

        while ((bytesRead = inputStream.read(buffer)) != -1) {
            outputStream.write(buffer, 0, bytesRead);
            progress += bytesRead;
/*            int percentage = ((progress / filesize.intValue()) * 100);*/
            if (uploadingCallback != null) {
                uploadingCallback.uploadListener(progress);
            }

        }
        outputStream.flush();
        writer.flush();
        uploadingCallback = null;
        inputStream.close();
        writer.append(LINE_FEED);
        writer.flush();
    }

    public void addFilePart(String fieldName, InputStream inputStream) throws IOException {
        addFilePart(fieldName, inputStream, "file");
    }

    /**
     * Completes the request and receives response from the server.
     *
     * @return a list of Strings as response in case the server returned status
     * OK, otherwise an exception is thrown.
     * @throws IOException
     */
    public HttpURLConnection execute() throws IOException {
        writer.append("--" + boundary + "--").append(LINE_FEED);
        writer.close();

        return httpConn;
    }

}

我所做的更改是按照此线程的建议将以下内容添加到 httpURLConnection:How to implement file upload progress bar in androidhttpConn.setFixedLengthStreamingMode(filesize);

然后我创建了一个简单的界面来监听上传进度:

public interface UploadingCallback {

    void uploadListener(int progress);

}

然后我在 HttpURLConnection 写入照片时附加了它:

        while ((bytesRead = inputStream.read(buffer)) != -1) {
            outputStream.write(buffer, 0, bytesRead);
            progress += bytesRead;
/*            int percentage = ((progress / filesize.intValue()) * 100);*/
            if (uploadingCallback != null) {
                uploadingCallback.uploadListener(progress);
            }

        }

代码运行但是上传的进度好像没有正确测量。照片大约 365kb,上传大约需要十分之一秒(我在 17:56:55.481 开始上传,到 17:56:55.554 就完成了,也就是 0.7 多秒)。我不相信我的互联网连接有那么快,预计至少需要 5 秒。我有一种感觉,它正在测量将照片写入缓冲区所花费的时间,而不是将其发送到 cloudinary 的服务器所花费的时间。

如何让它测量上传照片所需的时间,以便我可以将数据用于我的进度条?

04-24 17:56:55.481 28306-28725/com.a  upload  4096
04-24 17:56:55.486 28306-28725/com.a  upload  8192
04-24 17:56:55.486 28306-28725/com.a  upload  12288
04-24 17:56:55.486 28306-28725/com.a  upload  16384
04-24 17:56:55.487 28306-28725/com.a  upload  20480
04-24 17:56:55.487 28306-28725/com.a  upload  24576
04-24 17:56:55.487 28306-28725/com.a  upload  28672
04-24 17:56:55.487 28306-28725/com.a  upload  32768
04-24 17:56:55.491 28306-28725/com.a  upload  36864
04-24 17:56:55.492 28306-28725/com.a  upload  40960
04-24 17:56:55.493 28306-28725/com.a  upload  45056
04-24 17:56:55.493 28306-28725/com.a  upload  49152
04-24 17:56:55.493 28306-28725/com.a  upload  53248
04-24 17:56:55.493 28306-28725/com.a  upload  57344
04-24 17:56:55.494 28306-28725/com.a  upload  61440
04-24 17:56:55.494 28306-28725/com.a  upload  65536
04-24 17:56:55.494 28306-28725/com.a  upload  69632
04-24 17:56:55.494 28306-28725/com.a  upload  73728
04-24 17:56:55.494 28306-28725/com.a  upload  77824
04-24 17:56:55.495 28306-28725/com.a  upload  81920
04-24 17:56:55.495 28306-28725/com.a  upload  86016
04-24 17:56:55.495 28306-28725/com.a  upload  90112
04-24 17:56:55.495 28306-28725/com.a  upload  94208
04-24 17:56:55.495 28306-28725/com.a  upload  98304
04-24 17:56:55.495 28306-28725/com.a  upload  102400
04-24 17:56:55.495 28306-28725/com.a  upload  106496
04-24 17:56:55.496 28306-28725/com.a  upload  110592
04-24 17:56:55.496 28306-28725/com.a  upload  114688
04-24 17:56:55.496 28306-28725/com.a  upload  118784
04-24 17:56:55.497 28306-28725/com.a  upload  122880
04-24 17:56:55.498 28306-28725/com.a  upload  126976
04-24 17:56:55.498 28306-28725/com.a  upload  131072
04-24 17:56:55.498 28306-28725/com.a  upload  135168
04-24 17:56:55.498 28306-28725/com.a  upload  139264
04-24 17:56:55.499 28306-28725/com.a  upload  143360
04-24 17:56:55.506 28306-28725/com.a  upload  147456
04-24 17:56:55.510 28306-28725/com.a  upload  151552
04-24 17:56:55.510 28306-28725/com.a  upload  155648
04-24 17:56:55.514 28306-28725/com.a  upload  159744
04-24 17:56:55.515 28306-28725/com.a  upload  163840
04-24 17:56:55.517 28306-28725/com.a  upload  167936
04-24 17:56:55.517 28306-28725/com.a  upload  172032
04-24 17:56:55.518 28306-28725/com.a  upload  176128
04-24 17:56:55.518 28306-28725/com.a  upload  180224
04-24 17:56:55.518 28306-28725/com.a  upload  184320
04-24 17:56:55.519 28306-28725/com.a  upload  188416
04-24 17:56:55.519 28306-28725/com.a  upload  192512
04-24 17:56:55.519 28306-28725/com.a  upload  196608
04-24 17:56:55.519 28306-28725/com.a  upload  200704
04-24 17:56:55.520 28306-28725/com.a  upload  204800
04-24 17:56:55.525 28306-28725/com.a  upload  208896
04-24 17:56:55.526 28306-28725/com.a  upload  212992
04-24 17:56:55.527 28306-28725/com.a  upload  217088
04-24 17:56:55.530 28306-28725/com.a  upload  221184
04-24 17:56:55.530 28306-28725/com.a  upload  225280
04-24 17:56:55.530 28306-28725/com.a  upload  229376
04-24 17:56:55.530 28306-28725/com.a  upload  233472
04-24 17:56:55.530 28306-28725/com.a  upload  237568
04-24 17:56:55.531 28306-28725/com.a  upload  241664
04-24 17:56:55.532 28306-28725/com.a  upload  245760
04-24 17:56:55.532 28306-28725/com.a  upload  249856
04-24 17:56:55.532 28306-28725/com.a  upload  253952
04-24 17:56:55.533 28306-28725/com.a  upload  258048
04-24 17:56:55.533 28306-28725/com.a  upload  262144
04-24 17:56:55.535 28306-28725/com.a  upload  266240
04-24 17:56:55.540 28306-28725/com.a  upload  270336
04-24 17:56:55.540 28306-28725/com.a  upload  274432
04-24 17:56:55.541 28306-28725/com.a  upload  278528
04-24 17:56:55.541 28306-28725/com.a  upload  282624
04-24 17:56:55.543 28306-28725/com.a  upload  286720
04-24 17:56:55.545 28306-28725/com.a  upload  290816
04-24 17:56:55.545 28306-28725/com.a  upload  294912
04-24 17:56:55.547 28306-28725/com.a  upload  299008
04-24 17:56:55.547 28306-28725/com.a  upload  303104
04-24 17:56:55.547 28306-28725/com.a  upload  307200
04-24 17:56:55.547 28306-28725/com.a  upload  311296
04-24 17:56:55.547 28306-28725/com.a  upload  315392
04-24 17:56:55.548 28306-28725/com.a  upload  319488
04-24 17:56:55.548 28306-28725/com.a  upload  323584
04-24 17:56:55.548 28306-28725/com.a  upload  327680
04-24 17:56:55.548 28306-28725/com.a  upload  331776
04-24 17:56:55.549 28306-28725/com.a  upload  335872
04-24 17:56:55.549 28306-28725/com.a  upload  339968
04-24 17:56:55.549 28306-28725/com.a  upload  344064
04-24 17:56:55.550 28306-28725/com.a  upload  348160
04-24 17:56:55.550 28306-28725/com.a  upload  352256
04-24 17:56:55.551 28306-28725/com.a  upload  356352
04-24 17:56:55.551 28306-28725/com.a  upload  360448
04-24 17:56:55.552 28306-28725/com.a  upload  364544
04-24 17:56:55.554 28306-28725/com.a  upload  365790

要亲自测试,您需要在 cloudinary 网站上创建一个免费帐户以获得您的 cloudname,这样您就可以将您的 Android SDK 连接到他们的服务以获得未签名的服务直接从 android 直接上传到他们的服务器。

编辑:

这是我尝试过的方法,当上传实际在 7 秒内完成时,它仍然在 0.7 秒内从 0 跳到 100%:

    while ((bytesRead = inputStream.read(buffer)) != -1) {
        outputStream.write(buffer, 0, bytesRead);
        progress += bytesRead;
        Log.d("MultiPart", "file transferred so far: "
                + progress);
        if (uploadingCallback != null) {
            uploadingCallback.uploadListener(progress);
        }
        Log.d("Flushing", "flush the writer");
        outputStream.flush();
        writer.flush();
    }

使用flush()方法和调用update callback()的时间有问题

从你的代码可以看出,每次读取图片的一部分时,都会将其写入输出缓冲区,但这并不意味着它已发送到服务器,可能会被缓冲,然后再写入'n 到服务器。

您有两个选择,要么在每个 outputStream.write() 之后调用 outputStream.flush(),但这会降低上传性能,因为您会失去缓冲的好处。

或者您可以在方法末尾的 outputStream.flush() 之后调用 updateCallback()。因为在 outputStream.flush() 之后你确定数据在服务器上,那个进度就结束了。

有关冲洗的更多信息,请参阅此线程 What is the purpose of flush() in Java streams?

这是在黑暗中拍摄的,因为我没有在 Android 环境中进行测试,但是我建议尝试以下操作。

不要使用固定长度,而是使用 setChunkedStreamingMode

//httpConn.setFixedLengthStreamingMode(filesize);
httpConn.setChunkedStreamingMode(4096); // or whatever size you see fit

这样做 应该 每次发送 4096 字节的数据时触发部分请求被发送并且基本上刷新内部缓冲区。


您也可以尝试在每次写入后手动刷新缓冲区,这可能会减慢文件上传速度,尤其是如果您经常刷新,但它可能会解决您的问题。您可能最终会调整缓冲区大小以找到一个最佳位置。

while ((bytesRead = inputStream.read(buffer)) != -1) {
    outputStream.write(buffer, 0, bytesRead);
    progress += bytesRead;
    /* int percentage = ((progress / filesize.intValue()) * 100);*/
    if (uploadingCallback != null) {
        uploadingCallback.uploadListener(progress);
    }
    // trigger the stream to write its data
    outputStream.flush();
 }

对于这些更改中的任何一个,您可能希望让用户选择设置他们自己的缓冲区大小,而不是传递总文件大小。 EG 将您的构造函数更改为以下内容:

MultipartUtility(String requestURL, String charset, 
                 String boundary, Map<String, String> headers, int chunkSize)