+1

FileProvider và FileUriExposedException trong Android Nougat 24

Android Nougat Troubles

Như anh em đã biết thì Android N đã ra mắt khá lâu, nhiệm vụ của anh em lập trình viên đó là kéo bản SDK mới nhất về (24) và set targetSdkVersion = 24 rồi kiếm 1 em đã update Nougat hoặc dùng máy ảo để cảm nhận những thay đổi mới nhất. Tuy nhiên đến đây có những vấn đề phát sinh xảy ra khi có những đoạn code không còn phù hợp tại bản api mới nữa và việc đầu tiên chúng ta nên làm đó là chạy lại một lượt app để đảm bảo rằng mọi chức năng đều ổn định. Nhưng nếu bạn là người không may khi app bị crash hay không thực hiện đúng chức năng thì hãy bình tĩnh, check log + google thần trưởng + hi vọng có thằng nào đó trên quả đất này đã tìm được giải pháp cho vấn đề bạn đang phải đối mặt.

Hôm nay mình gặp phải vấn đề là khi share ảnh qua Intent, và đây là những kí ức ùa về từ khi mới học Android:

Intent shareIntent = new Intent(Intent.ACTION_SEND);
    shareIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT);
    shareIntent.setType("image/*");
    Uri uriImage = Uri.fromFile(new File(imagePath));
    shareIntent.putExtra(Intent.EXTRA_STREAM, uriImage);
    startActivity(shareIntent);

Mọi chuyện tốt đẹp với đoạn code trên đã lùi về quá khứ, giờ thì khi thực hiện đoạn code này với targetSdkVersion = 24 trong build.gradle app sẽ crash và FileUriExposedException . Nếu chúng ta debug hoặc log ra đường dẫn của uriImage thì nó sẽ có dạng file://image_path . Và nguyên nhân của exception trên đó chính là do tiền tố file://.

Tại sao Android N lại không cho phép chuyển các Uri có tiền tố file:// giữa các app thông qua Intent nữa?

Có thể bạn sẽ khó chịu và phàn nàn tại sao Google lại làm vậy, nó khiến app của bạn chết bất đắc kỳ tử mà rõ ràng trước đây nó luôn hoàn thành tốt nhiệm vụ. Tất nhiên việc gì cũng có nguyên do của nó, thực sự đó hoàn toàn là một lý do tốt và cần thiết. Đó là vì** bảo mật**.

Nếu như đường dẫn file của bạn được gửi như trước đấy cho các app khác (trong trường hợp trên là các app muốn nhận ảnh), các app nhận file sẽ có toàn quyền truy cập file, nếu như file đó là file cá nhân của app bạn thì việc toàn quyền truy cập từ một app khác là điều vô cùng nguy hiểm. Bạn chỉ có thể hi vọng rằng sẽ không có điều gì tồi tệ xảy ra với đứa con của mình.

Chính về thế mà ở bản Android mới này, Google đã quyết định không cho phép điều đó nữa và ép tất cả các lập trình viên phải chọn một phương pháp thích hợp hơn.

Giải pháp

Nếu như file:// không được cho phép thì giải pháp là chúng ta sẽ gửi Uri với định dạng content:// thay vì định dạng của ContentProvider. Thực chất của giải pháp này là chia sẻ quyền truy cập tạm thời file cho các app khác, thêm nữa là chúng ta sẽ cung cấp một đường dẫn Uri "ảo" chứ hoàn toàn không phải là đường dẫn Uri thực sự mà chúng ta đang sử dụng. Để thực hiện điều này chúng ta cần phải có FileProvider. Ta định nghĩa provider ở trong file AndroidManifest như sau:

<manifest>
...
<application>
    ...
    <provider
        android:name="android.support.v4.content.FileProvider"
        android:authorities="<your_application_id>.fileprovider"
        android:exported="false"
        android:grantUriPermissions="true">
        ...
    </provider>
    ...
</application>
</manifest>

Ở đây, giá trị thuộc tính authorities dựa trên applicationId của app bạn (define trong file build.gradle), ví dụ như applicationId là com.framgia.example thì giá trị của authorities sẽ là com.framgia.example.fileprovider. Giá trị của thuộc tính exported là false bởi vì hiển nhiên rằng provider này sẽ không public. Giá trị của thuộc tính grantUriPermissions là true, giá trị này cho phép cung cấp quyền truy cập tạm thời cho file.

Tiếp đến chúng ta sẽ định nghĩa đường dẫn cho file gửi đi. Trước tiên ta tạo 1 file XML trong thư mục xml, trong thư mục resource res, ví dụ file xml cần tạo là file_paths.xml thì đường dẫn của file sẽ là res/xml/file_paths.xml với nội dung bên trong như sau:

<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <files-path name="my_images" path="images/"/>
</paths>

Ở đây trong thẻ files-path ta có 2 thuộc tính là name và path. Mình sẽ giải thích ngắn gọn chức năng 2 thuộc tính này, chi tiết hơn về các loại đường dẫn mời anh em tham khảo tại link developer nhé: Specifying Available Files

  • Giá trị của path ở đây chính là tên thư mục con chứa file mà chúng ta đang cần gửi, ví dụ: mình có 1 file ảnh test.jpg lưu ở thư mục images trong bộ nhớ trong của app thì sẽ có đường dẫn như sau: /data/data/com.framgia.example/files/images/test.jpg. Đây chính là đường dẫn thực sự của file.
  • Giá trị của name ở đây có nhiệm vụ che giấu thư mục thực sự chứa file. Chính vì thế đây sẽ là 1 phần của đường dẫn Uri ảo mà chúng ta sắp tạo.

Hoàn thành nốt đoạn code ở file AndroidManifest:

<manifest>
...
<application>
    ...
    <provider
        android:name="android.support.v4.content.FileProvider"
        android:authorities="<your_application_id>.fileprovider"
        android:exported="false"
        android:grantUriPermissions="true">
         <meta-data
            android:name="android.support.FILE_PROVIDER_PATHS"
            android:resource="@xml/file_paths" />
    </provider>
    ...
</application>
</manifest>

Tạo Content URI cho file

Chúng ta thay đổi đoạn lấy Uri của image từ file thành như sau (từ đoạn code share image ban đầu):

Uri uriImage = FileProvider.getUriForFile(getContext(), 
                    getApplicationContext().getPackageName() + ".fileprovider", 
                    file);

Lúc này đường dẫn của uriImage sẽ là content://com.framgia.example.fileprovider/my_images/test.jpg và nhớ rằng đây chỉ là đường dẫn "ảo" mà FileProvider tạo ra mà thôi.

Giờ thì có share ảnh và nhiều thứ khác ngon lành trên em Nougat thân yêu rồi 😄

Giải pháp 1 dòng

Giống với cách cheat để tránh khỏi phải request permission runtime ở trên Android Marshmallow đó là hạ targetSdkVersion xuống.

Ở đây ta chỉ cần chuyển targetSdkVersion nhỏ hơn 24 thì mọi chuyện sẽ bình thường trở lại mà chả cần phải thực hiện cả tá đoạn code trên.

Kết luận

Có thể có những bạn ước rằng mình đọc từ cuối lên đầu thì đã không tốn công nhiều đén thế : v. Tuy nhiên thì cách thay đổi targetSdkVersion không hề được khuyến khích vì nó ảnh hưởng tới trải nghiệm của người dùng + thêm nữa đó là vì lý do bảo mật của Google đã nói ở trên.


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí