Android Q:上传图片java.io.FileNotFoundException: open failed: EACCES (Permission denied)

从相册选择图片上传,框架使用的是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}

数据返回正常

之后应该会再写一个使用相册拍照的功能。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章