diff --git a/.github/workflows/ci-test.yml b/.github/workflows/ci-test.yml index 2d4042f2f..5fc827f36 100644 --- a/.github/workflows/ci-test.yml +++ b/.github/workflows/ci-test.yml @@ -18,6 +18,8 @@ jobs: uses: reactivecircus/android-emulator-runner@v2 with: api-level: 22 + profile: Nexus 6 + arch: x86_64 script: | ./gradlew connectedCheck ./gradlew build diff --git a/CHANGELOG.md b/CHANGELOG.md index 1173df9ff..4cc012e77 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,11 @@ #Changelog +## 8.3.0(2021-05-12) +* 支持资源通过 Uri 和 InputStream 方式上传 + ## 8.2.1(2021-04-14) * 优化分片上传逻辑,增加重试 ## 8.2.0(2021-02-20) -## 增加 * 增加网络监控功能,选择最优 Host 进行上传 * 优化日志统计 diff --git a/README.md b/README.md index 202bcb715..0f6f87aed 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ https://github.com/qiniudemo/qiniu-lab-android | Qiniu SDK 版本 | 最低 Android版本 | 依赖库版本 | |------------ |-----------------|------------------------| +| 8.3.x | Android 5.0+ | okhttp 4+ | | 8.2.x | Android 5.0+ | okhttp 4+ | | 8.1.x | Android 5.0+ | okhttp 4+ | | 8.0.x | Android 5.0+ | okhttp 4+ | @@ -29,7 +30,7 @@ https://github.com/qiniudemo/qiniu-lab-android | 7.0.7 | Android 2.2+ | android-async-http 1.4.8 | ### 注意 -* 推荐使用最新版:8.2.1 +* 推荐使用最新版:8.3.0 * AndroidNetwork.getMobileDbm()可以获取手机信号强度,需要如下权限(API>=18时生效) ``` diff --git a/library/build.gradle b/library/build.gradle index 4be9ddc90..d108afe2f 100755 --- a/library/build.gradle +++ b/library/build.gradle @@ -24,7 +24,7 @@ android { buildToolsVersion '29.0.2' defaultConfig { //applicationId "com.qiniu.android" - minSdkVersion 15 + minSdkVersion 19 targetSdkVersion 26 versionCode code versionName version @@ -52,7 +52,8 @@ android { dependencies { implementation 'com.squareup.okhttp3:okhttp:4.2.2' - implementation 'com.qiniu:happy-dns:0.2.16' +// implementation 'com.squareup.okhttp3:okhttp:3.12.1' + implementation 'com.qiniu:happy-dns:0.2.18' // for javax.annotation.Nullable use in custom MultipartBody and Headers implements. // implementation 'com.google.code.findbugs:jsr305:3.0.2' implementation 'org.conscrypt:conscrypt-android:2.2.1' diff --git a/library/library.iml b/library/library.iml index cff9603ba..244cd7b6d 100644 --- a/library/library.iml +++ b/library/library.iml @@ -97,7 +97,7 @@ - + diff --git a/library/src/androidTest/java/com/qiniu/android/ComplexUploadSceneTest.java b/library/src/androidTest/java/com/qiniu/android/ComplexUploadSceneTest.java index c9085459f..d97cda069 100644 --- a/library/src/androidTest/java/com/qiniu/android/ComplexUploadSceneTest.java +++ b/library/src/androidTest/java/com/qiniu/android/ComplexUploadSceneTest.java @@ -48,7 +48,9 @@ public void testMutiUploadV1(){ continue; } - uploadFile(file, keyUp, config, null, new UpCompletionHandler() { + UploadInfo info = new UploadInfo<>(file); + info.configWithFile(file); + upload(info, keyUp, config, null, new UpCompletionHandler() { @Override public void complete(String key, ResponseInfo info, JSONObject response) { synchronized (param){ @@ -100,7 +102,9 @@ public void testMutiUploadV2(){ continue; } - uploadFile(file, keyUp, config, null, new UpCompletionHandler() { + UploadInfo info = new UploadInfo<>(file); + info.configWithFile(file); + upload(info, keyUp, config, null, new UpCompletionHandler() { @Override public void complete(String key, ResponseInfo info, JSONObject response) { synchronized (param){ diff --git a/library/src/androidTest/java/com/qiniu/android/ConcurrentResumeUploadTest.java b/library/src/androidTest/java/com/qiniu/android/ConcurrentResumeUploadTest.java index b4aac0667..73edb1a1a 100644 --- a/library/src/androidTest/java/com/qiniu/android/ConcurrentResumeUploadTest.java +++ b/library/src/androidTest/java/com/qiniu/android/ConcurrentResumeUploadTest.java @@ -60,7 +60,7 @@ public void testCancelV1() { String key = "android_concurrent_resume_cancel_v1_" + size + "k"; try { File file = TempFile.createFile(size, key); - cancelTest(cancelPercent, file, key, configuration, null); + cancelTest((long) (size * cancelPercent), file, key, configuration, null); TempFile.remove(file); } catch (IOException e) { e.printStackTrace(); @@ -68,7 +68,7 @@ public void testCancelV1() { } } - public void testHttpV1(){ + public void testHttpV1() { Configuration configuration = new Configuration.Builder() .resumeUploadVersion(Configuration.RESUME_UPLOAD_VERSION_V1) .useConcurrentResumeUpload(true) @@ -87,7 +87,7 @@ public void testHttpV1(){ } } - public void testHttpsV1(){ + public void testHttpsV1() { Configuration configuration = new Configuration.Builder() .resumeUploadVersion(Configuration.RESUME_UPLOAD_VERSION_V1) .useConcurrentResumeUpload(true) @@ -106,19 +106,19 @@ public void testHttpsV1(){ } } - public void testReuploadV1(){ + public void testReuploadV1() { Configuration configuration = new Configuration.Builder() .resumeUploadVersion(Configuration.RESUME_UPLOAD_VERSION_V1) .useConcurrentResumeUpload(true) .useHttps(true) - .chunkSize(1024*1024) + .chunkSize(1024 * 1024) .build(); int[] sizeArray = {30000}; for (int size : sizeArray) { String key = "android_concurrent_resume_reupload_v1_" + size + "k"; try { File file = TempFile.createFile(size, key); - reuploadUploadTest((float)0.7, file, key, configuration, null); + reuploadUploadTest((long) (size * 0.7), file, key, configuration, null); TempFile.remove(file); } catch (IOException e) { e.printStackTrace(); @@ -126,7 +126,7 @@ public void testReuploadV1(){ } } - public void testNoKeyV1(){ + public void testNoKeyV1() { int size = 600; String key = "android_concurrent_resume_no_key_v1_" + size + "k"; File file = null; @@ -153,7 +153,7 @@ public void testNoKeyV1(){ TempFile.remove(file); } - public void test0kV1(){ + public void test0kV1() { int size = 0; String key = "android_concurrent_resume_0k_v1_" + size + "k"; File file = null; @@ -185,7 +185,7 @@ public void testSwitchRegionV2() { Configuration configuration = new Configuration.Builder() .resumeUploadVersion(Configuration.RESUME_UPLOAD_VERSION_V2) .useConcurrentResumeUpload(true) - .chunkSize(4*1024*1024) + .chunkSize(4 * 1024 * 1024) .useHttps(true) .build(); int[] sizeArray = {5000, 8000, 10000, 20000}; @@ -206,7 +206,7 @@ public void testCancelV2() { Configuration configuration = new Configuration.Builder() .resumeUploadVersion(Configuration.RESUME_UPLOAD_VERSION_V2) .useConcurrentResumeUpload(true) - .chunkSize(4*1024*1024) + .chunkSize(4 * 1024 * 1024) .useHttps(true) .build(); int[] sizeArray = {10000, 20000}; @@ -214,7 +214,7 @@ public void testCancelV2() { String key = "android_concurrent_resume_cancel_v2_" + size + "k"; try { File file = TempFile.createFile(size, key); - cancelTest(cancelPercent, file, key, configuration, null); + cancelTest((long) (size * cancelPercent), file, key, configuration, null); TempFile.remove(file); } catch (IOException e) { e.printStackTrace(); @@ -222,14 +222,14 @@ public void testCancelV2() { } } - public void testHttpV2(){ + public void testHttpV2() { Configuration configuration = new Configuration.Builder() .resumeUploadVersion(Configuration.RESUME_UPLOAD_VERSION_V2) .useConcurrentResumeUpload(true) - .chunkSize(4*1024*1024) + .chunkSize(4 * 1024 * 1024) .useHttps(false) .build(); - int[] sizeArray = {500, 1000, 3000, 4000, 5000, 8000, 10000, 20000}; + int[] sizeArray = {500, 2000, 4000, 5000, 8000, 20000}; long timestamp = new Date().getTime(); for (int size : sizeArray) { String key = "android_concurrent_resume_http_v2_" + timestamp + "_" + size + "k"; @@ -243,14 +243,14 @@ public void testHttpV2(){ } } - public void testHttpsV2(){ + public void testHttpsV2() { Configuration configuration = new Configuration.Builder() .resumeUploadVersion(Configuration.RESUME_UPLOAD_VERSION_V2) .useConcurrentResumeUpload(true) .useHttps(true) - .chunkSize(4*1024*1024) + .chunkSize(4 * 1024 * 1024) .build(); - int[] sizeArray = {500, 1000, 3000, 4000, 5000, 8000, 10000, 20000}; + int[] sizeArray = {500, 2000, 4000, 5000, 8000, 20000}; for (int size : sizeArray) { String key = "android_concurrent_resume_https_v2_" + size + "k"; try { @@ -263,19 +263,19 @@ public void testHttpsV2(){ } } - public void testReuploadV2(){ + public void testReuploadV2() { Configuration configuration = new Configuration.Builder() .resumeUploadVersion(Configuration.RESUME_UPLOAD_VERSION_V2) .useConcurrentResumeUpload(true) .useHttps(true) - .chunkSize(4*1024*1024) + .chunkSize(4 * 1024 * 1024) .build(); int[] sizeArray = {30000}; for (int size : sizeArray) { String key = "android_concurrent_resume_reupload_v2_" + size + "k"; try { File file = TempFile.createFile(size, key); - reuploadUploadTest((float)0.7, file, key, configuration, null); + reuploadUploadTest((long) (size * 0.7), file, key, configuration, null); TempFile.remove(file); } catch (IOException e) { e.printStackTrace(); @@ -283,7 +283,7 @@ public void testReuploadV2(){ } } - public void testNoKeyV2(){ + public void testNoKeyV2() { int size = 600; String key = "android_concurrent_resume_no_key_v2_" + size + "k"; File file = null; @@ -310,7 +310,7 @@ public void testNoKeyV2(){ TempFile.remove(file); } - public void test0kV2(){ + public void test0kV2() { int size = 0; String key = "android_concurrent_resume_0k_v2_" + size + "k"; File file = null; diff --git a/library/src/androidTest/java/com/qiniu/android/FormUploadTest.java b/library/src/androidTest/java/com/qiniu/android/FormUploadTest.java index 5d3d01bd2..693d996fc 100644 --- a/library/src/androidTest/java/com/qiniu/android/FormUploadTest.java +++ b/library/src/androidTest/java/com/qiniu/android/FormUploadTest.java @@ -6,6 +6,8 @@ import com.qiniu.android.storage.Configuration; import com.qiniu.android.storage.UploadOptions; +import junit.framework.Assert; + import java.io.File; import java.io.IOException; import java.util.HashMap; @@ -20,9 +22,14 @@ public void testSwitchRegion() { .build(); int[] sizeArray = {5, 50, 200, 500, 800, 1000, 2000, 3000, 4000}; for (int size : sizeArray) { - String key = "android_form_switch_region_" + size + "k"; - byte[] data = TempFile.getByte(size); - switchRegionTestWithData(data, key, configuration, null); + String key = "android_Form_switch_region_" + size + "k"; + File file = null; + try { + file = TempFile.createFile(size, key); + } catch (IOException e) { + Assert.assertTrue(e.getMessage(), false); + } + switchRegionTestWithFile(file, key, configuration, null); } } @@ -35,12 +42,17 @@ public void testCancel() { int[] sizeArray = {2000, 3000, 4000}; for (int size : sizeArray) { String key = "android_form_cancel_" + size + "k"; - byte[] data = TempFile.getByte(size*1024); - cancelTest(cancelPercent, data, key, configuration, null); + File file = null; + try { + file = TempFile.createFile(size, key); + } catch (IOException e) { + Assert.fail(e.getMessage()); + } + cancelTest((long) (size * cancelPercent), file, key, configuration, null); } } - public void testHttpV1() { + public void testHttp() { Configuration configuration = new Configuration.Builder() .resumeUploadVersion(Configuration.RESUME_UPLOAD_VERSION_V1) .useConcurrentResumeUpload(true) @@ -48,13 +60,18 @@ public void testHttpV1() { .build(); int[] sizeArray = {500, 1000, 3000, 4000, 5000, 8000, 10000, 20000}; for (int size : sizeArray) { - String key = "android_form_http_v1_" + size + "k"; - byte[] data = TempFile.getByte(size); - uploadDataAndAssertSuccessResult(data, key, configuration, null); + String key = "android_form_http" + size + "k"; + File file = null; + try { + file = TempFile.createFile(size, key); + } catch (IOException e) { + Assert.assertTrue(e.getMessage(), false); + } + uploadFileAndAssertSuccessResult(file, key, configuration, null); } } - public void testHttpsV1() { + public void testHttps() { Configuration configuration = new Configuration.Builder() .resumeUploadVersion(Configuration.RESUME_UPLOAD_VERSION_V1) .useConcurrentResumeUpload(true) @@ -62,9 +79,14 @@ public void testHttpsV1() { .build(); int[] sizeArray = {500, 1000, 3000, 4000, 5000, 8000, 10000, 20000}; for (int size : sizeArray) { - String key = "android_form_https_v1_" + size + "k"; - byte[] data = TempFile.getByte(size); - uploadDataAndAssertSuccessResult(data, key, configuration, null); + String key = "android_form_https" + size + "k"; + File file = null; + try { + file = TempFile.createFile(size, key); + } catch (IOException e) { + Assert.assertTrue(e.getMessage(), false); + } + uploadFileAndAssertSuccessResult(file, key, configuration, null); } } @@ -75,51 +97,67 @@ public void testSmall() { //mime type final String mimeType = "text/plain"; final UploadOptions options = new UploadOptions(params, mimeType, true, null, null); - byte[] data = "Hello, World!".getBytes(); - uploadDataAndAssertSuccessResult(data, "android_你好", null, options); + + String key = "android_small"; + File file = null; + try { + file = TempFile.createFile(10, key); + } catch (IOException e) { + Assert.assertTrue(e.getMessage(), false); + } + uploadFileAndAssertSuccessResult(file, key, null, options); } public void test100up() { int count = 100; for (int i = 1; i < count; i++) { - String key = "android_form_100_up_" + i + "k"; - byte[] data = TempFile.getByte(i * 1024); - uploadDataAndAssertSuccessResult(data, key, null, null); + String key = "android_form_100_UP_" + i + "k"; + File file = null; + try { + file = TempFile.createFile(10, key); + } catch (IOException e) { + Assert.assertTrue(e.getMessage(), false); + } + uploadFileAndAssertSuccessResult(file, key, null, null); } } public void testUpUnAuth() { - byte[] data = "Hello, World!".getBytes(); - uploadDataAndAssertResult(ResponseInfo.InvalidToken, data, "noAuth", "android_form_no_auth", null, null); + String key = "android_unAuth"; + File file = null; + try { + file = TempFile.createFile(10, key); + } catch (IOException e) { + Assert.assertTrue(e.getMessage(), false); + } + uploadFileAndAssertResult(ResponseInfo.InvalidToken, file, "noAuth", "android_form_no_auth", null, null); } public void testNoData() { - uploadDataAndAssertResult(ResponseInfo.ZeroSizeFile, null, "android_form_no_data", null, null); - } - - public void testNoFile() { - uploadFileAndAssertResult(ResponseInfo.ZeroSizeFile, null, "android_form_no_file", null, null); + String key = "android_noData"; + File file = null; + try { + file = TempFile.createFile(0, key); + } catch (IOException e) { + Assert.assertTrue(e.getMessage(), false); + } + uploadFileAndAssertResult(ResponseInfo.ZeroSizeFile, file, key, null, null); } public void testNoToken() { String key = "android_form_no_token"; File file = null; - byte[] data = null; try { file = TempFile.createFile(5 * 1024, key); - data = TempFile.getByte(5 * 1024); } catch (IOException e) { e.printStackTrace(); } - uploadDataAndAssertResult(ResponseInfo.InvalidToken, data, null, key, null, null); uploadFileAndAssertResult(ResponseInfo.InvalidToken, file, null, key, null, null); - uploadDataAndAssertResult(ResponseInfo.InvalidToken, data, "", key, null, null); uploadFileAndAssertResult(ResponseInfo.InvalidToken, file, "", key, null, null); - uploadDataAndAssertResult(ResponseInfo.InvalidToken, data, "ABC", key, null, null); uploadFileAndAssertResult(ResponseInfo.InvalidToken, file, "ABC", key, null, null); TempFile.remove(file); @@ -128,28 +166,22 @@ public void testNoToken() { public void testNoKey() { String key = "android_form_no_key"; File file = null; - byte[] data = null; try { file = TempFile.createFile(5 * 1024, key); - data = TempFile.getByte(5 * 1024); } catch (IOException e) { e.printStackTrace(); } - uploadDataAndAssertSuccessResult(data, null, null, null); uploadFileAndAssertSuccessResult(file, null, null, null); TempFile.remove(file); } public void testUrlConvert() { - String dataKey = "android_form_url_convert_data_new"; String fileKey = "android_form_url_convert_file_new"; File file = null; - byte[] data = null; try { file = TempFile.createFile(5, fileKey); - data = TempFile.getByte(5 * 1024); } catch (IOException e) { e.printStackTrace(); } @@ -160,11 +192,10 @@ public void testUrlConvert() { .urlConverter(new UrlConverter() { @Override public String convert(String url) { - return url.replace("upnono", "up"); + return url.replace("upnono", "up"); } }) .build(); - uploadDataAndAssertSuccessResult(data, dataKey, configuration, null); uploadFileAndAssertSuccessResult(file, fileKey, configuration, null); } @@ -179,20 +210,15 @@ public void testCustomParam() { metaParam.put("x-qn-meta-aaa", "meta_value_1"); metaParam.put("x-qn-meta-key-2", "meta_value_2"); - UploadOptions options = new UploadOptions(userParam, metaParam, null,true, null, null, null); + UploadOptions options = new UploadOptions(userParam, metaParam, null, true, null, null, null); - String dataKey = "android_form_custom_param_data"; String fileKey = "android_form_custom_param_file"; File file = null; - byte[] data = null; try { file = TempFile.createFile(5, fileKey); - data = TempFile.getByte(5 * 1024); } catch (IOException e) { e.printStackTrace(); } - - uploadDataAndAssertSuccessResult(data, dataKey, null, options); uploadFileAndAssertSuccessResult(file, fileKey, null, options); } diff --git a/library/src/androidTest/java/com/qiniu/android/ResumeUploadTest.java b/library/src/androidTest/java/com/qiniu/android/ResumeUploadTest.java index 0fdf9b146..477d70198 100644 --- a/library/src/androidTest/java/com/qiniu/android/ResumeUploadTest.java +++ b/library/src/androidTest/java/com/qiniu/android/ResumeUploadTest.java @@ -44,7 +44,7 @@ public void testCancelV1() { String key = "android_resume_cancel_v1_" + size + "k"; try { File file = TempFile.createFile(size, key); - cancelTest(cancelPercent, file, key, configuration, null); + cancelTest((long) (size * cancelPercent), file, key, configuration, null); TempFile.remove(file); } catch (IOException e) { e.printStackTrace(); @@ -52,7 +52,7 @@ public void testCancelV1() { } } - public void testHttpV1(){ + public void testHttpV1() { Configuration configuration = new Configuration.Builder() .resumeUploadVersion(Configuration.RESUME_UPLOAD_VERSION_V1) .useConcurrentResumeUpload(false) @@ -71,13 +71,13 @@ public void testHttpV1(){ } } - public void testHttpsV1(){ + public void testHttpsV1() { Configuration configuration = new Configuration.Builder() .resumeUploadVersion(Configuration.RESUME_UPLOAD_VERSION_V1) .useConcurrentResumeUpload(false) .useHttps(true) .build(); - int[] sizeArray = {500, 1000, 3000, 4000, 5000, 8000, 10000, 20000}; + int[] sizeArray = {500, 3000, 4000, 7000, 10000, 20000}; for (int size : sizeArray) { String key = "android_resume_https_v1_" + size + "k"; try { @@ -90,19 +90,19 @@ public void testHttpsV1(){ } } - public void testReuploadV1(){ + public void testReuploadV1() { Configuration configuration = new Configuration.Builder() .resumeUploadVersion(Configuration.RESUME_UPLOAD_VERSION_V1) .useConcurrentResumeUpload(false) .useHttps(true) - .chunkSize(1024*1024) + .chunkSize(1024 * 1024) .build(); int[] sizeArray = {30000}; for (int size : sizeArray) { String key = "android_resume_reupload_v1_" + size + "k"; try { File file = TempFile.createFile(size, key); - reuploadUploadTest((float)0.7, file, key, configuration, null); + reuploadUploadTest((long) (size * 0.7), file, key, configuration, null); TempFile.remove(file); } catch (IOException e) { e.printStackTrace(); @@ -110,7 +110,7 @@ public void testReuploadV1(){ } } - public void testNoKeyV1(){ + public void testNoKeyV1() { int size = 600; String key = "android_resume_no_key_v1_" + size + "k"; File file = null; @@ -137,7 +137,7 @@ public void testNoKeyV1(){ TempFile.remove(file); } - public void test0kV1(){ + public void test0kV1() { int size = 0; String key = "android_resume_0k_v1_" + size + "k"; File file = null; @@ -175,7 +175,7 @@ public void testCustomParamV1() { metaParam.put("x-qn-meta-aaa", "meta_value_1"); metaParam.put("x-qn-meta-key-2", "meta_value_2"); - UploadOptions options = new UploadOptions(userParam, metaParam, null,true, null, null, null); + UploadOptions options = new UploadOptions(userParam, metaParam, null, true, null, null, null); Configuration configuration = new Configuration.Builder() .resumeUploadVersion(Configuration.RESUME_UPLOAD_VERSION_V1) @@ -198,7 +198,7 @@ public void testSwitchRegionV2() { Configuration configuration = new Configuration.Builder() .resumeUploadVersion(Configuration.RESUME_UPLOAD_VERSION_V2) .useConcurrentResumeUpload(false) - .chunkSize(4*1024*1024) + .chunkSize(4 * 1024 * 1024) .useHttps(true) .build(); int[] sizeArray = {5000, 8000, 10000, 20000}; @@ -219,7 +219,7 @@ public void testCancelV2() { Configuration configuration = new Configuration.Builder() .resumeUploadVersion(Configuration.RESUME_UPLOAD_VERSION_V2) .useConcurrentResumeUpload(false) - .chunkSize(4*1024*1024) + .chunkSize(4 * 1024 * 1024) .useHttps(true) .build(); int[] sizeArray = {10000, 20000}; @@ -227,7 +227,7 @@ public void testCancelV2() { String key = "android_resume_cancel_v2_" + size + "k"; try { File file = TempFile.createFile(size, key); - cancelTest(cancelPercent, file, key, configuration, null); + cancelTest((long) (size * cancelPercent), file, key, configuration, null); TempFile.remove(file); } catch (IOException e) { e.printStackTrace(); @@ -235,11 +235,11 @@ public void testCancelV2() { } } - public void testHttpV2(){ + public void testHttpV2() { Configuration configuration = new Configuration.Builder() .resumeUploadVersion(Configuration.RESUME_UPLOAD_VERSION_V2) .useConcurrentResumeUpload(false) - .chunkSize(4*1024*1024) + .chunkSize(4 * 1024 * 1024) .useHttps(false) .build(); int[] sizeArray = {500, 1000, 3000, 4000, 5000, 8000, 10000, 20000}; @@ -255,14 +255,14 @@ public void testHttpV2(){ } } - public void testHttpsV2(){ + public void testHttpsV2() { Configuration configuration = new Configuration.Builder() .resumeUploadVersion(Configuration.RESUME_UPLOAD_VERSION_V2) .useConcurrentResumeUpload(false) - .chunkSize(4*1024*1024) + .chunkSize(4 * 1024 * 1024) .useHttps(true) .build(); - int[] sizeArray = {500, 1000, 3000, 4000, 5000, 8000, 10000, 20000}; + int[] sizeArray = {500, 3000, 4000, 7000, 10000, 20000}; for (int size : sizeArray) { String key = "android_resume_https_v2_" + size + "k"; try { @@ -275,19 +275,19 @@ public void testHttpsV2(){ } } - public void testReuploadV2(){ + public void testReuploadV2() { Configuration configuration = new Configuration.Builder() .resumeUploadVersion(Configuration.RESUME_UPLOAD_VERSION_V2) .useConcurrentResumeUpload(false) .useHttps(true) - .chunkSize(4*1024*1024) + .chunkSize(4 * 1024 * 1024) .build(); int[] sizeArray = {30000}; for (int size : sizeArray) { String key = "android_resume_reupload_v2_" + size + "k"; try { File file = TempFile.createFile(size, key); - reuploadUploadTest((float)0.7, file, key, configuration, null); + reuploadUploadTest((long) (size * 0.7), file, key, configuration, null); TempFile.remove(file); } catch (IOException e) { e.printStackTrace(); @@ -295,7 +295,7 @@ public void testReuploadV2(){ } } - public void testNoKeyV2(){ + public void testNoKeyV2() { int size = 600; String key = "android_resume_reupload_v2_" + size + "k"; File file = null; @@ -322,7 +322,7 @@ public void testNoKeyV2(){ TempFile.remove(file); } - public void test0kV2(){ + public void test0kV2() { int size = 0; String key = "android_resume_0k_v2_" + size + "k"; File file = null; @@ -360,12 +360,12 @@ public void testCustomParamV2() { metaParam.put("x-qn-meta-aaa", "meta_value_1"); metaParam.put("x-qn-meta-key-2", "meta_value_2"); - UploadOptions options = new UploadOptions(userParam, metaParam, null,true, null, null, null); + UploadOptions options = new UploadOptions(userParam, metaParam, null, true, null, null, null); Configuration configuration = new Configuration.Builder() .resumeUploadVersion(Configuration.RESUME_UPLOAD_VERSION_V2) .useConcurrentResumeUpload(false) - .chunkSize(4*1024*1024) + .chunkSize(4 * 1024 * 1024) .useHttps(false) .build(); diff --git a/library/src/androidTest/java/com/qiniu/android/TempFile.java b/library/src/androidTest/java/com/qiniu/android/TempFile.java index bb42b331c..03bf95a79 100644 --- a/library/src/androidTest/java/com/qiniu/android/TempFile.java +++ b/library/src/androidTest/java/com/qiniu/android/TempFile.java @@ -19,6 +19,7 @@ public static void remove(File f) { public static File createFile(int kiloSize) throws IOException { return createFile(kiloSize, "qiniu_" + (1024 * kiloSize) + "k"); } + public static File createFile(int kiloSize, String fileName) throws IOException { FileOutputStream fos = null; try { diff --git a/library/src/androidTest/java/com/qiniu/android/TestConfig.java b/library/src/androidTest/java/com/qiniu/android/TestConfig.java index 1072f6756..9950c6a06 100644 --- a/library/src/androidTest/java/com/qiniu/android/TestConfig.java +++ b/library/src/androidTest/java/com/qiniu/android/TestConfig.java @@ -8,22 +8,25 @@ */ public final class TestConfig { // TODO: 2020-05-09 bad token for testPutBytesWithFixedZoneUseBackupDomains - // 华东上传凭证 +// 华东上传凭证 public static final String bucket_z0 = "kodo-phone-zone0-space"; - public static final String token_z0 = "dxVQk8gyk3WswArbNhdKIwmwibJ9nFsQhMNUmtIM:m1kHxpdaFH3NK120iAkHlSwBpio=:eyJzY29wZSI6ImtvZG8tcGhvbmUtem9uZTAtc3BhY2UiLCJkZWFkbGluZSI6MTYxODgxNDM5MCwgInJldHVybkJvZHkiOiJ7XCJjYWxsYmFja1VybFwiOlwiaHR0cDpcL1wvY2FsbGJhY2suZGV2LnFpbml1LmlvXCIsIFwiZm9vXCI6JCh4OmZvbyksIFwiYmFyXCI6JCh4OmJhciksIFwibWltZVR5cGVcIjokKG1pbWVUeXBlKSwgXCJoYXNoXCI6JChldGFnKSwgXCJrZXlcIjokKGtleSksIFwiZm5hbWVcIjokKGZuYW1lKX0ifQ=="; + public static final String token_z0 = "dxVQk8gyk3WswArbNhdKIwmwibJ9nFsQhMNUmtIM:B6pGDhaMzboJHWnUDi60rrQSB_s=:eyJzY29wZSI6ImtvZG8tcGhvbmUtem9uZTAtc3BhY2UiLCJkZWFkbGluZSI6MTYyNDUwMzMxMywgInJldHVybkJvZHkiOiJ7XCJjYWxsYmFja1VybFwiOlwiaHR0cDpcL1wvY2FsbGJhY2suZGV2LnFpbml1LmlvXCIsIFwiZm9vXCI6JCh4OmZvbyksIFwiYmFyXCI6JCh4OmJhciksIFwibWltZVR5cGVcIjokKG1pbWVUeXBlKSwgXCJoYXNoXCI6JChldGFnKSwgXCJrZXlcIjokKGtleSksIFwiZm5hbWVcIjokKGZuYW1lKX0ifQ=="; // 华北上传凭证 public static final String bucket_z1 = "kodo-phone-zone1-space"; - public static final String token_z1 = "dxVQk8gyk3WswArbNhdKIwmwibJ9nFsQhMNUmtIM:1vkQkb72ANFiAftABJAF2dhbXd0=:eyJzY29wZSI6ImtvZG8tcGhvbmUtem9uZTEtc3BhY2UiLCJkZWFkbGluZSI6MTYxODgxNDM5MCwgInJldHVybkJvZHkiOiJ7XCJjYWxsYmFja1VybFwiOlwiaHR0cDpcL1wvY2FsbGJhY2suZGV2LnFpbml1LmlvXCIsIFwiZm9vXCI6JCh4OmZvbyksIFwiYmFyXCI6JCh4OmJhciksIFwibWltZVR5cGVcIjokKG1pbWVUeXBlKSwgXCJoYXNoXCI6JChldGFnKSwgXCJrZXlcIjokKGtleSksIFwiZm5hbWVcIjokKGZuYW1lKX0ifQ=="; + public static final String token_z1 = "dxVQk8gyk3WswArbNhdKIwmwibJ9nFsQhMNUmtIM:Oxn6g3rMUNXBoV4yjITYevAj7TI=:eyJzY29wZSI6ImtvZG8tcGhvbmUtem9uZTEtc3BhY2UiLCJkZWFkbGluZSI6MTYyNDUwMzMxMywgInJldHVybkJvZHkiOiJ7XCJjYWxsYmFja1VybFwiOlwiaHR0cDpcL1wvY2FsbGJhY2suZGV2LnFpbml1LmlvXCIsIFwiZm9vXCI6JCh4OmZvbyksIFwiYmFyXCI6JCh4OmJhciksIFwibWltZVR5cGVcIjokKG1pbWVUeXBlKSwgXCJoYXNoXCI6JChldGFnKSwgXCJrZXlcIjokKGtleSksIFwiZm5hbWVcIjokKGZuYW1lKX0ifQ=="; // 华南上传凭证 public static final String bucket_z2 = "kodo-phone-zone2-space"; - public static final String token_z2 = "dxVQk8gyk3WswArbNhdKIwmwibJ9nFsQhMNUmtIM:ZTqDdbvHJuP3hJFckpadCyW08Cs=:eyJzY29wZSI6ImtvZG8tcGhvbmUtem9uZTItc3BhY2UiLCJkZWFkbGluZSI6MTYxODgxNDM5MCwgInJldHVybkJvZHkiOiJ7XCJjYWxsYmFja1VybFwiOlwiaHR0cDpcL1wvY2FsbGJhY2suZGV2LnFpbml1LmlvXCIsIFwiZm9vXCI6JCh4OmZvbyksIFwiYmFyXCI6JCh4OmJhciksIFwibWltZVR5cGVcIjokKG1pbWVUeXBlKSwgXCJoYXNoXCI6JChldGFnKSwgXCJrZXlcIjokKGtleSksIFwiZm5hbWVcIjokKGZuYW1lKX0ifQ=="; + public static final String token_z2 = "dxVQk8gyk3WswArbNhdKIwmwibJ9nFsQhMNUmtIM:NBqIi2NnZ6ROpSwlAEmsb41Wxec=:eyJzY29wZSI6ImtvZG8tcGhvbmUtem9uZTItc3BhY2UiLCJkZWFkbGluZSI6MTYyNDUwMzMxMywgInJldHVybkJvZHkiOiJ7XCJjYWxsYmFja1VybFwiOlwiaHR0cDpcL1wvY2FsbGJhY2suZGV2LnFpbml1LmlvXCIsIFwiZm9vXCI6JCh4OmZvbyksIFwiYmFyXCI6JCh4OmJhciksIFwibWltZVR5cGVcIjokKG1pbWVUeXBlKSwgXCJoYXNoXCI6JChldGFnKSwgXCJrZXlcIjokKGtleSksIFwiZm5hbWVcIjokKGZuYW1lKX0ifQ=="; // 北美上传凭证 public static final String bucket_na0 = "kodo-phone-zone-na0-space"; - public static final String token_na0 = "dxVQk8gyk3WswArbNhdKIwmwibJ9nFsQhMNUmtIM:I8Q0E32hEelHH4xWBH2p17SxhdA=:eyJzY29wZSI6ImtvZG8tcGhvbmUtem9uZS1uYTAtc3BhY2UiLCJkZWFkbGluZSI6MTYxODgxNDM5MCwgInJldHVybkJvZHkiOiJ7XCJjYWxsYmFja1VybFwiOlwiaHR0cDpcL1wvY2FsbGJhY2suZGV2LnFpbml1LmlvXCIsIFwiZm9vXCI6JCh4OmZvbyksIFwiYmFyXCI6JCh4OmJhciksIFwibWltZVR5cGVcIjokKG1pbWVUeXBlKSwgXCJoYXNoXCI6JChldGFnKSwgXCJrZXlcIjokKGtleSksIFwiZm5hbWVcIjokKGZuYW1lKX0ifQ=="; + public static final String token_na0 = "dxVQk8gyk3WswArbNhdKIwmwibJ9nFsQhMNUmtIM:CjBzdmymDR-4iQh7GENWdFJnxXc=:eyJzY29wZSI6ImtvZG8tcGhvbmUtem9uZS1uYTAtc3BhY2UiLCJkZWFkbGluZSI6MTYyNDUwMzMxMywgInJldHVybkJvZHkiOiJ7XCJjYWxsYmFja1VybFwiOlwiaHR0cDpcL1wvY2FsbGJhY2suZGV2LnFpbml1LmlvXCIsIFwiZm9vXCI6JCh4OmZvbyksIFwiYmFyXCI6JCh4OmJhciksIFwibWltZVR5cGVcIjokKG1pbWVUeXBlKSwgXCJoYXNoXCI6JChldGFnKSwgXCJrZXlcIjokKGtleSksIFwiZm5hbWVcIjokKGZuYW1lKX0ifQ=="; // 东南亚上传凭证 public static final String bucket_as0 = "kodo-phone-zone-as0-space"; - public static final String token_as0 = "dxVQk8gyk3WswArbNhdKIwmwibJ9nFsQhMNUmtIM:DDXIo7KzUj3ceh5LveRXyNfsiZU=:eyJzY29wZSI6ImtvZG8tcGhvbmUtem9uZS1hczAtc3BhY2UiLCJkZWFkbGluZSI6MTYxODgxNDM5MCwgInJldHVybkJvZHkiOiJ7XCJjYWxsYmFja1VybFwiOlwiaHR0cDpcL1wvY2FsbGJhY2suZGV2LnFpbml1LmlvXCIsIFwiZm9vXCI6JCh4OmZvbyksIFwiYmFyXCI6JCh4OmJhciksIFwibWltZVR5cGVcIjokKG1pbWVUeXBlKSwgXCJoYXNoXCI6JChldGFnKSwgXCJrZXlcIjokKGtleSksIFwiZm5hbWVcIjokKGZuYW1lKX0ifQ=="; - public static final String invalidBucketToken = "dxVQk8gyk3WswArbNhdKIwmwibJ9nFsQhMNUmtIM:Kpi_0B6gZSf7nF5UgdZtvHx0h8M=:eyJzY29wZSI6InpvbmVfaW52YWxpZCIsImRlYWRsaW5lIjoxNjE4ODE0MzkwLCAicmV0dXJuQm9keSI6IntcImNhbGxiYWNrVXJsXCI6XCJodHRwOlwvXC9jYWxsYmFjay5kZXYucWluaXUuaW9cIiwgXCJmb29cIjokKHg6Zm9vKSwgXCJiYXJcIjokKHg6YmFyKSwgXCJtaW1lVHlwZVwiOiQobWltZVR5cGUpLCBcImhhc2hcIjokKGV0YWcpLCBcImtleVwiOiQoa2V5KSwgXCJmbmFtZVwiOiQoZm5hbWUpfSJ9"; + public static final String token_as0 = "dxVQk8gyk3WswArbNhdKIwmwibJ9nFsQhMNUmtIM:cp_BdEXGx5MieSwlg3kBhl-hfYE=:eyJzY29wZSI6ImtvZG8tcGhvbmUtem9uZS1hczAtc3BhY2UiLCJkZWFkbGluZSI6MTYyNDUwMzMxMywgInJldHVybkJvZHkiOiJ7XCJjYWxsYmFja1VybFwiOlwiaHR0cDpcL1wvY2FsbGJhY2suZGV2LnFpbml1LmlvXCIsIFwiZm9vXCI6JCh4OmZvbyksIFwiYmFyXCI6JCh4OmJhciksIFwibWltZVR5cGVcIjokKG1pbWVUeXBlKSwgXCJoYXNoXCI6JChldGFnKSwgXCJrZXlcIjokKGtleSksIFwiZm5hbWVcIjokKGZuYW1lKX0ifQ=="; + // 雾存储华东一区 + public static final String bucket_fog_cn_east1 = "test-fog-cn-east-1"; + public static final String token_fog_cn_east1 = "dxVQk8gyk3WswArbNhdKIwmwibJ9nFsQhMNUmtIM:iRev7qTeHdPXPURIyVsv2A3qfu0=:eyJzY29wZSI6InRlc3QtZm9nLWNuLWVhc3QtMSIsImRlYWRsaW5lIjoxNjI0NTAzMzEzLCAicmV0dXJuQm9keSI6IntcImNhbGxiYWNrVXJsXCI6XCJodHRwOlwvXC9jYWxsYmFjay5kZXYucWluaXUuaW9cIiwgXCJmb29cIjokKHg6Zm9vKSwgXCJiYXJcIjokKHg6YmFyKSwgXCJtaW1lVHlwZVwiOiQobWltZVR5cGUpLCBcImhhc2hcIjokKGV0YWcpLCBcImtleVwiOiQoa2V5KSwgXCJmbmFtZVwiOiQoZm5hbWUpfSJ9"; + public static final String invalidBucketToken = "dxVQk8gyk3WswArbNhdKIwmwibJ9nFsQhMNUmtIM:S_IC6jv5EHocYxtU1lrqGcQCUVs=:eyJzY29wZSI6InpvbmVfaW52YWxpZCIsImRlYWRsaW5lIjoxNjI0NTAzMzEzLCAicmV0dXJuQm9keSI6IntcImNhbGxiYWNrVXJsXCI6XCJodHRwOlwvXC9jYWxsYmFjay5kZXYucWluaXUuaW9cIiwgXCJmb29cIjokKHg6Zm9vKSwgXCJiYXJcIjokKHg6YmFyKSwgXCJtaW1lVHlwZVwiOiQobWltZVR5cGUpLCBcImhhc2hcIjokKGV0YWcpLCBcImtleVwiOiQoa2V5KSwgXCJmbmFtZVwiOiQoZm5hbWUpfSJ9"; // ----------- public static final String ak = "bjtWBQXrcxgo7HWwlC_bgHg81j352_GhgBGZPeOW"; diff --git a/library/src/androidTest/java/com/qiniu/android/UploadBaseTest.java b/library/src/androidTest/java/com/qiniu/android/UploadBaseTest.java index 22a521ca9..437c1db06 100644 --- a/library/src/androidTest/java/com/qiniu/android/UploadBaseTest.java +++ b/library/src/androidTest/java/com/qiniu/android/UploadBaseTest.java @@ -1,8 +1,11 @@ package com.qiniu.android; +import android.net.Uri; + import com.qiniu.android.http.ResponseInfo; import com.qiniu.android.storage.Configuration; import com.qiniu.android.storage.UpCompletionHandler; +import com.qiniu.android.storage.UpProgressBytesHandler; import com.qiniu.android.storage.UpProgressHandler; import com.qiniu.android.storage.UploadManager; import com.qiniu.android.storage.UploadOptions; @@ -12,11 +15,24 @@ import org.json.JSONObject; import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; import java.io.IOException; +import java.io.InputStream; +import java.io.RandomAccessFile; public class UploadBaseTest extends BaseTest { - protected UploadOptions defaultOptions = new UploadOptions(null, null, true, new UpProgressHandler() { + protected UploadOptions defaultOptions = new UploadOptions(null, null, true, new UpProgressBytesHandler() { + @Override + public void progress(String key, long uploadBytes, long totalBytes) { + double percent = 0; + if (totalBytes > 0) { + percent = (double) uploadBytes / (double) totalBytes; + } + LogUtil.d("== upload key:" + (key == null ? "" : key) + " uploadBytes:" + uploadBytes + " totalBytes:" + totalBytes + " percent:" + percent); + } + @Override public void progress(String key, double percent) { LogUtil.d("== upload key:" + (key == null ? "" : key) + " progress:" + percent); @@ -39,43 +55,27 @@ protected void uploadFileAndAssertSuccessResult(File file, String key, Configuration configuration, UploadOptions options) { + uploadFileAndAssertResult(ResponseInfo.RequestSuccess, file, key, configuration, options); + } - final UploadCompleteInfo completeInfo = new UploadCompleteInfo(); - uploadFile(file, key, configuration, options, new UpCompletionHandler() { - @Override - public void complete(String key, ResponseInfo info, JSONObject response) { - completeInfo.responseInfo = info; - completeInfo.key = key; - } - }); - - wait(new WaitConditional() { - @Override - public boolean shouldWait() { - return completeInfo.responseInfo == null; - } - }, 5 * 60); + protected void uploadFileAndAssertSuccessResult(UploadInfo file, + String key, + Configuration configuration, + UploadOptions options) { + uploadFileAndAssertResult(ResponseInfo.RequestSuccess, file, key, configuration, options); + } - LogUtil.d("=== upload response key:" + (key != null ? key : "") + " response:" + completeInfo.responseInfo); - assertTrue(completeInfo.responseInfo.toString(), completeInfo.responseInfo != null); - assertTrue(completeInfo.responseInfo.toString(), completeInfo.responseInfo.isOK()); - assertTrue(completeInfo.responseInfo.toString(), verifyUploadKey(key, completeInfo.key)); + protected void uploadFileAndAssertResult(int statusCode, + File file, + String key, + Configuration configuration, + UploadOptions options) { - // 成功验证 etag - String etag = null; - String serverEtag = null; - try { - etag = Etag.file(file); - serverEtag = completeInfo.responseInfo.response.getString("hash"); - } catch (Exception e) { - e.printStackTrace(); - } - LogUtil.d("=== upload etag:" + etag + " response etag:" + serverEtag); - assertEquals("file:" + etag + " server etag:" + serverEtag, etag, serverEtag); + uploadFileAndAssertResult(statusCode, file, TestConfig.token_na0, key, configuration, options); } protected void uploadFileAndAssertResult(int statusCode, - File file, + UploadInfo file, String key, Configuration configuration, UploadOptions options) { @@ -90,121 +90,62 @@ protected void uploadFileAndAssertResult(int statusCode, Configuration configuration, UploadOptions options) { - final UploadCompleteInfo completeInfo = new UploadCompleteInfo(); - uploadFile(file, token, key, configuration, options, new UpCompletionHandler() { - @Override - public void complete(String key, ResponseInfo info, JSONObject response) { - completeInfo.responseInfo = info; - completeInfo.key = key; - } - }); + UploadInfo fileInfo = new UploadInfo<>(file); + fileInfo.configWithFile(file); + uploadFileAndAssertResult(statusCode, fileInfo, token, key, configuration, options); - wait(new WaitConditional() { - @Override - public boolean shouldWait() { - return completeInfo.responseInfo == null; - } - }, 5 * 60); + Uri uri = Uri.fromFile(file); + UploadInfo uriInfo = new UploadInfo<>(uri); + uriInfo.configWithFile(file); + uploadFileAndAssertResult(statusCode, uriInfo, token, key, configuration, options); - LogUtil.d("=== upload response key:" + (key != null ? key : "") + " response:" + completeInfo.responseInfo); - assertTrue(completeInfo.responseInfo.toString(), completeInfo.responseInfo != null); - assertTrue(completeInfo.responseInfo.toString(), completeInfo.responseInfo.statusCode == statusCode); - assertTrue(completeInfo.responseInfo.toString(), verifyUploadKey(key, completeInfo.key)); - - // 成功验证 etag - if (statusCode == 200) { - String etag = null; - String serverEtag = null; + InputStream stream = null; + try { + stream = new FileInputStream(file); + } catch (FileNotFoundException e) { + } + UploadInfo streamInfo = new UploadInfo<>(stream); + streamInfo.configWithFile(file); + uploadFileAndAssertResult(statusCode, streamInfo, token, key, configuration, options); + if (stream != null) { try { - etag = Etag.file(file); - serverEtag = completeInfo.responseInfo.response.getString("hash"); - } catch (Exception e) { - e.printStackTrace(); + stream.close(); + } catch (IOException e) { } - LogUtil.d("=== upload etag:" + etag + " response etag:" + serverEtag); - assertEquals("file:" + etag + " server etag:" + serverEtag, etag, serverEtag); } - } - protected void uploadFile(File file, - String key, - Configuration configuration, - UploadOptions options, - UpCompletionHandler completionHandler) { - - uploadFile(file, TestConfig.token_na0, key, configuration, options, completionHandler); - } - - protected void uploadFile(File file, - String token, - String key, - Configuration configuration, - UploadOptions options, - UpCompletionHandler completionHandler) { - if (options == null) { - options = defaultOptions; + try { + stream = new FileInputStream(file); + } catch (FileNotFoundException e) { } - UploadManager manager = new UploadManager(configuration); - manager.put(file, key, token, completionHandler, options); - } - - - protected void uploadDataAndAssertSuccessResult(byte[] data, - String key, - Configuration configuration, - UploadOptions options) { - - final UploadCompleteInfo completeInfo = new UploadCompleteInfo(); - uploadData(data, key, configuration, options, new UpCompletionHandler() { - @Override - public void complete(String key, ResponseInfo info, JSONObject response) { - completeInfo.responseInfo = info; - completeInfo.key = key; - } - }); - - wait(new WaitConditional() { - @Override - public boolean shouldWait() { - return completeInfo.responseInfo == null; + streamInfo = new UploadInfo<>(stream); + streamInfo.configWithFile(file); + streamInfo.size = -1; + uploadFileAndAssertResult(statusCode, streamInfo, token, key, configuration, options); + if (stream != null) { + try { + stream.close(); + } catch (IOException e) { } - }, 5 * 60); - - LogUtil.d("=== upload response key:" + (key != null ? key : "") + " response:" + completeInfo.responseInfo); - assertTrue(completeInfo.responseInfo.toString(), completeInfo.responseInfo != null); - assertTrue(completeInfo.responseInfo.toString(), completeInfo.responseInfo.isOK()); - assertTrue(completeInfo.responseInfo.toString(), verifyUploadKey(key, completeInfo.key)); - - // 成功验证 etag - String etag = null; - String serverEtag = null; - try { - etag = Etag.data(data); - serverEtag = completeInfo.responseInfo.response.getString("hash"); - } catch (Exception e) { - e.printStackTrace(); } - LogUtil.d("=== upload etag:" + etag + " response etag:" + serverEtag); - assertEquals("file:" + etag + " server etag:" + serverEtag, etag, serverEtag); - } - - protected void uploadDataAndAssertResult(int statusCode, - byte[] data, - String key, - Configuration configuration, - UploadOptions options) { - uploadDataAndAssertResult(statusCode, data, TestConfig.token_na0, key, configuration, options); + if (file.length() < 10 * 1024 * 1024) { + byte[] data = getDataFromFile(file); + UploadInfo dataInfo = new UploadInfo<>(data); + dataInfo.configWithFile(file); + uploadFileAndAssertResult(statusCode, dataInfo, token, key, configuration, options); + } } - protected void uploadDataAndAssertResult(int statusCode, - byte[] data, + protected void uploadFileAndAssertResult(int statusCode, + UploadInfo file, String token, String key, Configuration configuration, UploadOptions options) { + final UploadCompleteInfo completeInfo = new UploadCompleteInfo(); - uploadData(data, token, key, configuration, options, new UpCompletionHandler() { + upload(file, token, key, configuration, options, new UpCompletionHandler() { @Override public void complete(String key, ResponseInfo info, JSONObject response) { completeInfo.responseInfo = info; @@ -217,19 +158,18 @@ public void complete(String key, ResponseInfo info, JSONObject response) { public boolean shouldWait() { return completeInfo.responseInfo == null; } - }, 5 * 60); + }, 10 * 60); - LogUtil.d("=== upload response key:" + (key != null ? key : "") + " response:" + completeInfo.responseInfo); + LogUtil.d("=== upload file type:" + file.type() + " response key:" + (key != null ? key : "") + " response:" + completeInfo.responseInfo); assertTrue(completeInfo.responseInfo.toString(), completeInfo.responseInfo != null); assertTrue(completeInfo.responseInfo.toString(), completeInfo.responseInfo.statusCode == statusCode); assertTrue(completeInfo.responseInfo.toString(), verifyUploadKey(key, completeInfo.key)); // 成功验证 etag - if (statusCode == 200) { - String etag = null; + if (statusCode == ResponseInfo.RequestSuccess) { + String etag = file.etag; String serverEtag = null; try { - etag = Etag.data(data); serverEtag = completeInfo.responseInfo.response.getString("hash"); } catch (Exception e) { e.printStackTrace(); @@ -239,28 +179,88 @@ public boolean shouldWait() { } } - protected void uploadData(byte[] data, - String key, - Configuration configuration, - UploadOptions options, - UpCompletionHandler completionHandler) { + protected void upload(UploadInfo file, + String key, + Configuration configuration, + UploadOptions options, + UpCompletionHandler completionHandler) { - uploadData(data, TestConfig.token_na0, key, configuration, options, completionHandler); + upload(file, TestConfig.token_na0, key, configuration, options, completionHandler); } - protected void uploadData(byte[] data, - String token, - String key, - Configuration configuration, - UploadOptions options, - UpCompletionHandler completionHandler) { + protected void upload(UploadInfo file, + String token, + String key, + Configuration configuration, + UploadOptions options, + UpCompletionHandler completionHandler) { if (options == null) { options = defaultOptions; } UploadManager manager = new UploadManager(configuration); - manager.put(data, key, token, completionHandler, options); + if (file.info instanceof File) { + manager.put((File) file.info, key, token, completionHandler, options); + } else if (file.info instanceof Uri) { + manager.put((Uri) file.info, null, key, token, completionHandler, options); + } else if (file.info instanceof InputStream) { + manager.put((InputStream) file.info, null, file.size, file.fileName, key, token, completionHandler, options); + } else if (file.info instanceof byte[]) { + manager.put((byte[]) file.info, key, token, completionHandler, options); + } else { + completionHandler.complete(key, ResponseInfo.fileError(new Exception("test case file type error")), null); + } + } + + protected byte[] getDataFromFile(File file) { + byte[] bytes = new byte[(int) file.length()]; + try { + RandomAccessFile accessFile = new RandomAccessFile(file, "r"); + accessFile.readFully(bytes); + } catch (Exception e) { + bytes = null; + } + return bytes; } + protected static class UploadInfo { + protected final T info; + protected String fileName; + protected long size = -1; + protected String etag; + protected String md5; + + public UploadInfo(T info) { + this.info = info; + } + + public void configWithFile(File file) { + fileName = file.getName(); + size = file.length(); + try { + etag = Etag.file(file); + } catch (Exception ignore) { + } + + try { + etag = Etag.file(file); + } catch (Exception ignore) { + } + } + + public String type() { + if (info instanceof File) { + return "file"; + } else if (info instanceof Uri) { + return "uri"; + } else if (info instanceof InputStream) { + return "stream"; + } else if (info instanceof byte[]) { + return "byte_array"; + } else { + return "none"; + } + } + } protected static class UploadCompleteInfo { String key; diff --git a/library/src/androidTest/java/com/qiniu/android/UploadFlowTest.java b/library/src/androidTest/java/com/qiniu/android/UploadFlowTest.java index a7e062db3..7170213d2 100644 --- a/library/src/androidTest/java/com/qiniu/android/UploadFlowTest.java +++ b/library/src/androidTest/java/com/qiniu/android/UploadFlowTest.java @@ -1,5 +1,7 @@ package com.qiniu.android; +import android.net.Uri; + import com.qiniu.android.common.FixedZone; import com.qiniu.android.common.ZoneInfo; import com.qiniu.android.common.ZonesInfo; @@ -8,6 +10,7 @@ import com.qiniu.android.storage.FileRecorder; import com.qiniu.android.storage.UpCancellationSignal; import com.qiniu.android.storage.UpCompletionHandler; +import com.qiniu.android.storage.UpProgressBytesHandler; import com.qiniu.android.storage.UpProgressHandler; import com.qiniu.android.storage.UploadOptions; import com.qiniu.android.utils.LogUtil; @@ -16,66 +19,66 @@ import org.json.JSONObject; import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; import java.io.IOException; +import java.io.InputStream; import java.util.ArrayList; public class UploadFlowTest extends UploadBaseTest { - protected void cancelTest(final float cancelPercent, + protected void cancelTest(long cancelPosition, File file, String key, Configuration configuration, UploadOptions options) { + cancelPosition *= 1024; - if (options == null) { - options = defaultOptions; - } - - final UploadOptions optionsReal = options; - final Flags flags = new Flags(); - UploadOptions cancelOptions = new UploadOptions(optionsReal.params, optionsReal.mimeType, optionsReal.checkCrc, new UpProgressHandler() { - @Override - public void progress(String key, double percent) { - if (cancelPercent <= percent) { - flags.shouldCancel = true; - } - if (optionsReal.progressHandler != null) { - optionsReal.progressHandler.progress(key, percent); - } - } - }, new UpCancellationSignal() { - @Override - public boolean isCancelled() { + LogUtil.d("======== progress File cancel ============================================"); + UploadInfo fileInfo = new UploadInfo<>(file); + fileInfo.configWithFile(file); + cancelTest(cancelPosition, fileInfo, key, configuration, options); - return flags.shouldCancel; - } - }, options.netReadyHandler); + Uri uri = Uri.fromFile(file); + UploadInfo uriInfo = new UploadInfo<>(uri); + uriInfo.configWithFile(file); + cancelTest(cancelPosition, uriInfo, key, configuration, options); - final UploadCompleteInfo completeInfo = new UploadCompleteInfo(); - uploadFile(file, key, configuration, cancelOptions, new UpCompletionHandler() { - @Override - public void complete(String key, ResponseInfo info, JSONObject response) { - completeInfo.key = key; - completeInfo.responseInfo = info; - } - }); + LogUtil.d("======== progress InputStream with size cancel ==========================="); + InputStream stream = null; + try { + stream = new FileInputStream(file); + } catch (FileNotFoundException e) { + } + UploadInfo streamInfo = new UploadInfo<>(stream); + streamInfo.configWithFile(file); + cancelTest(cancelPosition, streamInfo, key, configuration, options); - wait(new WaitConditional() { - @Override - public boolean shouldWait() { - return completeInfo.responseInfo == null; - } - }, 5 * 60); - assertTrue(completeInfo.responseInfo.toString(), completeInfo.responseInfo != null); - assertTrue(completeInfo.responseInfo.toString(), completeInfo.responseInfo.isCancelled()); - assertTrue(completeInfo.responseInfo.toString(), verifyUploadKey(key, completeInfo.key)); + LogUtil.d("======== progress InputStream without size cancel ========================="); + stream = null; + try { + stream = new FileInputStream(file); + } catch (FileNotFoundException e) { + } + streamInfo = new UploadInfo<>(stream); + streamInfo.configWithFile(file); + streamInfo.size = -1; + cancelTest(cancelPosition, streamInfo, key, configuration, options); + + LogUtil.d("======== progress data cancel ============================================="); + if (file.length() < 10 * 1024 * 1024) { + byte[] data = getDataFromFile(file); + UploadInfo dataInfo = new UploadInfo<>(data); + dataInfo.configWithFile(file); + cancelTest(cancelPosition, dataInfo, key, configuration, options); + } } - protected void cancelTest(final float cancelPercent, - byte[] data, - String key, - Configuration configuration, - UploadOptions options) { + private void cancelTest(final long cancelPosition, + UploadInfo file, + String key, + Configuration configuration, + UploadOptions options) { if (options == null) { options = defaultOptions; @@ -83,16 +86,20 @@ protected void cancelTest(final float cancelPercent, final UploadOptions optionsReal = options; final Flags flags = new Flags(); - UploadOptions cancelOptions = new UploadOptions(optionsReal.params, optionsReal.mimeType, optionsReal.checkCrc, new UpProgressHandler() { + UploadOptions cancelOptions = new UploadOptions(optionsReal.params, optionsReal.mimeType, optionsReal.checkCrc, new UpProgressBytesHandler() { @Override - public void progress(String key, double percent) { - if (cancelPercent <= percent) { + public void progress(String key, long uploadBytes, long totalBytes) { + if (cancelPosition < uploadBytes) { flags.shouldCancel = true; } - if (optionsReal.progressHandler != null) { - optionsReal.progressHandler.progress(key, percent); + if (optionsReal.progressHandler instanceof UpProgressBytesHandler) { + ((UpProgressBytesHandler)optionsReal.progressHandler).progress(key, uploadBytes, totalBytes); } } + + @Override + public void progress(String key, double percent) { + } }, new UpCancellationSignal() { @Override public boolean isCancelled() { @@ -102,7 +109,7 @@ public boolean isCancelled() { }, options.netReadyHandler); final UploadCompleteInfo completeInfo = new UploadCompleteInfo(); - uploadData(data, key, configuration, cancelOptions, new UpCompletionHandler() { + upload(file, key, configuration, cancelOptions, new UpCompletionHandler() { @Override public void complete(String key, ResponseInfo info, JSONObject response) { completeInfo.key = key; @@ -121,105 +128,83 @@ public boolean shouldWait() { assertTrue(completeInfo.responseInfo.toString(), verifyUploadKey(key, completeInfo.key)); } - - protected void reuploadUploadTest(final float resumePercent, - final File file, + protected void reuploadUploadTest(long resumePosition, + File file, String key, - final Configuration configuration, + Configuration configuration, UploadOptions options) { - FileRecorder fileRecorder = null; - try { - fileRecorder = new FileRecorder(Utils.sdkDirectory() + "/Test"); - } catch (IOException e) { - e.printStackTrace(); - } + resumePosition *= 1024; - final Configuration reuploadConfiguration = new Configuration.Builder() - .chunkSize(configuration.chunkSize) - .putThreshold(configuration.putThreshold) - .retryMax(configuration.retryMax) - .connectTimeout(configuration.connectTimeout) - .responseTimeout(configuration.responseTimeout) - .retryInterval(configuration.retryInterval) - .recorder(fileRecorder, null) - .proxy(configuration.proxy) - .urlConverter(configuration.urlConverter) - .useHttps(configuration.useHttps) - .allowBackupHost(configuration.allowBackupHost) - .useConcurrentResumeUpload(configuration.useConcurrentResumeUpload) - .resumeUploadVersion(configuration.resumeUploadVersion) - .concurrentTaskCount(configuration.concurrentTaskCount) - .zone(configuration.zone) - .build(); + LogUtil.d("======== progress File ReUpload =============================================="); + UploadInfo fileInfo = new UploadInfo<>(file); + fileInfo.configWithFile(file); + reuploadUploadTest(resumePosition, fileInfo, fileInfo, key, configuration, options); + LogUtil.d("======== progress Uri ReUpload =============================================="); + Uri uri = Uri.fromFile(file); + UploadInfo uriInfo = new UploadInfo<>(uri); + uriInfo.configWithFile(file); + reuploadUploadTest(resumePosition, uriInfo, uriInfo, key, configuration, options); - if (options == null) { - options = defaultOptions; + LogUtil.d("======== progress InputStream with size ReUpload ============================="); + InputStream firstStream = null; + try { + firstStream = new FileInputStream(file); + } catch (FileNotFoundException e) { } - final UploadOptions optionsReal = options; - - cancelTest(resumePercent, file, key, reuploadConfiguration, optionsReal); + UploadInfo firstStreamInfo = new UploadInfo<>(firstStream); + firstStreamInfo.configWithFile(file); + InputStream secondStream = null; + try { + secondStream = new FileInputStream(file); + } catch (FileNotFoundException e) { + } + UploadInfo secondStreamInfo = new UploadInfo<>(secondStream); + secondStreamInfo.configWithFile(file); - final Flags flags = new Flags(); - UploadOptions reuploadOptions = new UploadOptions(optionsReal.params, optionsReal.mimeType, optionsReal.checkCrc, new UpProgressHandler() { - @Override - public void progress(String key, double percent) { - if (!flags.flags) { - flags.flags = true; - double minPercent = 0; - double currentChunkCount = 0; - double chunkSize = 0; - if (!reuploadConfiguration.useConcurrentResumeUpload) { - currentChunkCount = 1; - chunkSize = reuploadConfiguration.chunkSize; - } else if (reuploadConfiguration.resumeUploadVersion == Configuration.RESUME_UPLOAD_VERSION_V1) { - currentChunkCount = reuploadConfiguration.concurrentTaskCount; - chunkSize = Configuration.BLOCK_SIZE; - } else { - currentChunkCount = reuploadConfiguration.concurrentTaskCount; - chunkSize = reuploadConfiguration.chunkSize; - } - minPercent = percent + currentChunkCount * chunkSize / (double) file.length(); - if (resumePercent <= minPercent) { - flags.isSuccess = true; - } - LogUtil.d("== upload reupload percent:" + percent + " minPercent:" + minPercent); - } + reuploadUploadTest(resumePosition, firstStreamInfo, secondStreamInfo, key, configuration, options); + try { + firstStream.close(); + secondStream.close(); + } catch (IOException e) { + } - if (optionsReal.progressHandler != null) { - optionsReal.progressHandler.progress(key, percent); - } - } - }, null, options.netReadyHandler); + LogUtil.d("======== progress InputStream without size ReUpload =========================="); + firstStream = null; + try { + firstStream = new FileInputStream(file); + } catch (FileNotFoundException e) { + } + firstStreamInfo = new UploadInfo<>(firstStream); + firstStreamInfo.configWithFile(file); + firstStreamInfo.size = -1; - final UploadCompleteInfo completeInfo = new UploadCompleteInfo(); - uploadFile(file, key, reuploadConfiguration, reuploadOptions, new UpCompletionHandler() { - @Override - public void complete(String key, ResponseInfo info, JSONObject response) { - completeInfo.key = key; - completeInfo.responseInfo = info; - } - }); + secondStream = null; + try { + secondStream = new FileInputStream(file); + } catch (FileNotFoundException e) { + } + secondStreamInfo = new UploadInfo<>(secondStream); + secondStreamInfo.configWithFile(file); + secondStreamInfo.size = -1; - wait(new WaitConditional() { - @Override - public boolean shouldWait() { - return completeInfo.responseInfo == null; - } - }, 5 * 60); - assertTrue(completeInfo.responseInfo.toString(), flags.isSuccess); - assertTrue(completeInfo.responseInfo.toString(), completeInfo.responseInfo != null); - assertTrue(completeInfo.responseInfo.toString(), completeInfo.responseInfo.isOK()); - assertTrue(completeInfo.responseInfo.toString(), verifyUploadKey(key, completeInfo.key)); + reuploadUploadTest(resumePosition, firstStreamInfo, secondStreamInfo, key, configuration, options); + try { + firstStream.close(); + secondStream.close(); + } catch (IOException e) { + } } - protected void reuploadUploadTest(final float reuploadPercent, - final byte[] data, - String key, - final Configuration configuration, - UploadOptions options) { + private void reuploadUploadTest(final long resumePosition, + final UploadInfo firstFile, + final UploadInfo secondFile, + String key, + final Configuration configuration, + UploadOptions options) { + FileRecorder fileRecorder = null; try { fileRecorder = new FileRecorder(Utils.sdkDirectory() + "/Test"); @@ -244,45 +229,40 @@ protected void reuploadUploadTest(final float reuploadPercent, .concurrentTaskCount(configuration.concurrentTaskCount) .zone(configuration.zone) .build(); + + if (options == null) { options = defaultOptions; } final UploadOptions optionsReal = options; - cancelTest(reuploadPercent, data, key, reuploadConfiguration, optionsReal); + cancelTest(resumePosition, firstFile, key, reuploadConfiguration, optionsReal); + + LogUtil.d("progress ReUpload ===================================================="); final Flags flags = new Flags(); - UploadOptions reuploadOptions = new UploadOptions(optionsReal.params, optionsReal.mimeType, optionsReal.checkCrc, new UpProgressHandler() { + UploadOptions reuploadOptions = new UploadOptions(optionsReal.params, optionsReal.mimeType, optionsReal.checkCrc, new UpProgressBytesHandler() { @Override public void progress(String key, double percent) { + } + + @Override + public void progress(String key, long uploadBytes, long totalBytes) { if (!flags.flags) { - double minPercent = 0; - double currentChunkCount = 0; - double chunkSize = 0; - if (!reuploadConfiguration.useConcurrentResumeUpload) { - currentChunkCount = 1; - chunkSize = reuploadConfiguration.chunkSize; - } else if (reuploadConfiguration.resumeUploadVersion == Configuration.RESUME_UPLOAD_VERSION_V1) { - currentChunkCount = reuploadConfiguration.concurrentTaskCount; - chunkSize = Configuration.BLOCK_SIZE; - } else { - currentChunkCount = reuploadConfiguration.concurrentTaskCount; - chunkSize = reuploadConfiguration.chunkSize; - } - minPercent = percent + currentChunkCount * chunkSize / (double) data.length; - if (reuploadPercent >= minPercent) { + flags.flags = true; + if (uploadBytes <= resumePosition && uploadBytes > 0) { flags.isSuccess = true; } - LogUtil.d("== reupload percent:" + percent + " minPercent:" + minPercent); } - if (optionsReal.progressHandler != null) { - optionsReal.progressHandler.progress(key, percent); + + if (optionsReal.progressHandler != null && optionsReal.progressHandler instanceof UpProgressBytesHandler) { + ((UpProgressBytesHandler)optionsReal.progressHandler).progress(key, uploadBytes, totalBytes); } } }, null, options.netReadyHandler); final UploadCompleteInfo completeInfo = new UploadCompleteInfo(); - uploadData(data, key, reuploadConfiguration, reuploadOptions, new UpCompletionHandler() { + upload(secondFile, key, reuploadConfiguration, reuploadOptions, new UpCompletionHandler() { @Override public void complete(String key, ResponseInfo info, JSONObject response) { completeInfo.key = key; @@ -295,8 +275,7 @@ public void complete(String key, ResponseInfo info, JSONObject response) { public boolean shouldWait() { return completeInfo.responseInfo == null; } - }, 5 * 60); - + }, 10* 60); assertTrue(completeInfo.responseInfo.toString(), flags.isSuccess); assertTrue(completeInfo.responseInfo.toString(), completeInfo.responseInfo != null); assertTrue(completeInfo.responseInfo.toString(), completeInfo.responseInfo.isOK()); @@ -307,50 +286,26 @@ protected void switchRegionTestWithFile(File file, String key, Configuration configuration, UploadOptions options) { - if (configuration == null) { - configuration = new Configuration.Builder().build(); - } - Configuration configurationReal = configuration; - ArrayList hostArray0 = new ArrayList<>(); - hostArray0.add("mock1.up.qiniup.com"); - hostArray0.add("mock2.up.qiniup.com"); - ZoneInfo zoneInfo0 = ZoneInfo.buildInfo(hostArray0, null, null); - ArrayList hostArray1 = new ArrayList<>(); - hostArray1.add("upload-na0.qiniup.com"); - hostArray1.add("up-na0.qiniup.com"); - ZoneInfo zoneInfo1 = ZoneInfo.buildInfo(hostArray1, null, null); + UploadInfo fileInfo = new UploadInfo<>(file); + fileInfo.configWithFile(file); + switchRegionTestWithFile(fileInfo, key, configuration, options); - ArrayList zoneInfoArray = new ArrayList<>(); - zoneInfoArray.add(zoneInfo0); - zoneInfoArray.add(zoneInfo1); - ZonesInfo zonesInfo = new ZonesInfo(zoneInfoArray); - FixedZone zone = new FixedZone(zonesInfo); + Uri uri = Uri.fromFile(file); + UploadInfo uriInfo = new UploadInfo<>(uri); + uriInfo.configWithFile(file); + switchRegionTestWithFile(uriInfo, key, configuration, options); - Configuration switchConfiguration = new Configuration.Builder() - .chunkSize(configurationReal.chunkSize) - .putThreshold(configurationReal.putThreshold) - .retryMax(configurationReal.retryMax) - .connectTimeout(configurationReal.connectTimeout) - .responseTimeout(configurationReal.responseTimeout) - .retryInterval(configurationReal.retryInterval) - .recorder(configurationReal.recorder, configurationReal.keyGen) - .proxy(configurationReal.proxy) - .urlConverter(configurationReal.urlConverter) - .useHttps(configurationReal.useHttps) - .allowBackupHost(configurationReal.allowBackupHost) - .useConcurrentResumeUpload(configurationReal.useConcurrentResumeUpload) - .resumeUploadVersion(configurationReal.resumeUploadVersion) - .concurrentTaskCount(configurationReal.concurrentTaskCount) - .zone(zone) - .build(); - uploadFileAndAssertSuccessResult(file, key, switchConfiguration, options); + byte[] data = getDataFromFile(file); + UploadInfo dataInfo = new UploadInfo<>(data); + dataInfo.configWithFile(file); + switchRegionTestWithFile(dataInfo, key, configuration, options); } - protected void switchRegionTestWithData(byte[] data, - String key, - Configuration configuration, - UploadOptions options) { + private void switchRegionTestWithFile(UploadInfo file, + String key, + Configuration configuration, + UploadOptions options) { if (configuration == null) { configuration = new Configuration.Builder().build(); } @@ -386,11 +341,11 @@ protected void switchRegionTestWithData(byte[] data, .useConcurrentResumeUpload(configurationReal.useConcurrentResumeUpload) .resumeUploadVersion(configurationReal.resumeUploadVersion) .concurrentTaskCount(configurationReal.concurrentTaskCount) + .zone(zone) .build(); - uploadDataAndAssertSuccessResult(data, key, switchConfiguration, options); + uploadFileAndAssertSuccessResult(file, key, switchConfiguration, options); } - protected static class Flags { boolean flags; boolean shouldCancel; diff --git a/library/src/androidTest/java/com/qiniu/android/UriTest.java b/library/src/androidTest/java/com/qiniu/android/UriTest.java new file mode 100644 index 000000000..32cf77f42 --- /dev/null +++ b/library/src/androidTest/java/com/qiniu/android/UriTest.java @@ -0,0 +1,190 @@ +package com.qiniu.android; + +import android.content.ContentResolver; +import android.content.ContentValues; +import android.net.Uri; +import android.provider.MediaStore; + +import com.qiniu.android.http.ResponseInfo; +import com.qiniu.android.storage.Configuration; +import com.qiniu.android.storage.UpCompletionHandler; +import com.qiniu.android.storage.UploadManager; +import com.qiniu.android.utils.ContextGetter; +import com.qiniu.android.utils.Etag; +import com.qiniu.android.utils.LogUtil; + +import junit.framework.Assert; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +public class UriTest extends BaseTest { + + private static boolean[][] testConfigList = { + {true, true, true}, + {true, true, false}, + {true, false, true}, + {true, false, false}, + + {false, true, true}, + {false, true, false}, + {false, false, true}, + {false, false, false}, + }; + + public void notestUpload() { + int MB = 1024; + int[] sizeList = {512, MB, 4*MB, 5*MB, 8*MB, 10*MB, 20*MB}; + for (int size : sizeList) { + String fileName = size + "KB" + ".mp4"; + + File file = createFile(size); + Uri uri = null; + try { + uri = writeFileToDownload(file, fileName); + } catch (FileNotFoundException e) { + e.printStackTrace(); + Assert.fail(e.getMessage()); + return; + } + + String etag = null; + try { + etag = Etag.file(file); + } catch (IOException e) { + e.printStackTrace(); + } + for (boolean[] config : testConfigList) { + testUpload(uri, fileName, etag, config[0], config[1], config[2]); + } + removeUri(uri); + } + } + + private void testUpload(Uri uri, String fileName, String etag, boolean isHttps, boolean isResumableV1, boolean isConcurrent) { + + assertNotNull("Uri write file error:" + fileName, uri); + + Configuration configuration = new Configuration.Builder() + .resumeUploadVersion(isResumableV1 ? Configuration.RESUME_UPLOAD_VERSION_V1 : Configuration.RESUME_UPLOAD_VERSION_V2) + .chunkSize(isResumableV1 ? 1024*1024*2 : 1024*1024*4) + .useConcurrentResumeUpload(isConcurrent) + .useHttps(isHttps) + .build(); + + UploadManager uploadManager = new UploadManager(configuration); + + String key = "uri_upload_"; + key += isHttps ? "https_" : "http_"; + key += isResumableV1 ? "v1_" : "v2_"; + key += isConcurrent ? "serial_" : "concurrent_"; + key += fileName; + final UploadCompleteInfo completeInfo = new UploadCompleteInfo(); + uploadManager.put(uri, null, key, TestConfig.token_na0, new UpCompletionHandler() { + @Override + public void complete(String key, ResponseInfo info, JSONObject response) { + completeInfo.key = key; + completeInfo.responseInfo = info; + } + }, null); + + wait(new WaitConditional() { + @Override + public boolean shouldWait() { + return completeInfo.responseInfo == null; + } + }, 10 * 60); + + LogUtil.d("=== upload response key:" + (key != null ? key : "") + " response:" + completeInfo.responseInfo); + assertTrue(completeInfo.responseInfo.toString(), completeInfo.responseInfo != null); + assertTrue(completeInfo.responseInfo.toString(), completeInfo.responseInfo.statusCode == ResponseInfo.RequestSuccess); + assertTrue(completeInfo.responseInfo.toString(), key.equals(completeInfo.key)); + + String serverEtag = null; + try { + serverEtag = completeInfo.responseInfo.response.getString("hash"); + } catch (JSONException e) { + e.printStackTrace(); + } + System.out.println(" etag:" + etag); + System.out.println("serverEtag:" + serverEtag); + assertNotNull("key:" + key, serverEtag); + assertEquals("key:" + key, etag, serverEtag); + } + + private File createFile(int size) { + File file = null; + try { + file = TempFile.createFile(size); + } catch (IOException e) { + e.printStackTrace(); + } + return file; + } + + private Uri writeFileToDownload(File file, String fileName) throws FileNotFoundException { + + InputStream inputStream = null; + try { + inputStream = new FileInputStream(file); + } catch (FileNotFoundException e) { + e.printStackTrace(); + throw e; + } + + ContentResolver resolver = ContextGetter.applicationContext().getContentResolver(); + + ContentValues contentValues = new ContentValues(); + contentValues.put(MediaStore.Video.Media.DISPLAY_NAME, fileName); + Uri imageUri = null; + try { + imageUri = resolver.insert(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, contentValues); + } catch (Exception e) { + e.printStackTrace(); + throw e; + } + + if (imageUri != null) { + // 若生成了uri,则表示该文件添加成功 + // 使用流将内容写入该uri中即可 + OutputStream outputStream = null; + try { + outputStream = resolver.openOutputStream(imageUri); + } catch (FileNotFoundException e) { + e.printStackTrace(); + } + if (outputStream != null) { + try { + byte[] buf = new byte[1024]; + int len; + while ((len = inputStream.read(buf)) > 0) { + outputStream.write(buf, 0, len); + } + outputStream.close(); + inputStream.close(); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + return imageUri; + } + + private void removeUri(Uri uri) { + ContentResolver resolver = ContextGetter.applicationContext().getContentResolver(); + resolver.delete(uri, null, null); + } + + protected static class UploadCompleteInfo { + String key; + ResponseInfo responseInfo; + } +} diff --git a/library/src/androidTest/java/com/qiniu/android/UtilsTest.java b/library/src/androidTest/java/com/qiniu/android/UtilsTest.java index 9eb9391c5..3770af187 100644 --- a/library/src/androidTest/java/com/qiniu/android/UtilsTest.java +++ b/library/src/androidTest/java/com/qiniu/android/UtilsTest.java @@ -27,4 +27,46 @@ public void testIPType(){ type = Utils.getIpType("2000::0001:2345:6789:abcd", testHost); assertTrue(type.equals(testHost + "-ipv6-2000-0000-0000-0000")); } + + public void testIsIPType(){ + + String ip = null; + boolean isIpv6 = false; + + ip = "10.10.120.3"; + isIpv6 = Utils.isIpv6(ip); + assertFalse(ip, isIpv6); + + ip = "130.101.120.3"; + isIpv6 = Utils.isIpv6(ip); + assertFalse(ip, isIpv6); + + ip = "2000:0000:0000:0000:0001:2345:6789:abcd"; + isIpv6 = Utils.isIpv6(ip); + assertTrue(ip, isIpv6); + + ip = "2000:0:0:0:0001:2345:6789:abcd"; + isIpv6 = Utils.isIpv6(ip); + assertTrue(ip, isIpv6); + + ip = "2000::0001:2345:6789:abcd"; + isIpv6 = Utils.isIpv6(ip); + assertTrue(ip, isIpv6); + + ip = "0::0"; + isIpv6 = Utils.isIpv6(ip); + assertTrue(ip, isIpv6); + + ip = "ffff::ffff:2345:6789:abcd"; + isIpv6 = Utils.isIpv6(ip); + assertTrue(ip, isIpv6); + + ip = "ff1::ffff:2345:6789:abcd"; + isIpv6 = Utils.isIpv6(ip); + assertTrue(ip, isIpv6); + + ip = "ffff1::ffff:2345:6789:abcd"; + isIpv6 = Utils.isIpv6(ip); + assertFalse(ip, isIpv6); + } } diff --git a/library/src/main/java/com/qiniu/android/common/Constants.java b/library/src/main/java/com/qiniu/android/common/Constants.java index 91e6c4b65..14a3e01d6 100644 --- a/library/src/main/java/com/qiniu/android/common/Constants.java +++ b/library/src/main/java/com/qiniu/android/common/Constants.java @@ -2,7 +2,7 @@ public final class Constants { - public static final String VERSION = "8.2.1"; + public static final String VERSION = "8.3.0"; public static final String UTF_8 = "utf-8"; } diff --git a/library/src/main/java/com/qiniu/android/common/ZoneInfo.java b/library/src/main/java/com/qiniu/android/common/ZoneInfo.java index b6a880368..1df62b8ab 100644 --- a/library/src/main/java/com/qiniu/android/common/ZoneInfo.java +++ b/library/src/main/java/com/qiniu/android/common/ZoneInfo.java @@ -25,6 +25,7 @@ public class ZoneInfo { public final int ttl; public final boolean http3Enabled; + public final boolean ipv6; public final List domains; public final List old_domains; @@ -72,11 +73,13 @@ public static ZoneInfo buildInfo(List mainHosts, private ZoneInfo(int ttl, boolean http3Enabled, + boolean ipv6, String regionId, List domains, List old_domains) { this.ttl = ttl; this.http3Enabled = http3Enabled; + this.ipv6 = ipv6; this.regionId = regionId; this.domains = domains; this.old_domains = old_domains; @@ -95,18 +98,22 @@ public static ZoneInfo buildFromJson(JSONObject obj) throws JSONException { int ttl = obj.optInt("ttl"); boolean http3Enabled = false; + boolean ipv6Enabled = false; try { JSONObject features = obj.getJSONObject("features"); - JSONObject http3 = features.getJSONObject("http3"); - http3Enabled = http3.getBoolean("enabled"); - } catch (Exception ignored) { - } + JSONObject http3 = features.optJSONObject("http3"); + if (http3 != null) { + http3Enabled = http3.optBoolean("enabled"); + } - String regionId = obj.optString("region"); - if (regionId == null) { - regionId = EmptyRegionId; + JSONObject ipv6 = features.optJSONObject("ipv6"); + if (ipv6 != null) { + ipv6Enabled = ipv6.optBoolean("enabled"); + } + } catch (Exception ignored) { } + String regionId = obj.optString("region", EmptyRegionId); JSONObject up = obj.optJSONObject("up"); if (up == null) { return null; @@ -141,7 +148,7 @@ public static ZoneInfo buildFromJson(JSONObject obj) throws JSONException { return null; } - ZoneInfo zoneInfo = new ZoneInfo(ttl, http3Enabled, regionId, domains, old_domains); + ZoneInfo zoneInfo = new ZoneInfo(ttl, http3Enabled, ipv6Enabled, regionId, domains, old_domains); zoneInfo.detailInfo = obj; zoneInfo.allHosts = allHosts; @@ -167,6 +174,7 @@ public boolean isValid() { return ttl > (currentTimestamp - buildTimestamp); } + @Deprecated public static class UploadServerGroup { public final String info; public final ArrayList main; diff --git a/library/src/main/java/com/qiniu/android/http/request/httpclient/ByteBody.java b/library/src/main/java/com/qiniu/android/http/request/httpclient/ByteBody.java index da5604fa1..a136b5006 100644 --- a/library/src/main/java/com/qiniu/android/http/request/httpclient/ByteBody.java +++ b/library/src/main/java/com/qiniu/android/http/request/httpclient/ByteBody.java @@ -1,11 +1,15 @@ package com.qiniu.android.http.request.httpclient; +import java.io.ByteArrayInputStream; import java.io.IOException; +import java.io.InputStream; import java.util.Arrays; import okhttp3.MediaType; import okhttp3.RequestBody; import okio.BufferedSink; +import okio.Okio; +import okio.Source; /** * Created by yangsen on 2020/6/10 @@ -37,23 +41,21 @@ public long contentLength() throws IOException { @Override public void writeTo(BufferedSink bufferedSink) throws IOException { - int byteIndex = 0; + int byteOffset = 0; int byteSize = SEGMENT_SIZE; - while (byteIndex < body.length){ - byteSize = Math.min(byteSize, body.length - byteIndex); - - RequestBody requestBody = getRequestBodyWithRange(byteIndex, byteSize); + while (byteOffset < body.length){ + byteSize = Math.min(byteSize, body.length - byteOffset); + RequestBody requestBody = getRequestBodyWithRange(byteOffset, byteSize); requestBody.writeTo(bufferedSink); bufferedSink.flush(); - byteIndex += byteSize; + byteOffset += byteSize; } - } private RequestBody getRequestBodyWithRange(int location, int size){ byte[] data = Arrays.copyOfRange(body, location, location + size); - return RequestBody.create(data, contentType()); + return RequestBody.create(contentType(), data); } } diff --git a/library/src/main/java/com/qiniu/android/http/request/httpclient/SystemHttpClient.java b/library/src/main/java/com/qiniu/android/http/request/httpclient/SystemHttpClient.java index 9161a8908..9a2c8bf0a 100644 --- a/library/src/main/java/com/qiniu/android/http/request/httpclient/SystemHttpClient.java +++ b/library/src/main/java/com/qiniu/android/http/request/httpclient/SystemHttpClient.java @@ -17,6 +17,8 @@ import org.json.JSONObject; import java.io.IOException; +import java.lang.reflect.Field; +import java.lang.reflect.Method; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.ProtocolException; @@ -56,6 +58,7 @@ public class SystemHttpClient implements IRequestClient { public static final String JsonMime = "application/json"; public static final String FormMime = "application/x-www-form-urlencoded"; + private boolean hasHandleComplete = false; private static ConnectionPool pool; private Request currentRequest; private OkHttpClient httpClient; @@ -73,7 +76,7 @@ public void request(Request request, metrics = new UploadSingleRequestMetrics(); metrics.clientName = "okhttp"; - metrics.clientVersion = Version.userAgent.replace("okhttp/", ""); + metrics.clientVersion = getOkHttpVersion(); metrics.setRequest(request); currentRequest = request; httpClient = createHttpClient(connectionProxy); @@ -382,27 +385,32 @@ public void callFailed(Call call, IOException ioe) { }; } - private synchronized void handleError(Request request, - int responseCode, - String errorMsg, - RequestClientCompleteHandler complete) { - if (metrics == null || metrics.response != null) { - return; + private void handleError(Request request, + int responseCode, + String errorMsg, + RequestClientCompleteHandler complete) { + synchronized (this) { + if (hasHandleComplete) { + return; + } + hasHandleComplete = true; } ResponseInfo info = ResponseInfo.create(request, responseCode, null, null, errorMsg); metrics.response = info; metrics.request = request; complete.complete(info, metrics, info.response); - releaseResource(); } - private synchronized void handleResponse(Request request, - okhttp3.Response response, - RequestClientCompleteHandler complete) { - if (metrics == null || metrics.response != null) { - return; + private void handleResponse(Request request, + okhttp3.Response response, + RequestClientCompleteHandler complete) { + synchronized (this) { + if (hasHandleComplete) { + return; + } + hasHandleComplete = true; } int statusCode = response.code(); @@ -420,7 +428,7 @@ private synchronized void handleResponse(Request request, String errorMessage = null; try { responseBody = response.body().bytes(); - } catch (IOException e) { + } catch (Exception e) { errorMessage = e.getMessage(); } @@ -509,6 +517,24 @@ private static JSONObject buildJsonResp(byte[] body) throws Exception { } + private static String getOkHttpVersion() { + try { + Method get = Version.class.getMethod("userAgent"); + Object version = get.invoke(Version.class); + return (version + "").replace("okhttp/", ""); + } catch (Exception ignore) { + } + + try { + Field versionField = Version.class.getField("userAgent"); + Object version = versionField.get(Version.class); + return (version + "").replace("okhttp/", ""); + } catch (Exception ignore) { + } + + return ""; + } + private static class ResponseTag { public String ip = ""; public long duration = -1; diff --git a/library/src/main/java/com/qiniu/android/http/serverRegion/UploadDomainRegion.java b/library/src/main/java/com/qiniu/android/http/serverRegion/UploadDomainRegion.java index bc5c51bb4..8c0ec21b7 100644 --- a/library/src/main/java/com/qiniu/android/http/serverRegion/UploadDomainRegion.java +++ b/library/src/main/java/com/qiniu/android/http/serverRegion/UploadDomainRegion.java @@ -23,6 +23,8 @@ public class UploadDomainRegion implements IUploadRegion { // 是否支持http3 private boolean http3Enabled; + // 是否使用ipv6 + private boolean ipv6Enabled; // 是否冻结过Host,PS:如果没有冻结过 Host,则当前 Region 上传也就不会有错误信息,可能会返回-9,所以必须要再进行一次尝试 private boolean hasFreezeHost; @@ -88,6 +90,8 @@ public void setupRegionData(ZoneInfo zoneInfo) { // 暂不开启 http3Enabled = false; + ipv6Enabled = zoneInfo.ipv6; + ArrayList domainHostList = new ArrayList<>(); if (zoneInfo.domains != null) { domainHostList.addAll(zoneInfo.domains); @@ -139,9 +143,14 @@ public IUploadServer getNextServer(UploadRequestState requestState, ResponseInfo IUploadServer domainServer = domain.getServer(new UploadServerDomain.GetServerCondition() { @Override public boolean condition(String host, UploadServer serverP, UploadServer filterServer) { - - // 1.1 剔除冻结对象 String filterServerIP = filterServer == null ? null : filterServer.getIp(); + + // 1.1 剔除 ipv6 + if (!ipv6Enabled && Utils.isIpv6(filterServerIP)) { + return false; + } + + // 1.2 剔除冻结对象 String frozenType = UploadServerFreezeUtil.getFrozenType(host, filterServerIP); boolean isFrozen = UploadServerFreezeUtil.isTypeFrozenByFreezeManagers(frozenType, new UploadServerFreezeManager[]{UploadServerFreezeUtil.globalHttp3Freezer()}); @@ -149,7 +158,7 @@ public boolean condition(String host, UploadServer serverP, UploadServer filterS return false; } - // 1.2 挑选网络状态最优 + // 1.3 挑选网络状态最优 return UploadServerNetworkStatus.isServerNetworkBetter(filterServer, serverP); } }); @@ -176,8 +185,14 @@ public boolean condition(String host, UploadServer serverP, UploadServer filterS IUploadServer domainServer = domain.getServer(new UploadServerDomain.GetServerCondition() { @Override public boolean condition(String host, UploadServer serverP, UploadServer filterServer) { - // 1.1 剔除冻结对象 String filterServerIP = filterServer == null ? null : filterServer.getIp(); + + // 1.1 剔除 ipv6 + if (!ipv6Enabled && Utils.isIpv6(filterServerIP)) { + return false; + } + + // 1.2 剔除冻结对象 String frozenType = UploadServerFreezeUtil.getFrozenType(host, filterServerIP); boolean isFrozen = UploadServerFreezeUtil.isTypeFrozenByFreezeManagers(frozenType, new UploadServerFreezeManager[]{partialHttp2Freezer, UploadServerFreezeUtil.globalHttp2Freezer()}); @@ -185,7 +200,7 @@ public boolean condition(String host, UploadServer serverP, UploadServer filterS return false; } - // 1.2 挑选网络状态最优 + // 1.3 挑选网络状态最优 return UploadServerNetworkStatus.isServerNetworkBetter(filterServer, serverP); } }); diff --git a/library/src/main/java/com/qiniu/android/storage/BaseUpload.java b/library/src/main/java/com/qiniu/android/storage/BaseUpload.java index eb364ef32..3cfcf7e61 100644 --- a/library/src/main/java/com/qiniu/android/storage/BaseUpload.java +++ b/library/src/main/java/com/qiniu/android/storage/BaseUpload.java @@ -18,7 +18,7 @@ abstract class BaseUpload implements Runnable { protected final String key; protected final String fileName; protected final byte[] data; - protected final File file; + protected final UploadSource uploadSource; protected final UpToken token; protected final UploadOptions option; protected final Configuration config; @@ -33,7 +33,7 @@ abstract class BaseUpload implements Runnable { private int currentRegionIndex; private ArrayList regions; - private BaseUpload(File file, + private BaseUpload(UploadSource source, byte[] data, String fileName, String key, @@ -43,7 +43,7 @@ private BaseUpload(File file, Recorder recorder, String recorderKey, UpTaskCompletionHandler completionHandler) { - this.file = file; + this.uploadSource = source; this.data = data; this.fileName = fileName != null ? fileName : "?"; this.key = key; @@ -57,7 +57,7 @@ private BaseUpload(File file, this.initData(); } - protected BaseUpload(File file, + protected BaseUpload(UploadSource source, String key, UpToken token, UploadOptions option, @@ -65,7 +65,7 @@ protected BaseUpload(File file, Recorder recorder, String recorderKey, UpTaskCompletionHandler completionHandler) { - this(file, null, file.getName(), key, token, option, config, recorder, recorderKey, completionHandler); + this(source, null, source.getFileName(), key, token, option, config, recorder, recorderKey, completionHandler); } protected BaseUpload(byte[] data, @@ -181,6 +181,7 @@ protected void insertRegionAtFirst(IUploadRegion region) { } protected boolean switchRegion() { + if (regions == null) { return false; } diff --git a/library/src/main/java/com/qiniu/android/storage/ConcurrentResumeUpload.java b/library/src/main/java/com/qiniu/android/storage/ConcurrentResumeUpload.java index 3f9278a9a..39598950d 100644 --- a/library/src/main/java/com/qiniu/android/storage/ConcurrentResumeUpload.java +++ b/library/src/main/java/com/qiniu/android/storage/ConcurrentResumeUpload.java @@ -5,12 +5,13 @@ import com.qiniu.android.utils.StringUtils; import java.io.File; +import java.io.InputStream; class ConcurrentResumeUpload extends PartsUpload { private GroupTaskThread groupTaskThread; - protected ConcurrentResumeUpload(File file, + protected ConcurrentResumeUpload(UploadSource source, String key, UpToken token, UploadOptions option, @@ -18,7 +19,7 @@ protected ConcurrentResumeUpload(File file, Recorder recorder, String recorderKey, UpTaskCompletionHandler completionHandler) { - super(file, key, token, option, config, recorder, recorderKey, completionHandler); + super(source, key, token, option, config, recorder, recorderKey, completionHandler); } @Override diff --git a/library/src/main/java/com/qiniu/android/storage/Configuration.java b/library/src/main/java/com/qiniu/android/storage/Configuration.java index 1dc210470..88ffc5cca 100644 --- a/library/src/main/java/com/qiniu/android/storage/Configuration.java +++ b/library/src/main/java/com/qiniu/android/storage/Configuration.java @@ -154,6 +154,14 @@ private KeyGenerator getKeyGen(KeyGenerator keyGen) { public String gen(String key, File file) { return key + "_._" + new StringBuffer(file.getAbsolutePath()).reverse(); } + + @Override + public String gen(String key, String sourceId) { + if (sourceId == null) { + sourceId = ""; + } + return key + "_._" + sourceId; + } }; } return keyGen; diff --git a/library/src/main/java/com/qiniu/android/storage/FormUpload.java b/library/src/main/java/com/qiniu/android/storage/FormUpload.java index e8c39d96a..1c52ab9ed 100644 --- a/library/src/main/java/com/qiniu/android/storage/FormUpload.java +++ b/library/src/main/java/com/qiniu/android/storage/FormUpload.java @@ -13,7 +13,7 @@ class FormUpload extends BaseUpload { private boolean isAsync = true; - private double previousPercent; + private final UpProgress upProgress; private RequestTransaction uploadTransaction; protected FormUpload(byte[] data, @@ -24,6 +24,7 @@ protected FormUpload(byte[] data, Configuration config, UpTaskCompletionHandler completionHandler) { super(data, key, fileName, token, option, config, completionHandler); + this.upProgress = new UpProgress(this.option.progressHandler); } @Override @@ -36,18 +37,7 @@ protected void startToUpload() { RequestProgressHandler progressHandler = new RequestProgressHandler() { @Override public void progress(long totalBytesWritten, long totalBytesExpectedToWrite) { - if (option.progressHandler != null){ - double percent = (double)totalBytesWritten / (double)totalBytesExpectedToWrite; - if (percent > 0.95){ - percent = 0.95; - } - if (percent > previousPercent){ - previousPercent = percent; - } else { - percent = previousPercent; - } - option.progressHandler.progress(key, percent); - } + upProgress.progress(key, totalBytesWritten, totalBytesExpectedToWrite); } }; uploadTransaction.uploadFormData(data, fileName, isAsync, progressHandler, new RequestTransaction.RequestCompleteHandler() { @@ -62,12 +52,7 @@ public void complete(ResponseInfo responseInfo, UploadRegionRequestMetrics reque return; } - AsyncRun.runInMain(new Runnable() { - @Override - public void run() { - option.progressHandler.progress(key, 1.0); - } - }); + upProgress.notifyDone(key, data.length); completeAction(responseInfo, response); } }); diff --git a/library/src/main/java/com/qiniu/android/storage/KeyGenerator.java b/library/src/main/java/com/qiniu/android/storage/KeyGenerator.java index 00ff525b1..a1043c557 100644 --- a/library/src/main/java/com/qiniu/android/storage/KeyGenerator.java +++ b/library/src/main/java/com/qiniu/android/storage/KeyGenerator.java @@ -13,5 +13,27 @@ public interface KeyGenerator { * @param file 本地文件名 * @return 持久化上传纪录的key */ + @Deprecated String gen(String key, File file); + + /** + * 根据服务器的key和本地文件唯一 ID 生成持久化纪录的key + * 如果开启断点续传功能,请确保持久化纪录的 key 相同的文件一定是同一个 + * SDK 断点续传流程: + * 1. 用户调用上传接口上传资源 A + * 2. 根据资源 A 信息调用 {@link KeyGenerator#gen(String, String)} 生成持久化纪录的 key + * 3. 根据生成持久化纪录的 key 获取本地缓存记录,无缓存则直接走新资源上传流程 + * 4. 解析缓存记录中的 sourceId 对比当前资源 A 的 sourceId,如果不同则走新资源上传流程 + * 5. 对比缓存资源的 size 和待上传资源 A 的 size,如果两个 size 均不为 -1 且不相等, + * 则走新资源上传流程;size 等于 -1 时,资源 A 为 InputStream 且不知道文件流大小,不验证 size + * 6. 断点续传生效,进入断点续传流程 + * + * @param key 服务器的key + * @param sourceId 本地文件 ID + * File: fileName + modifyTime + * Uri: fileName + modifyTime + * InputStream: fileName + * @return 持久化上传纪录的key + */ + String gen(String key, String sourceId); } diff --git a/library/src/main/java/com/qiniu/android/storage/PartsUpload.java b/library/src/main/java/com/qiniu/android/storage/PartsUpload.java index 7a934f794..5e67e6d32 100644 --- a/library/src/main/java/com/qiniu/android/storage/PartsUpload.java +++ b/library/src/main/java/com/qiniu/android/storage/PartsUpload.java @@ -20,7 +20,7 @@ class PartsUpload extends BaseUpload { private ResponseInfo uploadDataErrorResponseInfo; private JSONObject uploadDataErrorResponse; - protected PartsUpload(File file, + protected PartsUpload(UploadSource source, String key, UpToken token, UploadOptions option, @@ -28,7 +28,7 @@ protected PartsUpload(File file, Recorder recorder, String recorderKey, UpTaskCompletionHandler completionHandler) { - super(file, key, token, option, config, recorder, recorderKey, completionHandler); + super(source, key, token, option, config, recorder, recorderKey, completionHandler); } @Override @@ -37,18 +37,18 @@ protected void initData() { if (config != null && config.resumeUploadVersion == Configuration.RESUME_UPLOAD_VERSION_V1) { LogUtil.i("key:" + StringUtils.toNonnullString(key) + " 分片V1"); - uploadPerformer = new PartsUploadPerformerV1(file, fileName, key, token, option, config, recorderKey); + uploadPerformer = new PartsUploadPerformerV1(uploadSource, fileName, key, token, option, config, recorderKey); } else { LogUtil.i("key:" + StringUtils.toNonnullString(key) + " 分片V2"); - uploadPerformer = new PartsUploadPerformerV2(file, fileName, key, token, option, config, recorderKey); + uploadPerformer = new PartsUploadPerformerV2(uploadSource, fileName, key, token, option, config, recorderKey); } } boolean isAllUploaded() { - if (uploadPerformer.fileInfo == null) { + if (uploadPerformer.uploadInfo == null) { return false; } else { - return uploadPerformer.fileInfo.isAllUploaded(); + return uploadPerformer.uploadInfo.isAllUploaded(); } } @@ -85,7 +85,7 @@ protected int prepareToUpload() { LogUtil.i("key:" + StringUtils.toNonnullString(key) + " region:" + StringUtils.toNonnullString(uploadPerformer.currentRegion.getZoneInfo().regionId)); } - if (file == null || !uploadPerformer.canReadFile()) { + if (!uploadPerformer.canReadFile()) { code = ResponseInfo.LocalIOError; } @@ -94,6 +94,11 @@ protected int prepareToUpload() { @Override protected boolean switchRegion() { + // 重新加载资源,如果加载失败,不可切换 region + if (!uploadPerformer.couldReloadInfo() || !uploadPerformer.reloadInfo()) { + return false; + } + boolean isSuccess = super.switchRegion(); if (isSuccess) { uploadPerformer.switchRegion(getCurrentRegion()); @@ -142,6 +147,13 @@ public void complete() { return; } + // 只有再读取结束再能知道文件大小,需要检测 + if (uploadPerformer.uploadInfo.getSourceSize() == 0) { + ResponseInfo response = ResponseInfo.zeroSize("file is empty"); + completeAction(response, null); + return; + } + LogUtil.i("key:" + StringUtils.toNonnullString(key) + " completeUpload"); // 3. 组装文件 @@ -156,12 +168,6 @@ public void complete(ResponseInfo responseInfo, JSONObject response) { return; } - AsyncRun.runInMain(new Runnable() { - @Override - public void run() { - option.progressHandler.progress(key, 1.0); - } - }); completeAction(responseInfo, response); } }); @@ -282,7 +288,7 @@ private void reportBlock() { item.setReport(metrics.totalElapsedTime(), ReportItem.BlockKeyTotalElapsedTime); item.setReport(metrics.bytesSend(), ReportItem.BlockKeyBytesSent); item.setReport(uploadPerformer.recoveredFrom, ReportItem.BlockKeyRecoveredFrom); - item.setReport(file.length(), ReportItem.BlockKeyFileSize); + item.setReport(uploadSource.getSize(), ReportItem.BlockKeyFileSize); item.setReport(Utils.getCurrentProcessID(), ReportItem.BlockKeyPid); item.setReport(Utils.getCurrentThreadID(), ReportItem.BlockKeyTid); diff --git a/library/src/main/java/com/qiniu/android/storage/PartsUploadPerformer.java b/library/src/main/java/com/qiniu/android/storage/PartsUploadPerformer.java index 8330af0fb..e29d0fdd4 100644 --- a/library/src/main/java/com/qiniu/android/storage/PartsUploadPerformer.java +++ b/library/src/main/java/com/qiniu/android/storage/PartsUploadPerformer.java @@ -6,17 +6,12 @@ import com.qiniu.android.http.request.IUploadRegion; import com.qiniu.android.http.request.RequestTransaction; import com.qiniu.android.http.serverRegion.UploadDomainRegion; -import com.qiniu.android.utils.AsyncRun; import com.qiniu.android.utils.LogUtil; import com.qiniu.android.utils.StringUtils; import org.json.JSONException; import org.json.JSONObject; -import java.io.File; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.RandomAccessFile; import java.util.ArrayList; import java.util.List; @@ -26,8 +21,8 @@ abstract class PartsUploadPerformer { final String key; final String fileName; - final File file; - final RandomAccessFile randomAccessFile; + final UploadSource uploadSource; + private final UpProgress upProgress; final UpToken token; final UploadOptions options; @@ -37,20 +32,19 @@ abstract class PartsUploadPerformer { private IUploadRegion targetRegion; protected IUploadRegion currentRegion; - private double previousPercent; Long recoveredFrom; - UploadFileInfo fileInfo; + UploadInfo uploadInfo; List uploadTransactions; - PartsUploadPerformer(File file, + PartsUploadPerformer(UploadSource uploadSource, String fileName, String key, UpToken token, UploadOptions options, Configuration config, String recorderKey) { - this.file = file; + this.uploadSource = uploadSource; this.key = key; this.fileName = fileName; this.token = token; @@ -58,46 +52,38 @@ abstract class PartsUploadPerformer { this.config = config; this.recorder = config.recorder; this.recorderKey = recorderKey; + this.upProgress = new UpProgress(this.options.progressHandler); - RandomAccessFile randomAccessFile = null; - if (file != null) { - try { - randomAccessFile = new RandomAccessFile(file, "r"); - } catch (FileNotFoundException ignored) { - } - } - this.randomAccessFile = randomAccessFile; this.initData(); } void initData() { uploadTransactions = new ArrayList<>(); + uploadInfo = getDefaultUploadInfo(); recoverUploadInfoFromRecord(); - if (fileInfo == null) { - fileInfo = getDefaultUploadFileInfo(); - } } boolean canReadFile() { - return randomAccessFile != null; + return uploadInfo != null && uploadInfo.hasValidResource(); + } + + boolean couldReloadInfo() { + return uploadInfo.couldReloadSource(); + } + + boolean reloadInfo() { + return uploadInfo.reloadSource(); } void closeFile() { - if (randomAccessFile != null) { - try { - randomAccessFile.close(); - } catch (IOException e) { - try { - randomAccessFile.close(); - } catch (IOException ignored) { - } - } + if (uploadInfo != null) { + uploadInfo.close(); } } void switchRegion(IUploadRegion region) { - if (fileInfo != null) { - fileInfo.clearUploadState(); + if (uploadInfo != null) { + uploadInfo.clearUploadState(); } currentRegion = region; recoveredFrom = null; @@ -106,29 +92,16 @@ void switchRegion(IUploadRegion region) { } } - void notifyProgress() { - if (fileInfo == null) { + void notifyProgress(Boolean isCompleted) { + if (uploadInfo == null) { return; } - double percent = fileInfo.progress(); - if (percent > 0.95) { - percent = 0.95; - } - if (percent > previousPercent) { - previousPercent = percent; + + if (isCompleted) { + upProgress.notifyDone(key, uploadInfo.getSourceSize()); } else { - percent = previousPercent; + upProgress.progress(key, uploadInfo.uploadSize(), uploadInfo.getSourceSize()); } - - final double notifyPercent = percent; - AsyncRun.runInMain(new Runnable() { - @Override - public void run() { - if (options != null && options.progressHandler != null) { - options.progressHandler.progress(key, notifyPercent); - } - } - }); } void recordUploadInfo() { @@ -142,10 +115,10 @@ void recordUploadInfo() { if (currentRegion != null && currentRegion.getZoneInfo() != null) { zoneInfoJson = currentRegion.getZoneInfo().detailInfo; } - if (fileInfo != null) { - fileInfoJson = fileInfo.toJsonObject(); + if (uploadInfo != null) { + fileInfoJson = uploadInfo.toJsonObject(); } - if (zoneInfoJson != null && fileInfo != null) { + if (zoneInfoJson != null && fileInfoJson != null) { JSONObject info = new JSONObject(); try { info.put(kRecordZoneInfoKey, zoneInfoJson); @@ -162,8 +135,8 @@ void recordUploadInfo() { void removeUploadInfoRecord() { recoveredFrom = null; - if (fileInfo != null) { - fileInfo.clearUploadState(); + if (uploadInfo != null) { + uploadInfo.clearUploadState(); } if (recorder != null && recorderKey != null) { recorder.del(recorderKey); @@ -180,7 +153,7 @@ void recoverUploadInfoFromRecord() { " recoverUploadInfoFromRecord"); String key = recorderKey; - if (recorder == null || key == null || key.length() == 0 || file == null) { + if (recorder == null || key == null || key.length() == 0 || uploadSource == null) { return; } @@ -195,21 +168,20 @@ void recoverUploadInfoFromRecord() { try { JSONObject info = new JSONObject(new String(data)); ZoneInfo zoneInfo = ZoneInfo.buildFromJson(info.getJSONObject(kRecordZoneInfoKey)); - UploadFileInfo recoverFileInfo = getFileFromJson(info.getJSONObject(kRecordFileInfoKey)); - if (zoneInfo != null && recoverFileInfo != null && !recoverFileInfo.isEmpty() && file != null && - recoverFileInfo.size == file.length() && - recoverFileInfo.modifyTime == file.lastModified()) { + UploadInfo recoverUploadInfo = getUploadInfoFromJson(uploadSource, info.getJSONObject(kRecordFileInfoKey)); + if (zoneInfo != null && recoverUploadInfo != null && recoverUploadInfo.isValid() && uploadInfo.isSameUploadInfo(recoverUploadInfo)) { LogUtil.i("key:" + StringUtils.toNonnullString(key) + " recorderKey:" + StringUtils.toNonnullString(recorderKey) + " recoverUploadInfoFromRecord valid"); - fileInfo = recoverFileInfo; + recoverUploadInfo.checkInfoStateAndUpdate(); + uploadInfo = recoverUploadInfo; UploadDomainRegion region = new UploadDomainRegion(); region.setupRegionData(zoneInfo); currentRegion = region; targetRegion = region; - recoveredFrom = (long) ((recoverFileInfo.progress() * recoverFileInfo.size)); + recoveredFrom = recoverUploadInfo.uploadSize(); } else { LogUtil.i("key:" + StringUtils.toNonnullString(key) + " recorderKey:" + StringUtils.toNonnullString(recorderKey) + @@ -252,9 +224,9 @@ void destroyUploadRequestTransaction(RequestTransaction transaction) { } } - abstract UploadFileInfo getDefaultUploadFileInfo(); + abstract UploadInfo getDefaultUploadInfo(); - abstract UploadFileInfo getFileFromJson(JSONObject jsonObject); + abstract UploadInfo getUploadInfoFromJson(UploadSource source, JSONObject jsonObject); abstract void serverInit(PartsUploadPerformerCompleteHandler completeHandler); diff --git a/library/src/main/java/com/qiniu/android/storage/PartsUploadPerformerV1.java b/library/src/main/java/com/qiniu/android/storage/PartsUploadPerformerV1.java index 9e02249ed..79f6d1046 100644 --- a/library/src/main/java/com/qiniu/android/storage/PartsUploadPerformerV1.java +++ b/library/src/main/java/com/qiniu/android/storage/PartsUploadPerformerV1.java @@ -10,35 +10,32 @@ import org.json.JSONException; import org.json.JSONObject; -import java.io.File; import java.io.IOException; import java.util.ArrayList; class PartsUploadPerformerV1 extends PartsUploadPerformer { - private static int BlockSize = 4 * 1024 * 1024; - - PartsUploadPerformerV1(File file, + PartsUploadPerformerV1(UploadSource uploadSource, String fileName, String key, UpToken token, UploadOptions options, Configuration config, String recorderKey) { - super(file, fileName, key, token, options, config, recorderKey); + super(uploadSource, fileName, key, token, options, config, recorderKey); } @Override - UploadFileInfo getFileFromJson(JSONObject jsonObject) { + UploadInfo getUploadInfoFromJson(UploadSource source, JSONObject jsonObject) { if (jsonObject == null) { return null; } - return UploadFileInfoPartV1.fileFromJson(jsonObject); + return UploadInfoV1.infoFromJson(source, jsonObject); } @Override - UploadFileInfo getDefaultUploadFileInfo() { - return new UploadFileInfoPartV1(file.length(), BlockSize, getUploadChunkSize(), file.lastModified()); + UploadInfo getDefaultUploadInfo() { + return new UploadInfoV1(uploadSource, config); } @Override @@ -49,35 +46,35 @@ void serverInit(PartsUploadPerformerCompleteHandler completeHandler) { @Override void uploadNextData(final PartsUploadPerformerDataCompleteHandler completeHandler) { - UploadFileInfoPartV1 uploadFileInfo = (UploadFileInfoPartV1) fileInfo; + UploadInfoV1 info = (UploadInfoV1) uploadInfo; UploadBlock block = null; UploadData chunk = null; synchronized (this) { - block = uploadFileInfo.nextUploadBlock(); - if (block != null) { - chunk = block.nextUploadData(); + try { + block = info.nextUploadBlock(); + chunk = info.nextUploadData(block); if (chunk != null) { - chunk.isUploading = true; - chunk.isCompleted = false; + chunk.updateState(UploadData.State.Uploading); } - } - } - if (block == null || chunk == null) { - LogUtil.i("key:" + StringUtils.toNonnullString(key) + " no chunk left"); + } catch (Exception e) { + // 此处可能导致后面无法恢复 + LogUtil.i("key:" + StringUtils.toNonnullString(key) + e.getMessage()); - ResponseInfo responseInfo = ResponseInfo.sdkInteriorError("no chunk left"); - completeHandler.complete(true, responseInfo, null, null); - return; + ResponseInfo responseInfo = ResponseInfo.localIOError(e.getMessage()); + completeHandler.complete(true, responseInfo, null, null); + return; + } } - chunk.data = getChunkDataWithRetry(chunk, block); - if (chunk.data == null) { + if (block == null || chunk == null) { LogUtil.i("key:" + StringUtils.toNonnullString(key) + " no chunk left"); - - chunk.isUploading = false; - chunk.isCompleted = false; - ResponseInfo responseInfo = ResponseInfo.localIOError("get data error"); + ResponseInfo responseInfo = null; + if (uploadInfo.getSourceSize() == 0) { + responseInfo = ResponseInfo.zeroSize("file is empty"); + } else { + responseInfo = ResponseInfo.sdkInteriorError("no chunk left"); + } completeHandler.complete(true, responseInfo, null, null); return; } @@ -87,40 +84,34 @@ void uploadNextData(final PartsUploadPerformerDataCompleteHandler completeHandle RequestProgressHandler progressHandler = new RequestProgressHandler() { @Override public void progress(long totalBytesWritten, long totalBytesExpectedToWrite) { - uploadChunk.progress = (double) totalBytesWritten / (double) totalBytesExpectedToWrite; - notifyProgress(); + uploadChunk.setUploadSize(totalBytesWritten); + notifyProgress(false); } }; PartsUploadPerformerCompleteHandler completeHandlerP = new PartsUploadPerformerCompleteHandler() { @Override public void complete(ResponseInfo responseInfo, UploadRegionRequestMetrics requestMetrics, JSONObject response) { - uploadChunk.data = null; - - String blockContext = null; + String ctx = null; if (response != null) { try { - blockContext = response.getString("ctx"); + ctx = response.getString("ctx"); } catch (JSONException e) { } } - if (responseInfo.isOK() && blockContext != null) { - uploadBlock.context = blockContext; - uploadChunk.progress = 1; - uploadChunk.isUploading = false; - uploadChunk.isCompleted = true; + if (responseInfo.isOK() && ctx != null) { + uploadBlock.ctx = ctx; + uploadChunk.updateState(UploadData.State.Complete); recordUploadInfo(); - notifyProgress(); + notifyProgress(false); } else { - uploadChunk.isUploading = false; - uploadChunk.isCompleted = false; - + uploadChunk.updateState(UploadData.State.WaitToUpload); } completeHandler.complete(false, responseInfo, requestMetrics, response); } }; - if (uploadChunk.isFirstData()) { + if (info.isFirstData(uploadChunk)) { LogUtil.i("key:" + StringUtils.toNonnullString(key) + " makeBlock"); makeBlock(uploadBlock, uploadChunk, progressHandler, completeHandlerP); } else { @@ -131,20 +122,23 @@ public void complete(ResponseInfo responseInfo, UploadRegionRequestMetrics reque @Override void completeUpload(final PartsUploadPerformerCompleteHandler completeHandler) { - UploadFileInfoPartV1 uploadFileInfo = (UploadFileInfoPartV1) fileInfo; + UploadInfoV1 info = (UploadInfoV1) uploadInfo; String[] contexts = null; - ArrayList contextsList = uploadFileInfo.allBlocksContexts(); + ArrayList contextsList = info.allBlocksContexts(); if (contextsList != null && contextsList.size() > 0) { contexts = contextsList.toArray(new String[contextsList.size()]); } final RequestTransaction transaction = createUploadRequestTransaction(); - transaction.makeFile(uploadFileInfo.size, fileName, contexts, true, new RequestTransaction.RequestCompleteHandler() { + transaction.makeFile(info.getSourceSize(), fileName, contexts, true, new RequestTransaction.RequestCompleteHandler() { @Override public void complete(ResponseInfo responseInfo, UploadRegionRequestMetrics requestMetrics, JSONObject response) { + if (responseInfo.isOK()) { + notifyProgress(true); + } destroyUploadRequestTransaction(transaction); completeHandler.complete(responseInfo, requestMetrics, response); } @@ -173,7 +167,7 @@ private void uploadChunk(final UploadBlock block, final PartsUploadPerformerCompleteHandler completeHandler) { final RequestTransaction transaction = createUploadRequestTransaction(); - transaction.uploadChunk(block.context, block.offset, chunk.data, chunk.offset, true, progressHandler, new RequestTransaction.RequestCompleteHandler() { + transaction.uploadChunk(block.ctx, block.offset, chunk.data, chunk.offset, true, progressHandler, new RequestTransaction.RequestCompleteHandler() { @Override public void complete(ResponseInfo responseInfo, UploadRegionRequestMetrics requestMetrics, JSONObject response) { @@ -183,54 +177,4 @@ public void complete(ResponseInfo responseInfo, UploadRegionRequestMetrics reque }); } - - private byte[] getChunkDataWithRetry(UploadData chunk, UploadBlock block) { - byte[] uploadData = null; - - int maxTime = 3; - int index = 0; - while (index < maxTime) { - uploadData = getChunkData(chunk, block); - if (uploadData != null) { - break; - } - index ++; - } - - return uploadData; - } - - private synchronized byte[] getChunkData(UploadData chunk, UploadBlock block) { - if (randomAccessFile == null || chunk == null || block == null) { - return null; - } - int readSize = 0; - byte[] data = new byte[chunk.size]; - try { - randomAccessFile.seek((chunk.offset + block.offset)); - while (readSize < chunk.size) { - int ret = randomAccessFile.read(data, readSize, (chunk.size - readSize)); - if (ret < 0) { - break; - } - readSize += ret; - } - - // 读数据非预期 - if (readSize != chunk.size) { - data = null; - } - } catch (IOException e) { - data = null; - } - return data; - } - - private int getUploadChunkSize() { - if (config.useConcurrentResumeUpload) { - return BlockSize; - } else { - return config.chunkSize; - } - } } diff --git a/library/src/main/java/com/qiniu/android/storage/PartsUploadPerformerV2.java b/library/src/main/java/com/qiniu/android/storage/PartsUploadPerformerV2.java index a3cad8d56..d968a4e2b 100644 --- a/library/src/main/java/com/qiniu/android/storage/PartsUploadPerformerV2.java +++ b/library/src/main/java/com/qiniu/android/storage/PartsUploadPerformerV2.java @@ -4,47 +4,44 @@ import com.qiniu.android.http.metrics.UploadRegionRequestMetrics; import com.qiniu.android.http.request.RequestTransaction; import com.qiniu.android.http.request.handler.RequestProgressHandler; -import com.qiniu.android.utils.Etag; import com.qiniu.android.utils.LogUtil; import com.qiniu.android.utils.StringUtils; import org.json.JSONException; import org.json.JSONObject; -import java.io.File; -import java.io.IOException; import java.util.List; import java.util.Map; class PartsUploadPerformerV2 extends PartsUploadPerformer { - PartsUploadPerformerV2(File file, + PartsUploadPerformerV2(UploadSource uploadSource, String fileName, String key, UpToken token, UploadOptions options, Configuration config, String recorderKey) { - super(file, fileName, key, token, options, config, recorderKey); + super(uploadSource, fileName, key, token, options, config, recorderKey); } @Override - UploadFileInfo getFileFromJson(JSONObject jsonObject) { + UploadInfo getUploadInfoFromJson(UploadSource source, JSONObject jsonObject) { if (jsonObject == null) { return null; } - return UploadFileInfoPartV2.fileFromJson(jsonObject); + return UploadInfoV2.infoFromJson(source, jsonObject); } @Override - UploadFileInfo getDefaultUploadFileInfo() { - return new UploadFileInfoPartV2(file.length(), config.chunkSize, file.lastModified()); + UploadInfo getDefaultUploadInfo() { + return new UploadInfoV2(uploadSource, config); } @Override void serverInit(final PartsUploadPerformerCompleteHandler completeHandler) { - final UploadFileInfoPartV2 uploadFileInfo = (UploadFileInfoPartV2) fileInfo; - if (uploadFileInfo != null && uploadFileInfo.isValid()) { + final UploadInfoV2 info = (UploadInfoV2) uploadInfo; + if (info != null && info.isValid()) { LogUtil.i("key:" + StringUtils.toNonnullString(key) + " serverInit success"); ResponseInfo responseInfo = ResponseInfo.successResponse(); completeHandler.complete(responseInfo, null, null); @@ -68,8 +65,8 @@ public void complete(ResponseInfo responseInfo, UploadRegionRequestMetrics reque } } if (responseInfo.isOK() && uploadId != null && expireAt != null) { - uploadFileInfo.uploadId = uploadId; - uploadFileInfo.expireAt = expireAt; + info.uploadId = uploadId; + info.expireAt = expireAt; recordUploadInfo(); } completeHandler.complete(responseInfo, requestMetrics, response); @@ -79,51 +76,52 @@ public void complete(ResponseInfo responseInfo, UploadRegionRequestMetrics reque @Override void uploadNextData(final PartsUploadPerformerDataCompleteHandler completeHandler) { - UploadFileInfoPartV2 uploadFileInfo = (UploadFileInfoPartV2) fileInfo; + final UploadInfoV2 info = (UploadInfoV2) uploadInfo; UploadData data = null; synchronized (this) { - data = uploadFileInfo.nextUploadData(); - if (data != null) { - data.isUploading = true; - data.isCompleted = false; + try { + data = info.nextUploadData(); + if (data != null) { + data.updateState(UploadData.State.Uploading); + } + } catch (Exception e) { + // 此处可能无法恢复 + LogUtil.i("key:" + StringUtils.toNonnullString(key) + " " + e.getMessage()); + + ResponseInfo responseInfo = ResponseInfo.localIOError(e.getMessage()); + completeHandler.complete(true, responseInfo, null, responseInfo.response); + return; } } if (data == null) { LogUtil.i("key:" + StringUtils.toNonnullString(key) + " no data left"); - ResponseInfo responseInfo = ResponseInfo.sdkInteriorError("no data left"); + ResponseInfo responseInfo = null; + if (uploadInfo.getSourceSize() == 0) { + responseInfo = ResponseInfo.zeroSize("file is empty"); + } else { + responseInfo = ResponseInfo.sdkInteriorError("no chunk left"); + } completeHandler.complete(true, responseInfo, null, null); return; } - data.data = getUploadDataWithRetry(data); - if (data.data == null) { - LogUtil.i("key:" + StringUtils.toNonnullString(key) + " get data error"); - - data.isUploading = false; - data.isCompleted = false; - ResponseInfo responseInfo = ResponseInfo.localIOError("get data error"); - completeHandler.complete(true, responseInfo, null, responseInfo.response); - return; - } - final UploadData uploadData = data; RequestProgressHandler progressHandler = new RequestProgressHandler() { @Override public void progress(long totalBytesWritten, long totalBytesExpectedToWrite) { - uploadData.progress = (double) totalBytesWritten / (double) totalBytesExpectedToWrite; - notifyProgress(); + uploadData.setUploadSize(totalBytesWritten); + notifyProgress(false); } }; final RequestTransaction transaction = createUploadRequestTransaction(); - transaction.uploadPart(true, uploadFileInfo.uploadId, data.index, data.data, progressHandler, new RequestTransaction.RequestCompleteHandler() { + transaction.uploadPart(true, info.uploadId, info.getPartIndexOfData(data), data.data, progressHandler, new RequestTransaction.RequestCompleteHandler() { @Override public void complete(ResponseInfo responseInfo, UploadRegionRequestMetrics requestMetrics, JSONObject response) { - uploadData.data = null; destroyUploadRequestTransaction(transaction); String etag = null; @@ -136,15 +134,12 @@ public void complete(ResponseInfo responseInfo, UploadRegionRequestMetrics reque } } if (responseInfo.isOK() && etag != null && md5 != null) { - uploadData.progress = 1; uploadData.etag = etag; - uploadData.isUploading = false; - uploadData.isCompleted = true; + uploadData.updateState(UploadData.State.Complete); recordUploadInfo(); - notifyProgress(); + notifyProgress(false); } else { - uploadData.isUploading = false; - uploadData.isCompleted = false; + uploadData.updateState(UploadData.State.WaitToUpload); } completeHandler.complete(false, responseInfo, requestMetrics, response); } @@ -153,60 +148,21 @@ public void complete(ResponseInfo responseInfo, UploadRegionRequestMetrics reque @Override void completeUpload(final PartsUploadPerformerCompleteHandler completeHandler) { - final UploadFileInfoPartV2 uploadFileInfo = (UploadFileInfoPartV2) fileInfo; + final UploadInfoV2 info = (UploadInfoV2) uploadInfo; - List> partInfoArray = uploadFileInfo.getPartInfoArray(); + List> partInfoArray = info.getPartInfoArray(); final RequestTransaction transaction = createUploadRequestTransaction(); - transaction.completeParts(true, fileName, uploadFileInfo.uploadId, partInfoArray, new RequestTransaction.RequestCompleteHandler() { + transaction.completeParts(true, fileName, info.uploadId, partInfoArray, new RequestTransaction.RequestCompleteHandler() { @Override public void complete(ResponseInfo responseInfo, UploadRegionRequestMetrics requestMetrics, JSONObject response) { + if (responseInfo.isOK()) { + notifyProgress(true); + } destroyUploadRequestTransaction(transaction); completeHandler.complete(responseInfo, requestMetrics, response); } }); } - - private byte[] getUploadDataWithRetry(UploadData data) { - byte[] uploadData = null; - - int maxTime = 3; - int index = 0; - while (index < maxTime) { - uploadData = getUploadData(data); - if (uploadData != null) { - break; - } - index ++; - } - - return uploadData; - } - - private synchronized byte[] getUploadData(UploadData data) { - if (randomAccessFile == null || data == null) { - return null; - } - - int readSize = 0; - byte[] uploadData = new byte[data.size]; - try { - randomAccessFile.seek(data.offset); - while (readSize < data.size) { - int ret = randomAccessFile.read(uploadData, readSize, data.size - readSize); - if (ret < 0) { - break; - } - readSize += ret; - } - // 读数据非预期 - if (readSize != data.size) { - uploadData = null; - } - } catch (IOException e) { - uploadData = null; - } - return uploadData; - } } diff --git a/library/src/main/java/com/qiniu/android/storage/UpProgress.java b/library/src/main/java/com/qiniu/android/storage/UpProgress.java new file mode 100644 index 000000000..ac9d33a08 --- /dev/null +++ b/library/src/main/java/com/qiniu/android/storage/UpProgress.java @@ -0,0 +1,89 @@ +package com.qiniu.android.storage; + +import com.qiniu.android.utils.AsyncRun; +import com.qiniu.android.utils.LogUtil; + +class UpProgress { + + private volatile long maxProgressUploadBytes = -1; + private volatile long previousUploadBytes = 0; + private final UpProgressHandler handler; + + UpProgress(UpProgressHandler handler) { + this.handler = handler; + } + + public void progress(final String key, long uploadBytes, final long totalBytes) { + if (handler == null || uploadBytes < 0 || (totalBytes > 0 && uploadBytes > totalBytes)) { + return; + } + + if (totalBytes > 0) { + if (this.maxProgressUploadBytes < 0) { + this.maxProgressUploadBytes = (long)(totalBytes * 0.95); + } + + if (uploadBytes > maxProgressUploadBytes) { + return; + } + } + + if (uploadBytes > previousUploadBytes) { + previousUploadBytes = uploadBytes; + } else { + // 不大于之前回调百分比,不再回调 + return; + } + + if (handler instanceof UpProgressBytesHandler) { + final long uploadBytesFinal = uploadBytes; + AsyncRun.runInMain(new Runnable() { + @Override + public void run() { + LogUtil.i("key:" + key + " progress uploadBytes:" + uploadBytesFinal + " totalBytes:" + totalBytes); + ((UpProgressBytesHandler) handler).progress(key, uploadBytesFinal, totalBytes); + } + }); + return; + } + + if (totalBytes < 0) { + return; + } + + final double notifyPercent = (double) uploadBytes / (double) totalBytes; + AsyncRun.runInMain(new Runnable() { + @Override + public void run() { + LogUtil.i("key:" + key + " progress:" + notifyPercent); + handler.progress(key, notifyPercent); + } + }); + } + + public void notifyDone(final String key, final long totalBytes) { + if (handler == null) { + return; + } + + // 不管什么类型 source, 在资源 EOF 时间,均可以获取到文件的大小 + if (handler instanceof UpProgressBytesHandler) { + AsyncRun.runInMain(new Runnable() { + @Override + public void run() { + LogUtil.i("key:" + key + " progress uploadBytes:" + totalBytes + " totalBytes:" + totalBytes); + ((UpProgressBytesHandler) handler).progress(key, totalBytes, totalBytes); + } + }); + return; + } + + AsyncRun.runInMain(new Runnable() { + @Override + public void run() { + LogUtil.i("key:" + key + " progress:1"); + handler.progress(key, 1); + } + }); + } +} diff --git a/library/src/main/java/com/qiniu/android/storage/UpProgressBytesHandler.java b/library/src/main/java/com/qiniu/android/storage/UpProgressBytesHandler.java new file mode 100644 index 000000000..35b2ef445 --- /dev/null +++ b/library/src/main/java/com/qiniu/android/storage/UpProgressBytesHandler.java @@ -0,0 +1,14 @@ +package com.qiniu.android.storage; + +public interface UpProgressBytesHandler extends UpProgressHandler { + /** + * 用户自定义进度处理类必须实现的方法 + * 注: + * 使用此接口,{@link UpProgressHandler#progress(String, double)} 会无效 + * + * @param key 上传文件的保存文件名 + * @param uploadBytes 已上传大小 + * @param totalBytes 总大小;无法获取大小时为 -1 + */ + void progress(String key, long uploadBytes, long totalBytes); +} diff --git a/library/src/main/java/com/qiniu/android/storage/UploadBlock.java b/library/src/main/java/com/qiniu/android/storage/UploadBlock.java index e6175bcc1..930c58835 100644 --- a/library/src/main/java/com/qiniu/android/storage/UploadBlock.java +++ b/library/src/main/java/com/qiniu/android/storage/UploadBlock.java @@ -1,66 +1,58 @@ package com.qiniu.android.storage; import org.json.JSONArray; -import org.json.JSONException; import org.json.JSONObject; import java.util.ArrayList; +import java.util.List; class UploadBlock { final long offset; - final long size; + final int size; final int index; - final ArrayList uploadDataList; + final List uploadDataList; - String context; + String md5 = null; + String ctx = null; - UploadBlock(long offset, long blockSize, int dataSize, int index) { + UploadBlock(long offset, int blockSize, int dataSize, int index) { this.offset = offset; this.size = blockSize; this.index = index; this.uploadDataList = createDataList(dataSize); } - private UploadBlock(long offset, - long blockSize, - int index, - ArrayList uploadDataList) { + private UploadBlock(long offset, int blockSize, int index, List uploadDataList) { this.offset = offset; this.size = blockSize; this.index = index; this.uploadDataList = uploadDataList; } - static UploadBlock blockFromJson(JSONObject jsonObject) { + static UploadBlock blockFromJson(JSONObject jsonObject) throws Exception { if (jsonObject == null) { return null; } - long offset = 0; - long size = 0; - int index = 0; - String context = null; + + long offset = jsonObject.getLong("offset"); + int size = jsonObject.getInt("size"); + int index = jsonObject.getInt("index"); + String md5 = jsonObject.optString("md5"); + String ctx = jsonObject.optString("ctx"); ArrayList uploadDataList = new ArrayList(); - try { - offset = jsonObject.getLong("offset"); - size = jsonObject.getLong("size"); - index = jsonObject.getInt("index"); - context = jsonObject.getString("context"); - JSONArray dataJsonArray = jsonObject.getJSONArray("uploadDataList"); - for (int i = 0; i < dataJsonArray.length(); i++) { - JSONObject dataJson = dataJsonArray.getJSONObject(i); - UploadData data = UploadData.dataFromJson(dataJson); - if (data != null) { - uploadDataList.add(data); - } + JSONArray dataJsonArray = jsonObject.getJSONArray("uploadDataList"); + for (int i = 0; i < dataJsonArray.length(); i++) { + JSONObject dataJson = dataJsonArray.getJSONObject(i); + UploadData data = UploadData.dataFromJson(dataJson); + if (data != null) { + uploadDataList.add(data); } - } catch (JSONException e) { } - ; + UploadBlock block = new UploadBlock(offset, size, index, uploadDataList); - if (context != null && context.length() > 0) { - block.context = context; - } + block.md5 = md5; + block.ctx = ctx; return block; } @@ -70,7 +62,7 @@ boolean isCompleted() { } boolean isCompleted = true; for (UploadData data : uploadDataList) { - if (!data.isCompleted) { + if (!data.isUploaded()) { isCompleted = false; break; } @@ -78,63 +70,65 @@ boolean isCompleted() { return isCompleted; } - double progress() { + long uploadSize() { if (uploadDataList == null) { return 0; } - double progress = 0; + long uploadSize = 0; for (UploadData data : uploadDataList) { - progress += data.progress * ((double) data.size / size); + uploadSize += data.uploadSize(); } - return progress; + return uploadSize; } private ArrayList createDataList(int dataSize) { long offset = 0; - int dataIndex = 1; + int dataIndex = 0; ArrayList datas = new ArrayList(); while (offset < size) { long lastSize = size - offset; int dataSizeP = Math.min((int) lastSize, dataSize); UploadData data = new UploadData(offset, dataSizeP, dataIndex); - if (data != null) { - datas.add(data); - offset += dataSizeP; - dataIndex += 1; - } + datas.add(data); + offset += dataSizeP; + dataIndex += 1; } return datas; } - JSONObject toJsonObject() { + void checkInfoStateAndUpdate() { + for (UploadData data : uploadDataList) { + data.checkStateAndUpdate(); + } + } + + JSONObject toJsonObject() throws Exception { JSONObject jsonObject = new JSONObject(); - try { - jsonObject.put("offset", offset); - jsonObject.put("size", size); - jsonObject.put("index", index); - jsonObject.put("context", (context != null ? context : "")); - if (uploadDataList != null && uploadDataList.size() > 0) { - JSONArray dataJsonArray = new JSONArray(); - for (UploadData data : uploadDataList) { - JSONObject dataJson = data.toJsonObject(); - if (dataJson != null) { - dataJsonArray.put(dataJson); - } + jsonObject.putOpt("offset", offset); + jsonObject.putOpt("size", size); + jsonObject.putOpt("index", index); + jsonObject.putOpt("md5", md5); + jsonObject.putOpt("ctx", ctx); + if (uploadDataList != null && uploadDataList.size() > 0) { + JSONArray dataJsonArray = new JSONArray(); + for (UploadData data : uploadDataList) { + JSONObject dataJson = data.toJsonObject(); + if (dataJson != null) { + dataJsonArray.put(dataJson); } - jsonObject.put("uploadDataList", dataJsonArray); } - } catch (JSONException e) { + jsonObject.put("uploadDataList", dataJsonArray); } return jsonObject; } - protected UploadData nextUploadData() { + protected UploadData nextUploadDataWithoutCheckData() { if (uploadDataList == null || uploadDataList.size() == 0) { return null; } UploadData data = null; for (UploadData dataP : uploadDataList) { - if (!dataP.isCompleted && !dataP.isUploading) { + if (dataP.needToUpload()) { data = dataP; break; } @@ -143,7 +137,8 @@ protected UploadData nextUploadData() { } protected void clearUploadState() { - context = null; + md5 = null; + ctx = null; if (uploadDataList == null || uploadDataList.size() == 0) { return; } diff --git a/library/src/main/java/com/qiniu/android/storage/UploadData.java b/library/src/main/java/com/qiniu/android/storage/UploadData.java index c260b0520..f2e49f5a7 100644 --- a/library/src/main/java/com/qiniu/android/storage/UploadData.java +++ b/library/src/main/java/com/qiniu/android/storage/UploadData.java @@ -1,6 +1,5 @@ package com.qiniu.android.storage; -import org.json.JSONException; import org.json.JSONObject; class UploadData { @@ -9,10 +8,11 @@ class UploadData { final int size; final int index; + String md5; String etag; - boolean isCompleted; - boolean isUploading; - double progress; + + private State state; + private long uploadSize = 0; byte[] data; @@ -20,59 +20,112 @@ class UploadData { this.offset = offset; this.size = size; this.index = index; - this.isCompleted = false; - this.isUploading = false; - this.progress = 0; + this.state = State.NeedToCheck; + this.uploadSize = 0; } - static UploadData dataFromJson(JSONObject jsonObject) { + static UploadData dataFromJson(JSONObject jsonObject) throws Exception { if (jsonObject == null) { return null; } - long offset = 0; - int size = 0; - int index = 0; - String etag = null; - boolean isCompleted = false; - double progress = 0; - try { - offset = jsonObject.getLong("offset"); - size = jsonObject.getInt("size"); - index = jsonObject.getInt("index"); - isCompleted = jsonObject.getBoolean("isCompleted"); - progress = jsonObject.getDouble("progress"); - etag = jsonObject.getString("etag"); - } catch (JSONException ignored) { - } + + long offset = jsonObject.getLong("offset"); + int size = jsonObject.getInt("size"); + int index = jsonObject.getInt("index"); + String etag = jsonObject.optString("etag"); + State state = State.state(jsonObject.getInt("state")); + String md5 = jsonObject.optString("md5"); + UploadData uploadData = new UploadData(offset, size, index); - uploadData.isCompleted = isCompleted; - uploadData.progress = progress; uploadData.etag = etag; + uploadData.md5 = md5; + uploadData.state = state; + uploadData.uploadSize = 0; return uploadData; } - boolean isFirstData() { - return index == 1; + // 需要上传,但需要检测块信息是否有效 + boolean needToUpload() { + switch (state) { + case NeedToCheck: + case WaitToUpload: + return true; + default: + return false; + } + } + + // 是否已经上传 + boolean isUploaded() { + return state == State.Complete; + } + + State getState() { + return state; + } + + void updateState(State state) { + switch (state) { + case NeedToCheck: + case WaitToUpload: + case Uploading: + uploadSize = 0; + etag = null; + break; + case Complete: + data = null; + } + this.state = state; + } + + void setUploadSize(long uploadSize) { + this.uploadSize = uploadSize; + } + + long uploadSize() { + return state == State.Complete ? size : uploadSize; } void clearUploadState() { etag = null; - isCompleted = false; - isUploading = false; + md5 = null; + state = State.WaitToUpload; } - JSONObject toJsonObject() { - JSONObject jsonObject = new JSONObject(); - try { - jsonObject.put("offset", offset); - jsonObject.put("size", size); - jsonObject.put("index", index); - jsonObject.put("isCompleted", isCompleted); - jsonObject.put("progress", progress); - jsonObject.put("etag", etag); - } catch (JSONException e) { - e.printStackTrace(); + void checkStateAndUpdate() { + if ((state == State.WaitToUpload || state == State.Uploading) && data == null) { + state = State.NeedToCheck; } + } + + JSONObject toJsonObject() throws Exception { + JSONObject jsonObject = new JSONObject(); + jsonObject.putOpt("offset", offset); + jsonObject.putOpt("size", size); + jsonObject.putOpt("index", index); + jsonObject.putOpt("etag", etag); + jsonObject.putOpt("md5", md5); + jsonObject.putOpt("state", state.intValue()); return jsonObject; } + + enum State { + NeedToCheck, // 需要检测数据 + WaitToUpload, // 等待上传 + Uploading, // 正在上传 + Complete; // 上传结束 + + private int intValue() { + return this.ordinal(); + } + + private static State state(int value) { + State[] states = State.values(); + if (value < 0 || value >= states.length) { + return NeedToCheck; + } else { + return states[value]; + } + } + } } diff --git a/library/src/main/java/com/qiniu/android/storage/UploadFileInfo.java b/library/src/main/java/com/qiniu/android/storage/UploadFileInfo.java deleted file mode 100644 index e595ebfe7..000000000 --- a/library/src/main/java/com/qiniu/android/storage/UploadFileInfo.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.qiniu.android.storage; - -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -abstract class UploadFileInfo { - - final long size; - final long modifyTime; - - UploadFileInfo(long fileSize, - long modifyTime) { - this.size = fileSize; - this.modifyTime = modifyTime; - } - - static UploadFileInfo fileFromJson(JSONObject jsonObject){ - return null; - } - - double progress() { - return 0; - } - - abstract boolean isEmpty(); - - abstract boolean isValid(); - - abstract void clearUploadState(); - - abstract boolean isAllUploaded(); - - abstract JSONObject toJsonObject(); -} diff --git a/library/src/main/java/com/qiniu/android/storage/UploadFileInfoPartV1.java b/library/src/main/java/com/qiniu/android/storage/UploadFileInfoPartV1.java deleted file mode 100644 index d095af6b3..000000000 --- a/library/src/main/java/com/qiniu/android/storage/UploadFileInfoPartV1.java +++ /dev/null @@ -1,159 +0,0 @@ -package com.qiniu.android.storage; - -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -import java.util.ArrayList; - -class UploadFileInfoPartV1 extends UploadFileInfo { - - final ArrayList uploadBlocks; - - UploadFileInfoPartV1(long size, - long modifyTime, - ArrayList uploadBlocks) { - super(size, modifyTime); - this.uploadBlocks = uploadBlocks; - } - - UploadFileInfoPartV1(long size, int blockSize, int dataSize, long modifyTime) { - super(size, modifyTime); - this.uploadBlocks = createBlocks(blockSize, dataSize); - } - - static UploadFileInfoPartV1 fileFromJson(JSONObject jsonObject) { - if (jsonObject == null) { - return null; - } - long size = 0; - long modifyTime = 0; - ArrayList uploadBlocks = new ArrayList(); - try { - size = jsonObject.getLong("size"); - modifyTime = jsonObject.getLong("modifyTime"); - JSONArray blockJsonArray = jsonObject.getJSONArray("uploadBlocks"); - for (int i = 0; i < blockJsonArray.length(); i++) { - JSONObject blockJson = blockJsonArray.getJSONObject(i); - UploadBlock block = UploadBlock.blockFromJson(blockJson); - if (block != null) { - uploadBlocks.add(block); - } - } - } catch (JSONException e) { - } - - UploadFileInfoPartV1 fileInfo = new UploadFileInfoPartV1(size, modifyTime, uploadBlocks); - return fileInfo; - } - - private ArrayList createBlocks(int blockSize, int dataSize) { - long offset = 0; - int blockIndex = 0; - ArrayList blocks = new ArrayList<>(); - while (offset < size) { - long lastSize = size - offset; - int blockSizeP = Math.min((int)lastSize, blockSize); - UploadBlock block = new UploadBlock(offset, blockSizeP, dataSize, blockIndex); - if (block != null) { - blocks.add(block); - offset += blockSizeP; - blockIndex += 1; - } - } - return blocks; - } - - double progress() { - if (uploadBlocks == null || uploadBlocks.size() == 0) { - return 0; - } - double progress = 0; - for (UploadBlock block : uploadBlocks) { - progress += block.progress() * ((double) block.size / (double) size); - } - return progress; - } - - @Override - boolean isEmpty() { - return uploadBlocks == null || uploadBlocks.size() == 0; - } - - @Override - boolean isValid() { - return !isEmpty(); - } - - UploadBlock nextUploadBlock() { - if (uploadBlocks == null || uploadBlocks.size() == 0) { - return null; - } - UploadBlock block = null; - for (UploadBlock blockP : uploadBlocks) { - UploadData data = blockP.nextUploadData(); - if (data != null) { - block = blockP; - break; - } - } - return block; - } - - void clearUploadState() { - if (uploadBlocks == null || uploadBlocks.size() == 0) { - return; - } - for (UploadBlock block : uploadBlocks) { - block.clearUploadState(); - } - } - - boolean isAllUploaded() { - if (uploadBlocks == null || uploadBlocks.size() == 0) { - return true; - } - boolean isAllUploaded = true; - for (UploadBlock block : uploadBlocks) { - if (!block.isCompleted()) { - isAllUploaded = false; - break; - } - } - return isAllUploaded; - } - - ArrayList allBlocksContexts() { - if (uploadBlocks == null || uploadBlocks.size() == 0) { - return null; - } - ArrayList contexts = new ArrayList(); - for (UploadBlock block : uploadBlocks) { - if (block.context != null) { - contexts.add(block.context); - } - } - return contexts; - } - - JSONObject toJsonObject() { - JSONObject jsonObject = new JSONObject(); - try { - jsonObject.put("size", size); - jsonObject.put("modifyTime", modifyTime); - if (uploadBlocks != null && uploadBlocks.size() > 0) { - JSONArray blockJsonArray = new JSONArray(); - for (UploadBlock block : uploadBlocks) { - JSONObject blockJson = block.toJsonObject(); - if (blockJson != null) { - blockJsonArray.put(blockJson); - } - } - jsonObject.put("uploadBlocks", blockJsonArray); - } - } catch (JSONException e) { - } - return jsonObject; - } -} - diff --git a/library/src/main/java/com/qiniu/android/storage/UploadFileInfoPartV2.java b/library/src/main/java/com/qiniu/android/storage/UploadFileInfoPartV2.java deleted file mode 100644 index 3f4165170..000000000 --- a/library/src/main/java/com/qiniu/android/storage/UploadFileInfoPartV2.java +++ /dev/null @@ -1,173 +0,0 @@ -package com.qiniu.android.storage; - -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -import java.util.ArrayList; -import java.util.Date; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -public class UploadFileInfoPartV2 extends UploadFileInfo { - - final ArrayList uploadDataList; - - String uploadId; - // 单位:秒 - Long expireAt; - - private UploadFileInfoPartV2(long size, - long modifyTime, - ArrayList uploadDataList) { - super(size, modifyTime); - this.uploadDataList = uploadDataList; - } - - UploadFileInfoPartV2(long size, int dataSize, long modifyTime) { - super(size, modifyTime); - this.uploadDataList = createDataList(dataSize); - } - - static UploadFileInfoPartV2 fileFromJson(JSONObject jsonObject) { - if (jsonObject == null) { - return null; - } - long size = 0; - long modifyTime = 0; - Long expireAt = null; - String uploadId = null; - ArrayList uploadDataList = new ArrayList<>(); - try { - size = jsonObject.getLong("size"); - modifyTime = jsonObject.getLong("modifyTime"); - expireAt = jsonObject.getLong("expireAt"); - uploadId = jsonObject.getString("uploadId"); - JSONArray dataJsonArray = jsonObject.getJSONArray("uploadDataList"); - for (int i = 0; i < dataJsonArray.length(); i++) { - JSONObject dataJson = dataJsonArray.getJSONObject(i); - UploadData data = UploadData.dataFromJson(dataJson); - if (data != null) { - uploadDataList.add(data); - } - } - } catch (JSONException e) { - } - - UploadFileInfoPartV2 fileInfo = new UploadFileInfoPartV2(size, modifyTime, uploadDataList); - fileInfo.expireAt = expireAt; - fileInfo.uploadId = uploadId; - return fileInfo; - } - - private ArrayList createDataList(int dataSize) { - long offset = 0; - int dataIndex = 1; - ArrayList dataList = new ArrayList(); - while (offset < size) { - long lastSize = size - offset; - int dataSizeP = Math.min((int)lastSize, dataSize); - UploadData data = new UploadData(offset, dataSizeP, dataIndex); - if (data != null) { - dataList.add(data); - offset += dataSizeP; - dataIndex += 1; - } - } - return dataList; - } - - double progress() { - if (uploadDataList == null) { - return 0; - } - double progress = 0; - for (UploadData data : uploadDataList) { - progress += data.progress * ((double) data.size / size); - } - return progress; - } - - @Override - boolean isEmpty() { - return uploadDataList == null || uploadDataList.size() == 0; - } - - @Override - boolean isValid() { - return !isEmpty() && uploadId != null && (expireAt - new Date().getTime() * 0.001) > 3600*24; - } - - UploadData nextUploadData() { - if (uploadDataList == null || uploadDataList.size() == 0) { - return null; - } - UploadData data = null; - for (UploadData dataP : uploadDataList) { - if (!dataP.isCompleted && !dataP.isUploading) { - data = dataP; - break; - } - } - return data; - } - - void clearUploadState() { - for (UploadData data : uploadDataList) { - data.clearUploadState(); - } - } - - boolean isAllUploaded() { - if (uploadDataList == null || uploadDataList.size() == 0) { - return true; - } - boolean isCompleted = true; - for (UploadData data : uploadDataList) { - if (!data.isCompleted) { - isCompleted = false; - break; - } - } - return isCompleted; - } - - List> getPartInfoArray() { - if (uploadId == null || uploadId.length() == 0) { - return null; - } - ArrayList> infoArray = new ArrayList<>(); - for (UploadData data : uploadDataList) { - if (data.etag != null) { - HashMap info = new HashMap<>(); - info.put("etag", data.etag); - info.put("partNumber", data.index); - infoArray.add(info); - } - } - return infoArray; - } - - JSONObject toJsonObject() { - JSONObject jsonObject = new JSONObject(); - try { - jsonObject.put("size", size); - jsonObject.put("modifyTime", modifyTime); - jsonObject.put("expireAt", expireAt); - jsonObject.put("uploadId", uploadId); - if (uploadDataList != null && uploadDataList.size() > 0) { - JSONArray dataJsonArray = new JSONArray(); - for (UploadData data : uploadDataList) { - JSONObject dataJson = data.toJsonObject(); - if (dataJson != null) { - dataJsonArray.put(dataJson); - } - } - jsonObject.put("uploadDataList", dataJsonArray); - } - } catch (JSONException ignored) { - } - return jsonObject; - } -} diff --git a/library/src/main/java/com/qiniu/android/storage/UploadInfo.java b/library/src/main/java/com/qiniu/android/storage/UploadInfo.java new file mode 100644 index 000000000..4cf676ffd --- /dev/null +++ b/library/src/main/java/com/qiniu/android/storage/UploadInfo.java @@ -0,0 +1,157 @@ +package com.qiniu.android.storage; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.IOException; + +abstract class UploadInfo { + + private String sourceId; + private long sourceSize = UploadSource.UnknownSourceSize; + protected String fileName = null; + + private UploadSource source; + + UploadInfo(UploadSource source) { + this.source = source; + this.sourceSize = source.getSize(); + this.sourceId = source.getId() != null ? source.getId() : ""; + } + + void setInfoFromJson(JSONObject jsonObject) { + try { + sourceSize = jsonObject.getLong("sourceSize"); + sourceId = jsonObject.optString("sourceId"); + } catch (JSONException ignored) { + } + } + + /** + * 是否可以重新加载文件信息,也即是否可以重新读取信息 + * + * @return return + */ + boolean couldReloadSource() { + return source.couldReloadSource(); + } + + /** + * 重新加载文件信息,以便于重新读取 + * + * @return 重新加载是否成功 + */ + boolean reloadSource() { + return source.reloadSource(); + } + + /** + * 是否为同一个 UploadInfo + * 同一个:source 相同,上传方式相同 + */ + boolean isSameUploadInfo(UploadInfo info) { + if (info == null || !sourceId.equals(info.sourceId)) { + return false; + } + + // 检测文件大小,如果能获取到文件大小的话,就进行检测 + if (info.sourceSize > UploadSource.UnknownSourceSize && + sourceSize > UploadSource.UnknownSourceSize && + info.sourceSize != sourceSize) { + return false; + } + + return true; + } + + /** + * 获取资源 id + * + * @return 资源 id + */ + String getSourceId() { + return sourceId; + } + + /** + * 获取资源大小 + * + * @return + */ + long getSourceSize() { + return sourceSize; + } + + /** + * 数据源是否有效,为空则无效 + * + * @return 是否有效 + */ + boolean hasValidResource() { + return source != null; + } + + /** + * 是否有效 + * + * @return 是否有效 + */ + boolean isValid() { + return hasValidResource(); + } + + /** + * 获取已上传数据的大小 + * + * @return 已上传数据的大小 + */ + abstract long uploadSize(); + + /** + * 文件内容是否完全上传完毕 + * + * @return 是否完全上传完毕 + */ + abstract boolean isAllUploaded(); + + /** + * 清楚上传状态 + */ + abstract void clearUploadState(); + + /** + * 检查文件状态, 主要处理没有 data 但处于上传状态 + */ + abstract void checkInfoStateAndUpdate(); + + /** + * 转 json + * + * @return json + */ + JSONObject toJsonObject() { + JSONObject jsonObject = new JSONObject(); + try { + jsonObject.put("sourceId", sourceId); + jsonObject.put("sourceSize", sourceSize); + } catch (JSONException ignore) { + } + return jsonObject; + } + + void close() { + source.close(); + } + + byte[] readData(int dataSize, long dataOffset) throws IOException { + if (source == null) { + throw new IOException("file is not exist"); + } + + byte[] data = source.readData(dataSize, dataOffset); + if (data.length != dataSize || data.length == 0) { + sourceSize = dataOffset + data.length; + } + return data; + } +} diff --git a/library/src/main/java/com/qiniu/android/storage/UploadInfoV1.java b/library/src/main/java/com/qiniu/android/storage/UploadInfoV1.java new file mode 100644 index 000000000..a29748114 --- /dev/null +++ b/library/src/main/java/com/qiniu/android/storage/UploadInfoV1.java @@ -0,0 +1,333 @@ +package com.qiniu.android.storage; + +import com.qiniu.android.utils.BytesUtils; +import com.qiniu.android.utils.MD5; +import com.qiniu.android.utils.StringUtils; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +class UploadInfoV1 extends UploadInfo { + + private static final String TypeKey = "infoType"; + private static final String TypeValue = "UploadInfoV1"; + private static final int BlockSize = 4 * 1024 * 1024; + + private final int dataSize; + private List blockList; + + private boolean isEOF = false; + private IOException readException = null; + + private UploadInfoV1(UploadSource source, int dataSize, List blockList) { + super(source); + this.dataSize = dataSize; + this.blockList = blockList; + } + + UploadInfoV1(UploadSource source, Configuration configuration) { + super(source); + if (configuration.useConcurrentResumeUpload || configuration.chunkSize > BlockSize) { + this.dataSize = BlockSize; + } else { + this.dataSize = configuration.chunkSize; + } + this.blockList = new ArrayList<>(); + } + + static UploadInfoV1 infoFromJson(UploadSource source, JSONObject jsonObject) { + if (jsonObject == null) { + return null; + } + + int dataSize = 0; + String type = null; + List blockList = new ArrayList<>(); + try { + type = jsonObject.optString(TypeKey); + dataSize = jsonObject.getInt("dataSize"); + JSONArray blockJsonArray = jsonObject.getJSONArray("blockList"); + for (int i = 0; i < blockJsonArray.length(); i++) { + JSONObject blockJson = blockJsonArray.getJSONObject(i); + try { + UploadBlock block = UploadBlock.blockFromJson(blockJson); + if (block != null) { + blockList.add(block); + } + } catch (Exception ignore) { + break; + } + } + } catch (JSONException ignored) { + return null; + } + + UploadInfoV1 info = new UploadInfoV1(source, dataSize, blockList); + info.setInfoFromJson(jsonObject); + if (!TypeValue.equals(type) || !source.getId().equals(info.getSourceId())) { + return null; + } + + return info; + } + + boolean isFirstData(UploadData data) { + return data.index == 0; + } + + @Override + boolean reloadSource() { + isEOF = false; + readException = null; + return super.reloadSource(); + } + + @Override + boolean isSameUploadInfo(UploadInfo info) { + if (!super.isSameUploadInfo(info)) { + return false; + } + + if (!(info instanceof UploadInfoV1)) { + return false; + } + + UploadInfoV1 infoV1 = (UploadInfoV1) info; + return dataSize == infoV1.dataSize; + } + + @Override + void clearUploadState() { + if (blockList == null || blockList.size() == 0) { + return; + } + for (UploadBlock block : blockList) { + block.clearUploadState(); + } + } + + @Override + long uploadSize() { + if (blockList == null || blockList.size() == 0) { + return 0; + } + long uploadSize = 0; + for (UploadBlock block : blockList) { + uploadSize += block.uploadSize(); + } + return uploadSize; + } + + // 文件已经读取结束 & 所有块均上传 + @Override + boolean isAllUploaded() { + if (!isEOF) { + return false; + } + + if (blockList == null || blockList.size() == 0) { + return true; + } + boolean isAllUploaded = true; + for (UploadBlock block : blockList) { + if (!block.isCompleted()) { + isAllUploaded = false; + break; + } + } + return isAllUploaded; + } + + @Override + void checkInfoStateAndUpdate() { + for (UploadBlock block : blockList) { + block.checkInfoStateAndUpdate(); + } + } + + @Override + JSONObject toJsonObject() { + JSONObject jsonObject = super.toJsonObject(); + if (jsonObject == null) { + return null; + } + try { + jsonObject.put(TypeKey, TypeValue); + jsonObject.put("dataSize", dataSize); + if (blockList != null && blockList.size() > 0) { + JSONArray blockJsonArray = new JSONArray(); + for (UploadBlock block : blockList) { + JSONObject blockJson = block.toJsonObject(); + if (blockJson != null) { + blockJsonArray.put(blockJson); + } + } + jsonObject.put("blockList", blockJsonArray); + } + } catch (Exception e) { + return null; + } + return jsonObject; + } + + UploadBlock nextUploadBlock() throws IOException { + + // 从 blockList 中读取需要上传的 block + UploadBlock block = nextUploadBlockFormBlockList(); + + // 内存的 blockList 中没有可上传的数据,则从资源中读并创建 block + if (block == null) { + if (isEOF) { + return null; + } else if (readException != null) { + // 资源读取异常,不可读取 + throw readException; + } + + // 从资源中读取新的 block 进行上传 + long blockOffset = 0; + if (blockList.size() > 0) { + UploadBlock lastBlock = blockList.get(blockList.size() - 1); + blockOffset = lastBlock.offset + lastBlock.size; + } + block = new UploadBlock(blockOffset, BlockSize, dataSize, blockList.size()); + } + + UploadBlock loadBlock = null; + try { + loadBlock = loadBlockData(block); + } catch (IOException e) { + readException = e; + throw e; + } + + if (loadBlock == null) { + // 没有加在到 block, 也即数据源读取结束 + isEOF = true; + // 有多余的 block 则移除,移除中包含 block + if (blockList.size() > block.index) { + blockList = blockList.subList(0, block.index); + } + } else { + // 加在到 block + if (loadBlock.index == blockList.size()) { + // 新块:block index 等于 blockList size 则为新创建 block,需要加入 blockList + blockList.add(loadBlock); + } else if (loadBlock != block) { + // 更换块:重新加在了 block, 更换信息 + blockList.set(loadBlock.index, loadBlock); + } + + // 数据源读取结束,块读取大小小于预期,读取结束 + if (loadBlock.size < BlockSize) { + isEOF = true; + // 有多余的 block 则移除,移除中不包含 block + if (blockList.size() > block.index + 1) { + blockList = blockList.subList(0, block.index + 1); + } + } + } + + return loadBlock; + } + + private UploadBlock nextUploadBlockFormBlockList() { + if (blockList == null || blockList.size() == 0) { + return null; + } + UploadBlock block = null; + for (UploadBlock blockP : blockList) { + UploadData data = blockP.nextUploadDataWithoutCheckData(); + if (data != null) { + block = blockP; + break; + } + } + return block; + } + + + // 加载块中的数据 + // 1. 数据块已加载,直接返回 + // 2. 数据块未加载,读块数据 + // 2.1 如果未读到数据,则已 EOF,返回 null + // 2.2 如果读到数据 + // 2.2.1 如果块数据符合预期,则当片未上传,则加载片数据 + // 2.2.2 如果块数据不符合预期,创建新块,加载片信息 + private UploadBlock loadBlockData(UploadBlock block) throws IOException { + if (block == null) { + return null; + } + + // 已经加载过 block 数据 + // 没有需要上传的片 或者 有需要上传片但是已加载过片数据 + UploadData nextUploadData = block.nextUploadDataWithoutCheckData(); + if (nextUploadData.getState() == UploadData.State.WaitToUpload) { + return block; + } + + // 未加载过 block 数据 + // 根据 block 信息加载 blockBytes + byte[] blockBytes = null; + try { + blockBytes = readData(block.size, block.offset); + } catch (IOException e) { + throw e; + } + + // 没有数据不需要上传 + if (blockBytes == null || blockBytes.length == 0) { + return null; + } + + String md5 = MD5.encrypt(blockBytes); + // 判断当前 block 的数据是否和实际数据吻合,不吻合则之前 block 被抛弃,重新创建 block + if (blockBytes.length != block.size || block.md5 == null || !block.md5.equals(md5)) { + block = new UploadBlock(block.offset, blockBytes.length, dataSize, block.index); + block.md5 = md5; + } + + for (UploadData data : block.uploadDataList) { + if (data.getState() != UploadData.State.Complete) { + // 还未上传的 + try { + data.data = BytesUtils.subBytes(blockBytes, (int) data.offset, data.size); + data.updateState(UploadData.State.WaitToUpload); + } catch (IOException e) { + throw e; + } + } else { + // 已经上传的 + data.updateState(UploadData.State.Complete); + } + } + + return block; + } + + UploadData nextUploadData(UploadBlock block) throws IOException { + if (block == null) { + return null; + } + return block.nextUploadDataWithoutCheckData(); + } + + ArrayList allBlocksContexts() { + if (blockList == null || blockList.size() == 0) { + return null; + } + ArrayList contexts = new ArrayList(); + for (UploadBlock block : blockList) { + String ctx = block.ctx; + if (!StringUtils.isNullOrEmpty(ctx)) { + contexts.add(ctx); + } + } + return contexts; + } +} diff --git a/library/src/main/java/com/qiniu/android/storage/UploadInfoV2.java b/library/src/main/java/com/qiniu/android/storage/UploadInfoV2.java new file mode 100644 index 000000000..6e9de6fe7 --- /dev/null +++ b/library/src/main/java/com/qiniu/android/storage/UploadInfoV2.java @@ -0,0 +1,333 @@ +package com.qiniu.android.storage; + +import com.qiniu.android.utils.MD5; +import com.qiniu.android.utils.StringUtils; + +import org.json.JSONArray; +import org.json.JSONObject; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +class UploadInfoV2 extends UploadInfo { + private final static String TypeKey = "infoType"; + private final static String TypeValue = "UploadInfoV2"; + private final static int maxDataSize = 1024 * 1024 * 1024; + + private final int dataSize; + private List dataList; + + private boolean isEOF = false; + private IOException readException = null; + + String uploadId; + // 单位:秒 + Long expireAt; + + private UploadInfoV2(UploadSource source, int dataSize, List dataList) { + super(source); + this.dataSize = dataSize; + this.dataList = dataList; + } + + UploadInfoV2(UploadSource source, Configuration configuration) { + super(source); + this.dataSize = Math.min(configuration.chunkSize, maxDataSize); + this.dataList = new ArrayList<>(); + } + + static UploadInfoV2 infoFromJson(UploadSource source, JSONObject jsonObject) { + if (jsonObject == null) { + return null; + } + + int dataSize = 0; + String type = null; + Long expireAt = null; + String uploadId = null; + List dataList = new ArrayList<>(); + try { + type = jsonObject.optString(TypeKey); + dataSize = jsonObject.getInt("dataSize"); + expireAt = jsonObject.getLong("expireAt"); + uploadId = jsonObject.optString("uploadId"); + JSONArray dataJsonArray = jsonObject.getJSONArray("dataList"); + for (int i = 0; i < dataJsonArray.length(); i++) { + JSONObject dataJson = dataJsonArray.getJSONObject(i); + UploadData data = UploadData.dataFromJson(dataJson); + if (data != null) { + dataList.add(data); + } + } + } catch (Exception e) { + return null; + } + + UploadInfoV2 info = new UploadInfoV2(source, dataSize, dataList); + info.setInfoFromJson(jsonObject); + info.expireAt = expireAt; + info.uploadId = uploadId; + + if (!TypeValue.equals(type) || !source.getId().equals(info.getSourceId())) { + return null; + } + + return info; + } + + int getPartIndexOfData(UploadData data) { + return data.index + 1; // 片的 index, 从 1 开始 + } + + UploadData nextUploadData() throws IOException { + + // 从 dataList 中读取需要上传的 data + UploadData data = nextUploadDataFormDataList(); + + // 内存的 blockList 中没有可上传的数据,则从资源中读并创建 block + if (data == null) { + if (isEOF) { + return null; + } else if (readException != null) { + // 资源读取异常,不可读取 + throw readException; + } + + // 从资源中读取新的 data 进行上传 + long dataOffset = 0; + if (dataList.size() > 0) { + UploadData lastData = dataList.get(dataList.size() - 1); + dataOffset = lastData.offset + lastData.size; + } + int dataIndex = dataList.size(); + data = new UploadData(dataOffset, dataSize, dataIndex); + } + + UploadData loadData = null; + try { + loadData = loadData(data); + } catch (IOException e) { + readException = e; + throw e; + } + + if (loadData == null) { + // 没有加在到 data, 也即数据源读取结束 + isEOF = true; + // 有多余的 data 则移除,移除中包含 data + if (dataList.size() > data.index) { + dataList = dataList.subList(0, data.index); + } + } else { + // 加在到 data + if (loadData.index == dataList.size()) { + // 新块:data index 等于 dataList size 则为新创建 block,需要加入 dataList + dataList.add(loadData); + } else if (loadData != data) { + // 更换块:重新加在了 data, 更换信息 + dataList.set(loadData.index, loadData); + } + + // 数据源读取结束,块读取大小小于预期,读取结束 + if (loadData.size < data.size) { + isEOF = true; + // 有多余的 block 则移除,移除中不包含 block + if (dataList.size() > data.index + 1) { + dataList = dataList.subList(0, data.index + 1); + } + } + } + + return loadData; + } + + private UploadData nextUploadDataFormDataList() { + if (dataList == null || dataList.size() == 0) { + return null; + } + UploadData data = null; + for (UploadData dataP : dataList) { + if (dataP.needToUpload()) { + data = dataP; + break; + } + } + return data; + } + + // 加载片中的数据 + // 1. 数据片已加载,直接返回 + // 2. 数据块未加载,读块数据 + // 2.1 如果未读到数据,则已 EOF,返回 null + // 2.2 如果块读到数据 + // 2.2.1 如果块数据符合预期,当片未上传,则加载片数据 + // 2.2.2 如果块数据不符合预期,创建新块,加载片信息 + private UploadData loadData(UploadData data) throws IOException { + if (data == null) { + return null; + } + + // 之前已加载并验证过数据,不必在验证 + if (data.data != null) { + return data; + } + + // 根据 data 信息加载 dataBytes + byte[] dataBytes = null; + try { + dataBytes = readData(data.size, data.offset); + } catch (IOException e) { + readException = e; + throw e; + } + + // 没有数据不需要上传 + if (dataBytes == null || dataBytes.length == 0) { + return null; + } + + String md5 = MD5.encrypt(dataBytes); + // 判断当前 block 的数据是否和实际数据吻合,不吻合则之前 block 被抛弃,重新创建 block + if (dataBytes.length != data.size || data.md5 == null || !data.md5.equals(md5)) { + data = new UploadData(data.offset, dataBytes.length, data.index); + data.md5 = md5; + } + + if (StringUtils.isNullOrEmpty(data.etag)) { + data.data = dataBytes; + data.updateState(UploadData.State.WaitToUpload); + } else { + data.updateState(UploadData.State.Complete); + } + + return data; + } + + List> getPartInfoArray() { + if (uploadId == null || uploadId.length() == 0) { + return null; + } + ArrayList> infoArray = new ArrayList<>(); + for (UploadData data : dataList) { + if (data.getState() == UploadData.State.Complete && !StringUtils.isNullOrEmpty(data.etag)) { + HashMap info = new HashMap<>(); + info.put("etag", data.etag); + info.put("partNumber", getPartIndexOfData(data)); + infoArray.add(info); + } + } + return infoArray; + } + + @Override + boolean reloadSource() { + isEOF = false; + readException = null; + return super.reloadSource(); + } + + @Override + boolean isSameUploadInfo(UploadInfo info) { + if (!super.isSameUploadInfo(info)) { + return false; + } + + if (!(info instanceof UploadInfoV2)) { + return false; + } + + UploadInfoV2 infoV2 = (UploadInfoV2) info; + return dataSize == infoV2.dataSize; + } + + @Override + void clearUploadState() { + for (UploadData data : dataList) { + data.clearUploadState(); + } + } + + @Override + long uploadSize() { + if (dataList == null || dataList.size() == 0) { + return 0; + } + long uploadSize = 0; + for (UploadData data : dataList) { + uploadSize += data.uploadSize(); + } + return uploadSize; + } + + @Override + boolean isValid() { + if (!super.isValid()) { + return false; + } + + if (StringUtils.isNullOrEmpty(uploadId) || expireAt == null) { + return false; + } + + long timestamp = new Date().getTime() / 1000; + return expireAt > (timestamp - 3600 * 24 * 2); + } + + // 文件已经读取结束 & 所有片均上传 + @Override + boolean isAllUploaded() { + if (!isEOF) { + return false; + } + + if (dataList == null || dataList.size() == 0) { + return true; + } + boolean isCompleted = true; + for (UploadData data : dataList) { + if (!data.isUploaded()) { + isCompleted = false; + break; + } + } + return isCompleted; + } + + @Override + void checkInfoStateAndUpdate() { + for (UploadData data : dataList) { + data.checkStateAndUpdate(); + } + } + + @Override + JSONObject toJsonObject() { + JSONObject jsonObject = super.toJsonObject(); + if (jsonObject == null) { + return null; + } + try { + jsonObject.put(TypeKey, TypeValue); + jsonObject.put("dataSize", dataSize); + jsonObject.put("expireAt", expireAt); + jsonObject.put("uploadId", uploadId); + if (dataList != null && dataList.size() > 0) { + JSONArray dataJsonArray = new JSONArray(); + for (UploadData data : dataList) { + JSONObject dataJson = data.toJsonObject(); + if (dataJson != null) { + dataJsonArray.put(dataJson); + } + } + jsonObject.put("dataList", dataJsonArray); + } + } catch (Exception ignored) { + return null; + } + return jsonObject; + } +} diff --git a/library/src/main/java/com/qiniu/android/storage/UploadManager.java b/library/src/main/java/com/qiniu/android/storage/UploadManager.java index 8454ac6b0..40400e878 100644 --- a/library/src/main/java/com/qiniu/android/storage/UploadManager.java +++ b/library/src/main/java/com/qiniu/android/storage/UploadManager.java @@ -1,11 +1,15 @@ package com.qiniu.android.storage; +import android.content.ContentResolver; +import android.net.Uri; + import com.qiniu.android.collect.ReportItem; import com.qiniu.android.collect.UploadInfoReporter; import com.qiniu.android.http.ResponseInfo; import com.qiniu.android.http.dns.DnsPrefetchTransaction; import com.qiniu.android.http.metrics.UploadTaskMetrics; import com.qiniu.android.utils.AsyncRun; +import com.qiniu.android.utils.ContextGetter; import com.qiniu.android.utils.Utils; import com.qiniu.android.utils.Wait; @@ -14,6 +18,7 @@ import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; +import java.io.InputStream; import java.io.RandomAccessFile; import java.util.ArrayList; @@ -61,7 +66,7 @@ public void put(final byte[] data, final String token, final UpCompletionHandler complete, final UploadOptions options) { - if (checkAndNotifyError(key, token, data, complete)){ + if (checkAndNotifyError(key, token, data, complete)) { return; } putData(data, null, key, token, options, complete); @@ -81,7 +86,7 @@ public void put(String filePath, String token, UpCompletionHandler completionHandler, final UploadOptions options) { - if (checkAndNotifyError(key, token, filePath, completionHandler)){ + if (checkAndNotifyError(key, token, filePath, completionHandler)) { return; } put(new File(filePath), key, token, completionHandler, options); @@ -91,21 +96,75 @@ public void put(String filePath, /** * 上传文件 * - * @param file 上传的文件对象 - * @param key 上传文件保存的文件名 - * @param token 上传凭证 + * @param file 上传的文件对象 + * @param key 上传文件保存的文件名 + * @param token 上传凭证 * @param completionHandler 上传完成的后续处理动作 - * @param options 上传数据的可选参数 + * @param options 上传数据的可选参数 */ public void put(final File file, final String key, final String token, final UpCompletionHandler completionHandler, final UploadOptions options) { - if (checkAndNotifyError(key, token, file, completionHandler)){ + if (checkAndNotifyError(key, token, file, completionHandler)) { + return; + } + putSource(new UploadSourceFile(file), key, token, options, completionHandler); + } + + /** + * 上传文件 + * + * @param uri 上传的文件对象 Uri + * @param resolver resolver, 在根据 Uri 构建 InputStream 时使用 + * 注:为 null 时,使用 {@link ContextGetter#applicationContext()} 获取 resolver + * @param key 上传文件保存的文件名 + * @param token 上传凭证 + * @param completionHandler 上传完成的后续处理动作 + * @param options 上传数据的可选参数 + */ + public void put(final Uri uri, + final ContentResolver resolver, + final String key, + final String token, + final UpCompletionHandler completionHandler, + final UploadOptions options) { + if (checkAndNotifyError(key, token, uri, completionHandler)) { + return; + } + putSource(new UploadSourceUri(uri, resolver), key, token, options, completionHandler); + } + + /** + * 上传文件 + * + * @param inputStream 上传的资源流 + * 注:资源流需要在上传结束后自行关闭,SDK 内部不做关闭操作 + * @param id 资源 id, 作为构建断点续传信息保存的 key, 如果为空则使用 fileName + * @param size 上传资源的大小,不知道大小,配置 -1 + * @param fileName 上传资源流的文件名 + * @param key 上传资源保存的文件名 + * @param token 上传凭证 + * @param completionHandler 上传完成的后续处理动作 + * @param options 上传数据的可选参数 + */ + public void put(final InputStream inputStream, + final String id, + final long size, + final String fileName, + final String key, + final String token, + final UpCompletionHandler completionHandler, + final UploadOptions options) { + if (checkAndNotifyError(key, token, inputStream, completionHandler)) { return; } - putFile(file, key, token, options, completionHandler); + UploadSourceStream stream = new UploadSourceStream(inputStream); + stream.setId(id); + stream.setSize(size); + stream.setFileName(fileName); + putSource(stream, key, token, options, completionHandler); } /** @@ -136,19 +195,33 @@ public void complete(String key, ResponseInfo info, JSONObject response) { } }; - if (!checkAndNotifyError(key, token, data, completionHandler)){ + if (!checkAndNotifyError(key, token, data, completionHandler)) { putData(data, null, key, token, options, completionHandler); } wait.startWait(); - if (responseInfos.size() > 0){ + if (responseInfos.size() > 0) { return responseInfos.get(0); } else { return null; } } + /** + * 同步上传文件。使用 form 表单方式上传,建议只在文件较小情况下使用此方式,如 file.size() < 1024 * 1024。 + * 注:切勿在主线程调用 + * + * @param file 上传的文件绝对路径 + * @param key 上传数据保存的文件名 + * @param token 上传凭证 + * @param options 上传数据的可选参数 + * @return 响应信息 ResponseInfo#response 响应体,序列化后 json 格式 + */ + public ResponseInfo syncPut(String file, String key, String token, UploadOptions options) { + return syncPut(new File(file), key, token, options); + } + /** * 同步上传文件。使用 form 表单方式上传,建议只在文件较小情况下使用此方式,如 file.size() < 1024 * 1024。 * 注:切勿在主线程调用 @@ -159,13 +232,76 @@ public void complete(String key, ResponseInfo info, JSONObject response) { * @param options 上传数据的可选参数 * @return 响应信息 ResponseInfo#response 响应体,序列化后 json 格式 */ - public ResponseInfo syncPut(File file, + public ResponseInfo syncPut(File file, String key, String token, UploadOptions options) { + return syncPut(new UploadSourceFile(file), key, token, options); + } + + /** + * 同步上传文件。使用 form 表单方式上传,建议只在文件较小情况下使用此方式,如 file.size() < 1024 * 1024。 + * 注:切勿在主线程调用 + * + * @param uri 上传的文件对象 Uri + * @param resolver resolver, 在根据 Uri 构建 InputStream 时使用 + * 注:为 null 时,使用 {@link ContextGetter#applicationContext()} 获取 resolver + * @param key 上传数据保存的文件名 + * @param token 上传凭证 + * @param options 上传数据的可选参数 + * @return 响应信息 ResponseInfo#response 响应体,序列化后 json 格式 + */ + public ResponseInfo syncPut(Uri uri, + ContentResolver resolver, String key, String token, UploadOptions options) { - final Wait wait = new Wait(); + return syncPut(new UploadSourceUri(uri, resolver), key, token, options); + } + /** + * 同步上传文件。使用 form 表单方式上传,建议只在文件较小情况下使用此方式,如 file.size() < 1024 * 1024。 + * 注:切勿在主线程调用 + * + * @param inputStream 上传的资源流 + * 注:资源流需要在上传结束后自行关闭,SDK 内部不做关闭操作 + * @param id 资源 id, 作为构建断点续传信息保存的 key, 如果为空则使用 fileName + * @param size 上传资源的大小,不知道大小,配置 -1 + * @param fileName 上传资源流的文件名 + * @param key 上传数据保存的文件名 + * @param token 上传凭证 + * @param options 上传数据的可选参数 + * @return 响应信息 ResponseInfo#response 响应体,序列化后 json 格式 + */ + public ResponseInfo syncPut(InputStream inputStream, + String id, + long size, + String fileName, + String key, + String token, + UploadOptions options) { + + UploadSourceStream stream = new UploadSourceStream(inputStream); + stream.setId(id); + stream.setSize(size); + stream.setFileName(fileName); + return syncPut(new UploadSourceStream(inputStream), key, token, options); + } + + /** + * 同步上传文件。使用 form 表单方式上传,建议只在文件较小情况下使用此方式,如 file.size() < 1024 * 1024。 + * 注:切勿在主线程调用 + * + * @param source 上传的文件对象 + * @param key 上传数据保存的文件名 + * @param token 上传凭证 + * @param options 上传数据的可选参数 + * @return 响应信息 ResponseInfo#response 响应体,序列化后 json 格式 + */ + private ResponseInfo syncPut(UploadSource source, + String key, + String token, + UploadOptions options) { + + final Wait wait = new Wait(); final ArrayList responseInfos = new ArrayList(); UpCompletionHandler completionHandler = new UpCompletionHandler() { @Override @@ -177,40 +313,25 @@ public void complete(String key, ResponseInfo info, JSONObject response) { } }; - if (!checkAndNotifyError(key, token, file, completionHandler)){ - putFile(file, key, token, options, completionHandler); + if (!checkAndNotifyError(key, token, source, completionHandler)) { + putSource(source, key, token, options, completionHandler); } wait.startWait(); - if (responseInfos.size() > 0){ + if (responseInfos.size() > 0) { return responseInfos.get(0); } else { return null; } } - /** - * 同步上传文件。使用 form 表单方式上传,建议只在文件较小情况下使用此方式,如 file.size() < 1024 * 1024。 - * 注:切勿在主线程调用 - * - * @param file 上传的文件绝对路径 - * @param key 上传数据保存的文件名 - * @param token 上传凭证 - * @param options 上传数据的可选参数 - * @return 响应信息 ResponseInfo#response 响应体,序列化后 json 格式 - */ - public ResponseInfo syncPut(String file, String key, String token, UploadOptions options) { - return syncPut(new File(file), key, token, options); - } - - private void putData(final byte[] data, final String fileName, final String key, final String token, final UploadOptions option, - final UpCompletionHandler completionHandler){ + final UpCompletionHandler completionHandler) { final UpToken t = UpToken.parse(token); if (t == null || !t.isValid()) { @@ -231,11 +352,11 @@ public void complete(ResponseInfo responseInfo, String key, UploadTaskMetrics re AsyncRun.runInBack(up); } - private void putFile(final File file, - final String key, - final String token, - final UploadOptions option, - final UpCompletionHandler completionHandler){ + private void putSource(final UploadSource source, + final String key, + final String token, + final UploadOptions option, + final UpCompletionHandler completionHandler) { final UpToken t = UpToken.parse(token); if (t == null || !t.isValid()) { @@ -246,26 +367,18 @@ private void putFile(final File file, DnsPrefetchTransaction.addDnsCheckAndPrefetchTransaction(config.zone, t); - if (file.length() <= config.putThreshold) { + if (source.getSize() > 0 && source.getSize() <= config.putThreshold) { ResponseInfo errorInfo = null; - byte[] data = new byte[(int) file.length()]; - RandomAccessFile randomAccessFile = null; + byte[] data = null; try { - randomAccessFile = new RandomAccessFile(file, "r"); - randomAccessFile.read(data); - } catch (FileNotFoundException e) { - errorInfo = ResponseInfo.localIOError("get upload file data error"); + data = source.readData((int) source.getSize(), 0); } catch (IOException e) { - errorInfo = ResponseInfo.localIOError("get upload file data error"); + errorInfo = ResponseInfo.localIOError("get upload file data error:" + e.getMessage()); } finally { - if (randomAccessFile != null){ - try { - randomAccessFile.close(); - } catch (IOException ignored){} - } + source.close(); } - if (errorInfo == null){ - putData(data, file.getName(), key, token, option, completionHandler); + if (errorInfo == null) { + putData(data, source.getFileName(), key, token, option, completionHandler); } else { completeAction(token, key, errorInfo, null, null, completionHandler); } @@ -274,7 +387,7 @@ private void putFile(final File file, String recorderKey = key; if (config.recorder != null && config.keyGen != null) { - recorderKey = config.keyGen.gen(key, file); + recorderKey = config.keyGen.gen(key, source.getId()); } BaseUpload.UpTaskCompletionHandler completionHandlerP = new BaseUpload.UpTaskCompletionHandler() { @@ -284,10 +397,10 @@ public void complete(ResponseInfo responseInfo, String key, UploadTaskMetrics re } }; if (config.useConcurrentResumeUpload) { - final ConcurrentResumeUpload up = new ConcurrentResumeUpload(file, key, t, option, config, config.recorder, recorderKey, completionHandlerP); + final ConcurrentResumeUpload up = new ConcurrentResumeUpload(source, key, t, option, config, config.recorder, recorderKey, completionHandlerP); AsyncRun.runInBack(up); } else { - final PartsUpload up = new PartsUpload(file, key, t, option, config, config.recorder, recorderKey, completionHandlerP); + final PartsUpload up = new PartsUpload(source, key, t, option, config, config.recorder, recorderKey, completionHandlerP); AsyncRun.runInBack(up); } } @@ -295,22 +408,24 @@ public void complete(ResponseInfo responseInfo, String key, UploadTaskMetrics re private boolean checkAndNotifyError(String key, String token, Object input, - UpCompletionHandler completionHandler){ - if (completionHandler == null){ + UpCompletionHandler completionHandler) { + if (completionHandler == null) { throw new NullPointerException("complete handler is null"); } ResponseInfo responseInfo = null; - if (input == null){ + if (input == null) { responseInfo = ResponseInfo.zeroSize("no input data"); - } else if (input instanceof byte[] && ((byte[])input).length == 0){ + } else if (input instanceof byte[] && ((byte[]) input).length == 0) { responseInfo = ResponseInfo.zeroSize("no input data"); - } else if (input instanceof File && ((File)input).length() == 0){ + } else if (input instanceof File && ((File) input).length() == 0) { + responseInfo = ResponseInfo.zeroSize("file is empty"); + } else if (input instanceof UploadSource && ((UploadSource) input).getSize() == 0) { responseInfo = ResponseInfo.zeroSize("file is empty"); - } else if (token == null || token.length() == 0){ + } else if (token == null || token.length() == 0) { responseInfo = ResponseInfo.invalidToken("no token"); } - if (responseInfo != null){ + if (responseInfo != null) { completeAction(token, key, responseInfo, responseInfo.response, null, completionHandler); return true; } else { @@ -323,10 +438,10 @@ private void completeAction(final String token, final ResponseInfo responseInfo, final JSONObject response, final UploadTaskMetrics taskMetrics, - final UpCompletionHandler completionHandler){ + final UpCompletionHandler completionHandler) { reportQuality(key, responseInfo, taskMetrics, token); - if (completionHandler != null){ + if (completionHandler != null) { final Wait wait = new Wait(); AsyncRun.runInMain(new Runnable() { @Override @@ -342,7 +457,7 @@ public void run() { private void reportQuality(String key, ResponseInfo responseInfo, UploadTaskMetrics taskMetrics, - String token){ + String token) { UpToken upToken = UpToken.parse(token); if (upToken == null || !upToken.isValid()) { @@ -353,7 +468,7 @@ private void reportQuality(String key, ReportItem item = new ReportItem(); item.setReport(ReportItem.LogTypeQuality, ReportItem.QualityKeyLogType); - item.setReport((Utils.currentTimestamp()/1000), ReportItem.QualityKeyUpTime); + item.setReport((Utils.currentTimestamp() / 1000), ReportItem.QualityKeyUpTime); item.setReport(ReportItem.qualityResult(responseInfo), ReportItem.QualityKeyResult); item.setReport(key, ReportItem.QualityKeyTargetKey); item.setReport(upToken.bucket, ReportItem.QualityKeyTargetBucket); @@ -369,7 +484,7 @@ private void reportQuality(String key, String errorType = ReportItem.requestReportErrorType(responseInfo); item.setReport(errorType, ReportItem.QualityKeyErrorType); - if (responseInfo != null && errorType != null){ + if (responseInfo != null && errorType != null) { String errorDesc = responseInfo.error != null ? responseInfo.error : responseInfo.message; item.setReport(errorDesc, ReportItem.QualityKeyErrorDescription); } diff --git a/library/src/main/java/com/qiniu/android/storage/UploadSource.java b/library/src/main/java/com/qiniu/android/storage/UploadSource.java new file mode 100644 index 000000000..b91400757 --- /dev/null +++ b/library/src/main/java/com/qiniu/android/storage/UploadSource.java @@ -0,0 +1,70 @@ +package com.qiniu.android.storage; + +import java.io.IOException; + +interface UploadSource { + /** + * 未知大小 + */ + long UnknownSourceSize = -1; + + /** + * 获取资源唯一标识 + * 作为断点续传时判断是否为同一资源的依据之一; + * 如果两个资源的 record key 和 资源唯一标识均相同则认为资源为同一资源,断点续传才会生效 + * 注: + * 同一资源的数据必须完全相同,否则上传可能会出现异常 + * + * @return 资源修改时间 + */ + String getId(); + + /** + * 是否可以重新加载文件信息,也即是否可以重新读取信息 + * @return return + */ + boolean couldReloadSource(); + + /** + * 重新加载文件信息,以便于重新读取 + * + * @return 重新加载是否成功 + */ + boolean reloadSource(); + + /** + * 获取资源文件名 + * @return 资源文件名 + */ + String getFileName(); + + /** + * 获取资源大小 + * 无法获取大小时返回 -1 + * 作用: + * 1. 验证资源是否为同一资源 + * 2. 计算上传进度 + * + * @return 资源大小 + */ + long getSize(); + + /** + * 读取数据 + * 1. 返回 byte[] 可能为空,但不会为 null; + * 2. 当 byte[] 大小和 dataSize 不同时,则源数据已经读取结束 + * 3. 读取异常时抛出 IOException + * 4. 仅支持串行调用,且 dataOffset 依次递增 + * + * @param dataSize 数据大小 + * @param dataOffset 数据偏移量 + * @return 数据 + * @throws IOException 异常 + */ + byte[] readData(int dataSize, long dataOffset) throws IOException; + + /** + * 关闭流 + */ + void close(); +} diff --git a/library/src/main/java/com/qiniu/android/storage/UploadSourceFile.java b/library/src/main/java/com/qiniu/android/storage/UploadSourceFile.java new file mode 100644 index 000000000..c1fc02861 --- /dev/null +++ b/library/src/main/java/com/qiniu/android/storage/UploadSourceFile.java @@ -0,0 +1,96 @@ +package com.qiniu.android.storage; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.RandomAccessFile; + +class UploadSourceFile implements UploadSource { + + private Exception readException = null; + private final File file; + private final RandomAccessFile randomAccessFile; + + UploadSourceFile(File file) { + this.file = file; + RandomAccessFile randomAccessFile = null; + try { + randomAccessFile = new RandomAccessFile(file, "r"); + } catch (Exception e) { + readException = e; + } + this.randomAccessFile = randomAccessFile; + } + + @Override + public String getId() { + return getFileName() + "_" + file.lastModified(); + } + + @Override + public boolean couldReloadSource() { + return randomAccessFile != null; + } + + @Override + public boolean reloadSource() { + return true; + } + + @Override + public String getFileName() { + return file.getName(); + } + + @Override + public long getSize() { + return file.length(); + } + + @Override + public byte[] readData(int dataSize, long dataOffset) throws IOException { + if (randomAccessFile == null) { + if (readException != null) { + throw new IOException(readException); + } else { + throw new IOException("file is invalid"); + } + } + + int readSize = 0; + byte[] buffer = new byte[dataSize]; + try { + randomAccessFile.seek(dataOffset); + while (readSize < dataSize) { + int ret = randomAccessFile.read(buffer, readSize, (dataSize - readSize)); + if (ret < 0) { + break; + } + readSize += ret; + } + + if (readSize < dataSize) { + byte[] newBuffer = new byte[readSize]; + System.arraycopy(buffer, 0, newBuffer, 0, readSize); + buffer = newBuffer; + } + } catch (IOException e) { + throw new IOException(e.getMessage()); + } + return buffer; + } + + @Override + public void close() { + if (randomAccessFile != null) { + try { + randomAccessFile.close(); + } catch (IOException e) { + try { + randomAccessFile.close(); + } catch (IOException ignored) { + } + } + } + } +} diff --git a/library/src/main/java/com/qiniu/android/storage/UploadSourceStream.java b/library/src/main/java/com/qiniu/android/storage/UploadSourceStream.java new file mode 100644 index 000000000..d02a6e2a6 --- /dev/null +++ b/library/src/main/java/com/qiniu/android/storage/UploadSourceStream.java @@ -0,0 +1,117 @@ +package com.qiniu.android.storage; + +import com.qiniu.android.utils.StringUtils; +import java.io.IOException; +import java.io.InputStream; + +class UploadSourceStream implements UploadSource { + + private long readOffset = 0; + + private InputStream inputStream; + private String id; + private long size = UploadSource.UnknownSourceSize; + private String fileName; + + UploadSourceStream(InputStream inputStream) { + this.inputStream = inputStream; + } + + protected InputStream getInputStream() { + return inputStream; + } + + protected void setInputStream(InputStream inputStream) { + this.inputStream = inputStream; + } + + + void setId(String id) { + this.id = id; + } + + @Override + public String getId() { + return !StringUtils.isNullOrEmpty(id) ? id : fileName; + } + + public void setFileName(String fileName) { + this.fileName = fileName; + } + + @Override + public String getFileName() { + return fileName; + } + + @Override + public long getSize() { + if (size > UnknownSourceSize) { + return size; + } else { + return UnknownSourceSize; + } + } + + public void setSize(long size) { + this.size = size; + } + + @Override + public boolean couldReloadSource() { + return false; + } + + @Override + public boolean reloadSource() { + readOffset = 0; + return false; + } + + @Override + public byte[] readData(int dataSize, long dataOffset) throws IOException { + if (inputStream == null) { + throw new IOException("inputStream is empty"); + } + + byte[] buffer = null; + synchronized (this) { + boolean isEOF = false; + while (true) { + if (readOffset == dataOffset) { + int readSize = 0; + buffer = new byte[dataSize]; + while (readSize < dataSize) { + int ret = inputStream.read(buffer, readSize, dataSize - readSize); + if (ret < 0) { + isEOF = true; + break; + } + readSize += ret; + } + + if (readSize < dataSize) { + byte[] newBuffer = new byte[readSize]; + System.arraycopy(buffer, 0, newBuffer, 0, readSize); + buffer = newBuffer; + } + + readOffset += readSize; + if (isEOF) { + size = readOffset; + } + break; + } else if (readOffset < dataOffset) { + readOffset += inputStream.skip(dataOffset - readOffset); + } else { + throw new IOException("read stream data error"); + } + } + } + return buffer; + } + + @Override + public void close() { + } +} diff --git a/library/src/main/java/com/qiniu/android/storage/UploadSourceUri.java b/library/src/main/java/com/qiniu/android/storage/UploadSourceUri.java new file mode 100644 index 000000000..edcb8ab53 --- /dev/null +++ b/library/src/main/java/com/qiniu/android/storage/UploadSourceUri.java @@ -0,0 +1,189 @@ +package com.qiniu.android.storage; + +import android.content.ContentResolver; +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.provider.MediaStore; + +import com.qiniu.android.utils.ContextGetter; +import com.qiniu.android.utils.StringUtils; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; + +class UploadSourceUri extends UploadSourceStream { + + private Exception readException = null; + + private final Uri uri; + private ContentResolver resolver; + private String modifyDate = ""; + + UploadSourceUri(Uri uri, ContentResolver resolver) { + super(null); + this.uri = uri; + this.resolver = resolver; + + reloadSource(); + loadFileInfo(); + } + + @Override + public String getId() { + return getFileName() + "_" + modifyDate; + } + + @Override + public boolean couldReloadSource() { + return uri != null && !StringUtils.isNullOrEmpty(uri.getScheme()); + } + + @Override + public boolean reloadSource() { + super.reloadSource(); + close(); + readException = null; + + InputStream inputStream = null; + try { + inputStream = createInputStream(); + setInputStream(inputStream); + } catch (Exception e) { + readException = e; + } + return inputStream != null; + } + + @Override + public byte[] readData(int dataSize, long dataOffset) throws IOException { + if (readException != null) { + throw new IOException(readException); + } + + return super.readData(dataSize, dataOffset); + } + + @Override + public void close() { + InputStream inputStream = getInputStream(); + if (inputStream != null) { + try { + inputStream.close(); + } catch (IOException e) { + try { + inputStream.close(); + } catch (IOException ignored) { + } + } + } + } + + private InputStream createInputStream() throws Exception { + if (uri == null) { + return null; + } + + ContentResolver resolver = getContextResolver(); + if (resolver == null) { + return null; + } + + InputStream inputStream = null; + try { + inputStream = resolver.openInputStream(uri); + } catch (Exception e) { + throw e; + } + return inputStream; + } + + private void loadFileInfo() { + if (uri == null) { + return; + } + + if ("file".equals(uri.getScheme())) { + tryLoadFileInfoByPath(); + } else { + tryLoadFileInfoByCursor(); + } + } + + private void tryLoadFileInfoByPath() { + if (uri.getPath() != null) { + File file = new File(uri.getPath()); + if (file.exists() && file.isFile()) { + setFileName(file.getName()); + setSize(file.length()); + modifyDate = file.lastModified() + ""; + } + } + } + + private void tryLoadFileInfoByCursor() { + ContentResolver resolver = getContextResolver(); + if (resolver == null) { + return; + } + + Cursor cursor = null; + try { + cursor = resolver.query(uri, null, null, null, null, null); + } catch (Exception e) { + e.printStackTrace(); + } + + if (cursor == null) { + return; + } + + try { + if (cursor.moveToFirst()) { + int dataIndex = cursor.getColumnIndex(MediaStore.Images.Media.DATA); + if (!cursor.isNull(dataIndex)) { + String path = cursor.getString(dataIndex); + if (path != null) { + File file = new File(path); + setSize(file.length()); + setFileName(file.getName()); + modifyDate = file.lastModified() / 1000 + ""; + return; + } + } + + int sizeIndex = cursor.getColumnIndex(MediaStore.Images.Media.SIZE); + if (!cursor.isNull(sizeIndex)) { + long size = cursor.getLong(sizeIndex); + setSize(size); + } + + int fileNameIndex = cursor.getColumnIndex(MediaStore.Images.Media.DISPLAY_NAME); + if (!cursor.isNull(fileNameIndex)) { + setFileName(cursor.getString(fileNameIndex)); + } + + int modifyDateIndex = cursor.getColumnIndex(MediaStore.Images.Media.DATE_MODIFIED); + if (!cursor.isNull(modifyDateIndex)) { + modifyDate = cursor.getString(modifyDateIndex); + } + } + } finally { + cursor.close(); + } + } + + private ContentResolver getContextResolver() { + if (resolver != null) { + return resolver; + } + + Context context = ContextGetter.applicationContext(); + if (context != null) { + resolver = context.getContentResolver(); + } + + return resolver; + } +} diff --git a/library/src/main/java/com/qiniu/android/utils/BytesUtils.java b/library/src/main/java/com/qiniu/android/utils/BytesUtils.java new file mode 100644 index 000000000..20fbc3496 --- /dev/null +++ b/library/src/main/java/com/qiniu/android/utils/BytesUtils.java @@ -0,0 +1,15 @@ +package com.qiniu.android.utils; + +import java.io.IOException; + +public class BytesUtils { + public static byte[] subBytes(byte[] source, int from, int length) throws IOException { + if (length + from > source.length) { + throw new IOException("copy bytes out of range"); + } + + byte[] buffer = new byte[length]; + System.arraycopy(source, from, buffer, 0, length); + return buffer; + } +} diff --git a/library/src/main/java/com/qiniu/android/utils/GroupTaskThread.java b/library/src/main/java/com/qiniu/android/utils/GroupTaskThread.java index 73ccc8627..525475bf3 100644 --- a/library/src/main/java/com/qiniu/android/utils/GroupTaskThread.java +++ b/library/src/main/java/com/qiniu/android/utils/GroupTaskThread.java @@ -17,11 +17,14 @@ public void run() { super.run(); while (!isInterrupted()){ + boolean isAllTasksCompleted = false; synchronized (this) { - if (isAllTasksCompleted()) { - completeAction(); - break; - } + isAllTasksCompleted = isAllTasksCompleted(); + } + + if (isAllTasksCompleted) { + completeAction(); + break; } GroupTask task = getNextWaitingTask(); diff --git a/library/src/main/java/com/qiniu/android/utils/IPAddressUtil.java b/library/src/main/java/com/qiniu/android/utils/IPAddressUtil.java new file mode 100644 index 000000000..4d3f46968 --- /dev/null +++ b/library/src/main/java/com/qiniu/android/utils/IPAddressUtil.java @@ -0,0 +1,262 @@ +package com.qiniu.android.utils; + +public class IPAddressUtil { + private final static int INADDR4SZ = 4; + private final static int INADDR16SZ = 16; + private final static int INT16SZ = 2; + + /* + * Converts IPv4 address in its textual presentation form + * into its numeric binary form. + * + * @param src a String representing an IPv4 address in standard format + * @return a byte array representing the IPv4 numeric address + */ + @SuppressWarnings("fallthrough") + public static byte[] textToNumericFormatV4(String src) + { + byte[] res = new byte[INADDR4SZ]; + + long tmpValue = 0; + int currByte = 0; + + int len = src.length(); + if (len == 0 || len > 15) { + return null; + } + /* + * When only one part is given, the value is stored directly in + * the network address without any byte rearrangement. + * + * When a two part address is supplied, the last part is + * interpreted as a 24-bit quantity and placed in the right + * most three bytes of the network address. This makes the + * two part address format convenient for specifying Class A + * network addresses as net.host. + * + * When a three part address is specified, the last part is + * interpreted as a 16-bit quantity and placed in the right + * most two bytes of the network address. This makes the + * three part address format convenient for specifying + * Class B net- work addresses as 128.net.host. + * + * When four parts are specified, each is interpreted as a + * byte of data and assigned, from left to right, to the + * four bytes of an IPv4 address. + * + * We determine and parse the leading parts, if any, as single + * byte values in one pass directly into the resulting byte[], + * then the remainder is treated as a 8-to-32-bit entity and + * translated into the remaining bytes in the array. + */ + for (int i = 0; i < len; i++) { + char c = src.charAt(i); + if (c == '.') { + if (tmpValue < 0 || tmpValue > 0xff || currByte == 3) { + return null; + } + res[currByte++] = (byte) (tmpValue & 0xff); + tmpValue = 0; + } else { + int digit = Character.digit(c, 10); + if (digit < 0) { + return null; + } + tmpValue *= 10; + tmpValue += digit; + } + } + if (tmpValue < 0 || tmpValue >= (1L << ((4 - currByte) * 8))) { + return null; + } + switch (currByte) { + case 0: + res[0] = (byte) ((tmpValue >> 24) & 0xff); + case 1: + res[1] = (byte) ((tmpValue >> 16) & 0xff); + case 2: + res[2] = (byte) ((tmpValue >> 8) & 0xff); + case 3: + res[3] = (byte) ((tmpValue >> 0) & 0xff); + } + return res; + } + + /* + * Convert IPv6 presentation level address to network order binary form. + * credit: + * Converted from C code from Solaris 8 (inet_pton) + * + * Any component of the string following a per-cent % is ignored. + * + * @param src a String representing an IPv6 address in textual format + * @return a byte array representing the IPv6 numeric address + */ + public static byte[] textToNumericFormatV6(String src) + { + // Shortest valid string is "::", hence at least 2 chars + if (src.length() < 2) { + return null; + } + + int colonp; + char ch; + boolean saw_xdigit; + int val; + char[] srcb = src.toCharArray(); + byte[] dst = new byte[INADDR16SZ]; + + int srcb_length = srcb.length; + int pc = src.indexOf ("%"); + if (pc == srcb_length -1) { + return null; + } + + if (pc != -1) { + srcb_length = pc; + } + + colonp = -1; + int i = 0, j = 0; + /* Leading :: requires some special handling. */ + if (srcb[i] == ':') + if (srcb[++i] != ':') + return null; + int curtok = i; + saw_xdigit = false; + val = 0; + while (i < srcb_length) { + ch = srcb[i++]; + int chval = Character.digit(ch, 16); + if (chval != -1) { + val <<= 4; + val |= chval; + if (val > 0xffff) + return null; + saw_xdigit = true; + continue; + } + if (ch == ':') { + curtok = i; + if (!saw_xdigit) { + if (colonp != -1) + return null; + colonp = j; + continue; + } else if (i == srcb_length) { + return null; + } + if (j + INT16SZ > INADDR16SZ) + return null; + dst[j++] = (byte) ((val >> 8) & 0xff); + dst[j++] = (byte) (val & 0xff); + saw_xdigit = false; + val = 0; + continue; + } + if (ch == '.' && ((j + INADDR4SZ) <= INADDR16SZ)) { + String ia4 = src.substring(curtok, srcb_length); + /* check this IPv4 address has 3 dots, ie. A.B.C.D */ + int dot_count = 0, index=0; + while ((index = ia4.indexOf ('.', index)) != -1) { + dot_count ++; + index ++; + } + if (dot_count != 3) { + return null; + } + byte[] v4addr = textToNumericFormatV4(ia4); + if (v4addr == null) { + return null; + } + for (int k = 0; k < INADDR4SZ; k++) { + dst[j++] = v4addr[k]; + } + saw_xdigit = false; + break; /* '\0' was seen by inet_pton4(). */ + } + return null; + } + if (saw_xdigit) { + if (j + INT16SZ > INADDR16SZ) + return null; + dst[j++] = (byte) ((val >> 8) & 0xff); + dst[j++] = (byte) (val & 0xff); + } + + if (colonp != -1) { + int n = j - colonp; + + if (j == INADDR16SZ) + return null; + for (i = 1; i <= n; i++) { + dst[INADDR16SZ - i] = dst[colonp + n - i]; + dst[colonp + n - i] = 0; + } + j = INADDR16SZ; + } + if (j != INADDR16SZ) + return null; + byte[] newdst = convertFromIPv4MappedAddress(dst); + if (newdst != null) { + return newdst; + } else { + return dst; + } + } + + /** + * @param src a String representing an IPv4 address in textual format + * @return a boolean indicating whether src is an IPv4 literal address + */ + public static boolean isIPv4LiteralAddress(String src) { + return textToNumericFormatV4(src) != null; + } + + /** + * @param src a String representing an IPv6 address in textual format + * @return a boolean indicating whether src is an IPv6 literal address + */ + public static boolean isIPv6LiteralAddress(String src) { + return textToNumericFormatV6(src) != null; + } + + /* + * Convert IPv4-Mapped address to IPv4 address. Both input and + * returned value are in network order binary form. + * + * @param src a String representing an IPv4-Mapped address in textual format + * @return a byte array representing the IPv4 numeric address + */ + public static byte[] convertFromIPv4MappedAddress(byte[] addr) { + if (isIPv4MappedAddress(addr)) { + byte[] newAddr = new byte[INADDR4SZ]; + System.arraycopy(addr, 12, newAddr, 0, INADDR4SZ); + return newAddr; + } + return null; + } + + /** + * Utility routine to check if the InetAddress is an + * IPv4 mapped IPv6 address. + * + * @return a boolean indicating if the InetAddress is + * an IPv4 mapped IPv6 address; or false if address is IPv4 address. + */ + private static boolean isIPv4MappedAddress(byte[] addr) { + if (addr.length < INADDR16SZ) { + return false; + } + if ((addr[0] == 0x00) && (addr[1] == 0x00) && + (addr[2] == 0x00) && (addr[3] == 0x00) && + (addr[4] == 0x00) && (addr[5] == 0x00) && + (addr[6] == 0x00) && (addr[7] == 0x00) && + (addr[8] == 0x00) && (addr[9] == 0x00) && + (addr[10] == (byte)0xff) && + (addr[11] == (byte)0xff)) { + return true; + } + return false; + } +} diff --git a/library/src/main/java/com/qiniu/android/utils/Utils.java b/library/src/main/java/com/qiniu/android/utils/Utils.java index b85293562..962b1834a 100644 --- a/library/src/main/java/com/qiniu/android/utils/Utils.java +++ b/library/src/main/java/com/qiniu/android/utils/Utils.java @@ -98,6 +98,14 @@ public static String getIpType(String ip, String host) { return type; } + public static boolean isIpv6(String ip) { + if (StringUtils.isNullOrEmpty(ip)) { + return false; + } + + return IPAddressUtil.isIPv6LiteralAddress(ip); + } + private static String getIPV4StringType(String ipv4String, String host) { if (host == null) { host = "";