從相冊選擇圖片上傳,框架使用的是Rx + Retrofit + OKHttp。因爲此版本使用了MVVM架構,targetSdkVersion升到了29。在上傳圖片出現了報錯
java.io.FileNotFoundException: /storage/emulated/0/DCIM/Camera/IMG20200608195140.jpg: open failed: EACCES (Permission denied)
原因是在android10開始,Google修改了文件相關權限,對於寫入和讀取文件,都有了新的一套機制,介紹內容網上很多,各大廠商也有介紹,在此不做贅述。
此解決方法適用於能獲取到文件的Uri,比如打開相冊選擇圖片,那麼它的onActivityResult中的Intent data就有選擇的圖片的Uri。
打開圖庫的方法不變:
/**
* 選擇照片
*/
public static void pickPhoto(Activity activity) {
Intent intent;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
intent = new Intent(
Intent.ACTION_PICK,
MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
} else {
intent = new Intent(
Intent.ACTION_GET_CONTENT);
intent.setType("image/*");
}
activity.startActivityForResult(intent, 2001);
}
和onActivityResult回調:
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (data == null) {
return;
}
if (resultCode == Activity.RESULT_OK) {
switch (requestCode) {
case 2001:
Uri originalUri = data.getData();
String path = UriUtil.getPath(this, originalUri);
if (path != null && new File(path).exists()) {
File mFile = new File(path);
viewModel.updateImg(mFile);
}
break;
}
}
}
先附上Android10之前我的圖片上傳
//從上文中獲取了文件
public static List<MultipartBody.Part> getFileBody(File file) {
MultipartBody.Builder builder = new MultipartBody.Builder()
.setType(MultipartBody.FORM);//表單類型
RequestBody body = RequestBody.create(MediaType.parse("multipart/form-data"), file);//表單類型
builder.addFormDataPart("file", file.getName(), body);
return builder.build().parts();
}
//Retrofit Api 接口
/**
* 文件上傳
*/
@Multipart
@POST("xxxx")
Observable<ObjResponse<ShowImageTypeData>> updateFile(@Part List<MultipartBody.Part> partLis);
//調用接口
public Observable<ShowImageTypeData> updateImg(File file) {
return api.updateFile(getFileBody(file))
.compose(new ObjTransform<>(null));
}
大同小異,主要就是獲取文件File類型實例,然後構建RequestBody,使用Retrofit上傳
Android Q不行了,會報錯,那麼主要就是使用到file的地方要修改,先看看RequestBody.create還能帶什麼參數:
public abstract class RequestBody {
/** Returns the Content-Type header for this body. */
public abstract MediaType contentType();
/**
* Returns the number of bytes that will be written to {@code out} in a call to {@link #writeTo},
* or -1 if that count is unknown.
*/
public long contentLength() throws IOException {
return -1;
}
/** Writes the content of this request to {@code out}. */
public abstract void writeTo(BufferedSink sink) throws IOException;
/**
* Returns a new request body that transmits {@code content}. If {@code contentType} is non-null
* and lacks a charset, this will use UTF-8.
*/
public static RequestBody create(MediaType contentType, String content) {
Charset charset = Util.UTF_8;
if (contentType != null) {
charset = contentType.charset();
if (charset == null) {
charset = Util.UTF_8;
contentType = MediaType.parse(contentType + "; charset=utf-8");
}
}
byte[] bytes = content.getBytes(charset);
return create(contentType, bytes);
}
/** Returns a new request body that transmits {@code content}. */
public static RequestBody create(final MediaType contentType, final ByteString content) {
return new RequestBody() {
@Override public MediaType contentType() {
return contentType;
}
@Override public long contentLength() throws IOException {
return content.size();
}
@Override public void writeTo(BufferedSink sink) throws IOException {
sink.write(content);
}
};
}
/** Returns a new request body that transmits {@code content}. */
public static RequestBody create(final MediaType contentType, final byte[] content) {
return create(contentType, content, 0, content.length);
}
/** Returns a new request body that transmits {@code content}. */
public static RequestBody create(final MediaType contentType, final byte[] content,
final int offset, final int byteCount) {
if (content == null) throw new NullPointerException("content == null");
Util.checkOffsetAndCount(content.length, offset, byteCount);
return new RequestBody() {
@Override public MediaType contentType() {
return contentType;
}
@Override public long contentLength() {
return byteCount;
}
@Override public void writeTo(BufferedSink sink) throws IOException {
sink.write(content, offset, byteCount);
}
};
}
/** Returns a new request body that transmits the content of {@code file}. */
public static RequestBody create(final MediaType contentType, final File file) {
if (file == null) throw new NullPointerException("content == null");
return new RequestBody() {
@Override public MediaType contentType() {
return contentType;
}
@Override public long contentLength() {
return file.length();
}
@Override public void writeTo(BufferedSink sink) throws IOException {
Source source = null;
try {
source = Okio.source(file);
sink.writeAll(source);
} finally {
Util.closeQuietly(source);
}
}
};
}
}
從上文可以看出除了一個file。其他大部分都是使用RequestBody create(final MediaType contentType, final byte[] content, final int offset, final int byteCount) 那麼我們也嘗試使用byte[]作爲上傳參數,現在就需要把文件轉爲byte[]。
直接上代碼,從onActivityResult開始:
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (data == null) {
return;
}
if (resultCode == Activity.RESULT_OK) {
switch (requestCode) {
case 2001:
Uri originalUri = data.getData();
try {
ParcelFileDescriptor parcelFileDescriptor = getContentResolver().openFileDescriptor(originalUri, "r");
FileDescriptor fileDescriptor = parcelFileDescriptor.getFileDescriptor();
FileInputStream fis = new FileInputStream(fileDescriptor);
ByteArrayOutputStream swapStream = new ByteArrayOutputStream();
byte[] buff = new byte[1024*4]; //buff用於存放循環讀取的臨時數據
int rc = 0;
while ((rc = fis.read(buff, 0, 100)) > 0) {
swapStream.write(buff, 0, rc);
}
byte[] in_b = swapStream.toByteArray(); //in_b爲轉換之後的結果
viewModel.updateImg(in_b);
} catch (IOException e) {
e.printStackTrace();
}
break;
}
}
}
因爲我分了好幾個文件,包括這些代碼也只是暫時寫在onActivityResult裏,你們只需要看,獲取到關鍵數據後怎麼做,然後自己組裝一下,應該容易的。在獲取到byte[]後,使用byte[]構建RequestBody:
{
底下那個“androidImg”是錯的,這個是設置文件名稱,假如說你填寫了這個的話,那麼就無法識別文件類型,後臺存儲的是一個無類型的文件,你再下載的話無法打開,只用圖片上傳以及使用Glide框架顯示的,沒這個問題,給Glide點個贊!
public static List<MultipartBody.Part> getFileBody(byte[] file) {
MultipartBody.Builder builder = new MultipartBody.Builder()
.setType(MultipartBody.FORM);//表單類型
RequestBody body = RequestBody.create(MediaType.parse("multipart/form-data"), file);//表單類型
builder.addFormDataPart("file", "androidImg", body);
return builder.build().parts();
}
}
重新把構建List<MultipartBody.Part>方法改一下,增加一個文件名,這個文件名稱只需要最後有個後綴就行了,前面帶文件路徑也沒關係,這是PostMan調用文件上傳接口時的數據:
POST /api/file-application/upload HTTP/1.1
Host: 192.168.16.36:8079
Authorization: Bearer xxxx
cache-control: no-cache
Postman-Token: 2ff47803-aadb-4aeb-90d4-cfe342cbf78e
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="file"; filename="C:\Users\xx\Desktop\QQ截圖20190301164159.png
------WebKitFormBoundary7MA4YWxkTrZu0gW--
說明一下,這個filename="xxx" 這個xxx就是剛剛填寫的androidImg
那麼文件名獲取,也是使用Uri獲取文件名:
/**
* 獲取對應uri的path
*
* @param context
* @param uri
* @return
*/
@SuppressLint("NewApi")
public static String getPath(final Context context, final Uri uri) {
final boolean isKitKat = Build.VERSION.SDK_INT >= 19;
// DocumentProvider
if (isKitKat && DocumentsContract.isDocumentUri(context, uri)) {
// ExternalStorageProvider
if (isExternalStorageDocument(uri)) {
final String docId = DocumentsContract.getDocumentId(uri);
final String[] split = docId.split(":");
final String type = split[0];
if ("primary".equalsIgnoreCase(type)) {
return Environment.getExternalStorageDirectory() + "/" + split[1];
}
// handle non-primary volumes
}
// DownloadsProvider
else if (isDownloadsDocument(uri)) {
final String id = DocumentsContract.getDocumentId(uri);
final Uri contentUri = ContentUris.withAppendedId(
Uri.parse("content://downloads/public_downloads"), Long.parseLong(id));
return getDataColumn(context, contentUri, null, null);
}
// MediaProvider
else if (isMediaDocument(uri)) {
final String docId = DocumentsContract.getDocumentId(uri);
final String[] split = docId.split(":");
final String type = split[0];
Uri contentUri = null;
if ("image".equals(type)) {
contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
} else if ("video".equals(type)) {
contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
} else if ("audio".equals(type)) {
contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
}
final String selection = "_id=?";
final String[] selectionArgs = new String[]{
split[1]
};
return getDataColumn(context, contentUri, selection, selectionArgs);
}
}
// MediaStore (and general)
else if ("content".equalsIgnoreCase(uri.getScheme())) {
// Return the remote address
if (isGooglePhotosUri(uri))
return uri.getLastPathSegment();
return getDataColumn(context, uri, null, null);
}
// File
else if ("file".equalsIgnoreCase(uri.getScheme())) {
return uri.getPath();
}
return null;
}
那麼再來構建List<MultipartBody.Part>:
public static List<MultipartBody.Part> getFileBody(byte[] file, String fileName) {
MultipartBody.Builder builder = new MultipartBody.Builder()
.setType(MultipartBody.FORM);//表單類型
RequestBody body = RequestBody.create(MediaType.parse("multipart/form-data"), file);//表單類型
builder.addFormDataPart("file", fileName, body);
return builder.build().parts();
}
把上面獲取到的文件路徑傳入這個方法中,之後使用Retrofit上沒有差別因爲接口傳入參數類型沒變
api.updateFile(AppUtils.getFileBody(file, fileName))
.compose(new ObjTransform<>(null));
附上失敗和成功的log:
使用file傳文件
--> POST http://192.168.16.36:8079/xxxxx http/1.1
Content-Type: multipart/form-data; boundary=36717d28-648b-44b2-a904-07b23fa5fdb8
Content-Length: 115873
Cache-Control: no-cache
Authorization: Bearer eyJhbGciOiJIUzUxMiJ9xxxxxx
Host: 192.168.16.36:8079
Connection: Keep-Alive
Accept-Encoding: gzip
User-Agent: okhttp/3.6.0
--> END POST
<-- HTTP FAILED: java.io.FileNotFoundException: /storage/emulated/0/tieba/FCF10754FF8F4014D623FF8F8B937C1D.jpg: open failed: EACCES (Permission denied)
使用byte[]傳文件:
--> POST http://192.168.16.36:8079/xxxxx http/1.1
Content-Type: multipart/form-data; boundary=36717d28-648b-44b2-a904-07b23fa5fdb8
Content-Length: 115873
Cache-Control: no-cache
Authorization: Bearer eyJhbGciOiJIUzUxMiJ9xxxxxx
Host: 192.168.16.36:8079
Connection: Keep-Alive
Accept-Encoding: gzip
User-Agent: okhttp/3.6.0
--> END POST
<-- 200 http://192.168.16.36:8079/xxxxx (213ms)
Server: nginx/1.11.6
Date: Wed, 10 Jun 2020 03:38:10 GMT
Content-Type: application/json
Transfer-Encoding: chunked
Connection: keep-alive
<-- END HTTP
{"code":1,"subCode":null,"message":"成功","data":xxxxx}
數據返回正常
之後應該會再寫一個使用相冊拍照的功能。