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}

數據返回正常

之後應該會再寫一個使用相冊拍照的功能。

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