Chia sẻ tệp

Sau khi thiết lập ứng dụng để chia sẻ tệp bằng URI nội dung, bạn có thể phản hồi yêu cầu của các ứng dụng khác đối với những tệp đó. Một cách để phản hồi những yêu cầu này là cung cấp giao diện lựa chọn tệp từ ứng dụng máy chủ mà các ứng dụng khác có thể gọi. Phương pháp này cho phép ứng dụng khách cho phép người dùng chọn một tệp từ ứng dụng máy chủ, sau đó nhận URI nội dung của tệp đã chọn.

Bài học này sẽ hướng dẫn bạn cách tạo một lựa chọn tệp Activity trong ứng dụng để phản hồi các yêu cầu về tệp.

Nhận yêu cầu về tệp

Để nhận yêu cầu về tệp từ ứng dụng khách và phản hồi bằng URI nội dung, ứng dụng của bạn phải cung cấp lựa chọn tệp Activity. Các ứng dụng khách bắt đầu Activity này bằng cách gọi startActivityForResult() với Intent chứa hành động ACTION_PICK. Khi ứng dụng khách gọi startActivityForResult(), ứng dụng của bạn có thể trả về kết quả cho ứng dụng khách, dưới dạng URI nội dung cho tệp mà người dùng đã chọn.

Để tìm hiểu cách triển khai yêu cầu cho một tệp trong ứng dụng khách, hãy xem bài học Yêu cầu tệp được chia sẻ.

Tạo một Hoạt động lựa chọn tệp

Để thiết lập việc lựa chọn tệp Activity, hãy bắt đầu bằng cách chỉ định Activity trong tệp kê khai, cùng với bộ lọc ý định khớp với hành động ACTION_PICK cũng như các danh mục CATEGORY_DEFAULTCATEGORY_OPENABLE. Ngoài ra, hãy thêm bộ lọc loại MIME cho các tệp mà ứng dụng của bạn phân phát đến các ứng dụng khác. Đoạn mã sau đây cho bạn biết cách chỉ định Activity và bộ lọc ý định mới:

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    ...
        <application>
        ...
            <activity
                android:name=".FileSelectActivity"
                android:label="@File Selector" >
                <intent-filter>
                    <action
                        android:name="android.intent.action.PICK"/>
                    <category
                        android:name="android.intent.category.DEFAULT"/>
                    <category
                        android:name="android.intent.category.OPENABLE"/>
                    <data android:mimeType="text/plain"/>
                    <data android:mimeType="image/*"/>
                </intent-filter>
            </activity>

Xác định Hoạt động lựa chọn tệp trong mã

Tiếp theo, hãy xác định một lớp con Activity hiển thị các tệp có sẵn từ thư mục files/images/ của ứng dụng trong bộ nhớ trong và cho phép người dùng chọn tệp mong muốn. Đoạn mã sau đây minh hoạ cách xác định Activity này và phản hồi lựa chọn của người dùng:

Kotlin

class MainActivity : Activity() {

    // The path to the root of this app's internal storage
    private lateinit var privateRootDir: File
    // The path to the "images" subdirectory
    private lateinit var imagesDir: File
    // Array of files in the images subdirectory
    private lateinit var imageFiles: Array<File>
    // Array of filenames corresponding to imageFiles
    private lateinit var imageFilenames: Array<String>

    // Initialize the Activity
    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        // Set up an Intent to send back to apps that request a file
        resultIntent = Intent("com.example.myapp.ACTION_RETURN_FILE")
        // Get the files/ subdirectory of internal storage
        privateRootDir = filesDir
        // Get the files/images subdirectory;
        imagesDir = File(privateRootDir, "images")
        // Get the files in the images subdirectory
        imageFiles = imagesDir.listFiles()
        // Set the Activity's result to null to begin with
        setResult(Activity.RESULT_CANCELED, null)
        /*
         * Display the file names in the ListView fileListView.
         * Back the ListView with the array imageFilenames, which
         * you can create by iterating through imageFiles and
         * calling File.getAbsolutePath() for each File
         */
        ...
    }
    ...
}

Java

public class MainActivity extends Activity {
    // The path to the root of this app's internal storage
    private File privateRootDir;
    // The path to the "images" subdirectory
    private File imagesDir;
    // Array of files in the images subdirectory
    File[] imageFiles;
    // Array of filenames corresponding to imageFiles
    String[] imageFilenames;
    // Initialize the Activity
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...
        // Set up an Intent to send back to apps that request a file
        resultIntent =
                new Intent("com.example.myapp.ACTION_RETURN_FILE");
        // Get the files/ subdirectory of internal storage
        privateRootDir = getFilesDir();
        // Get the files/images subdirectory;
        imagesDir = new File(privateRootDir, "images");
        // Get the files in the images subdirectory
        imageFiles = imagesDir.listFiles();
        // Set the Activity's result to null to begin with
        setResult(Activity.RESULT_CANCELED, null);
        /*
         * Display the file names in the ListView fileListView.
         * Back the ListView with the array imageFilenames, which
         * you can create by iterating through imageFiles and
         * calling File.getAbsolutePath() for each File
         */
         ...
    }
    ...
}

Phản hồi một lựa chọn tệp

Sau khi người dùng chọn một tệp dùng chung, ứng dụng của bạn phải xác định tệp nào đã được chọn, sau đó tạo URI nội dung cho tệp đó. Vì Activity hiển thị danh sách các tệp hiện có trong ListView, nên khi người dùng nhấp vào tên tệp, hệ thống sẽ gọi phương thức onItemClick() để bạn có thể lấy tệp đã chọn.

Khi dùng một ý định để gửi URI của một tệp từ ứng dụng này sang ứng dụng khác, bạn phải cẩn thận để lấy URI mà các ứng dụng khác có thể đọc. Bạn cần đặc biệt thận trọng để làm việc này trên các thiết bị chạy Android 6.0 (API cấp 23) trở lên do các thay đổi đối với mô hình quản lý quyền trong phiên bản Android đó, đặc biệt là READ_EXTERNAL_STORAGE trở thành quyền nguy hiểm mà ứng dụng nhận có thể thiếu.

Với những điểm cần cân nhắc này, bạn nên tránh sử dụng Uri.fromFile(), vì mã này có một số hạn chế. Phương thức này:

  • Không cho phép chia sẻ tệp giữa các hồ sơ.
  • Yêu cầu ứng dụng của bạn có quyền WRITE_EXTERNAL_STORAGE trên các thiết bị chạy Android 4.4 (API cấp 19) trở xuống.
  • Yêu cầu các ứng dụng nhận phải có quyền READ_EXTERNAL_STORAGE. Quyền này sẽ không thành công đối với các mục tiêu chia sẻ quan trọng (chẳng hạn như Gmail) không có quyền đó.

Thay vì dùng Uri.fromFile(), bạn có thể dùng quyền URI để cấp cho các ứng dụng khác quyền truy cập vào URI cụ thể. Mặc dù quyền URI không hoạt động trên các URI file:// do Uri.fromFile() tạo, nhưng chúng vẫn hoạt động trên các URI liên kết với Trình cung cấp nội dung. API FileProvider có thể giúp bạn tạo các URI như vậy. Phương pháp này cũng áp dụng với các tệp không có trong bộ nhớ ngoài, nhưng nằm trong bộ nhớ cục bộ của ứng dụng sẽ gửi ý định.

Trong onItemClick(), hãy lấy một đối tượng File cho tên tệp của tệp đã chọn và truyền đối tượng đó làm đối số cho getUriForFile(), cùng với thẩm quyền mà bạn đã chỉ định trong phần tử <provider> cho FileProvider. URI nội dung thu được chứa đơn vị quản lý, một phân đoạn đường dẫn tương ứng với thư mục của tệp (như được chỉ định trong siêu dữ liệu XML) và tên của tệp bao gồm đuôi tệp. Cách FileProvider ánh xạ các thư mục đến phân đoạn đường dẫn dựa trên siêu dữ liệu XML được mô tả trong phần Chỉ định các thư mục có thể chia sẻ.

Đoạn mã sau đây cho bạn biết cách phát hiện tệp đã chọn và lấy URI nội dung cho tệp đó:

Kotlin

    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        // Define a listener that responds to clicks on a file in the ListView
        fileListView.onItemClickListener = AdapterView.OnItemClickListener { _, _, position, _ ->
            /*
             * Get a File for the selected file name.
             * Assume that the file names are in the
             * imageFilename array.
             */
            val requestFile = File(imageFilenames[position])
            /*
             * Most file-related method calls need to be in
             * try-catch blocks.
             */
            // Use the FileProvider to get a content URI
            val fileUri: Uri? = try {
                FileProvider.getUriForFile(
                        this@MainActivity,
                        "com.example.myapp.fileprovider",
                        requestFile)
            } catch (e: IllegalArgumentException) {
                Log.e("File Selector",
                        "The selected file can't be shared: $requestFile")
                null
            }
            ...
        }
        ...
    }

Java

    protected void onCreate(Bundle savedInstanceState) {
        ...
        // Define a listener that responds to clicks on a file in the ListView
        fileListView.setOnItemClickListener(
                new AdapterView.OnItemClickListener() {
            @Override
            /*
             * When a filename in the ListView is clicked, get its
             * content URI and send it to the requesting app
             */
            public void onItemClick(AdapterView<?> adapterView,
                    View view,
                    int position,
                    long rowId) {
                /*
                 * Get a File for the selected file name.
                 * Assume that the file names are in the
                 * imageFilename array.
                 */
                File requestFile = new File(imageFilename[position]);
                /*
                 * Most file-related method calls need to be in
                 * try-catch blocks.
                 */
                // Use the FileProvider to get a content URI
                try {
                    fileUri = FileProvider.getUriForFile(
                            MainActivity.this,
                            "com.example.myapp.fileprovider",
                            requestFile);
                } catch (IllegalArgumentException e) {
                    Log.e("File Selector",
                          "The selected file can't be shared: " + requestFile.toString());
                }
                ...
            }
        });
        ...
    }

Hãy nhớ rằng bạn chỉ có thể tạo URI nội dung cho các tệp nằm trong thư mục mà bạn đã chỉ định trong tệp siêu dữ liệu chứa phần tử <paths>, như mô tả trong phần Chỉ định các thư mục có thể chia sẻ. Nếu gọi getUriForFile() cho File trong đường dẫn mà bạn chưa chỉ định, bạn sẽ nhận được một IllegalArgumentException.

Cấp quyền cho tệp

Giờ đây, khi đã có URI nội dung cho tệp mà bạn muốn chia sẻ với một ứng dụng khác, bạn cần cho phép ứng dụng khách truy cập vào tệp đó. Để cho phép truy cập, hãy cấp quyền cho ứng dụng khách bằng cách thêm URI nội dung vào Intent, sau đó đặt cờ quyền trên Intent. Các quyền bạn cấp là tạm thời và tự động hết hạn khi ngăn xếp tác vụ của ứng dụng nhận hoàn tất.

Đoạn mã sau đây hướng dẫn bạn cách thiết lập quyền đọc cho tệp:

Kotlin

    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        // Define a listener that responds to clicks on a file in the ListView
        fileListView.onItemClickListener = AdapterView.OnItemClickListener { _, _, position, _ ->
            ...
            if (fileUri != null) {
                // Grant temporary read permission to the content URI
                resultIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
                ...
            }
            ...
        }
        ...
    }

Java

    protected void onCreate(Bundle savedInstanceState) {
        ...
        // Define a listener that responds to clicks in the ListView
        fileListView.setOnItemClickListener(
                new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> adapterView,
                    View view,
                    int position,
                    long rowId) {
                ...
                if (fileUri != null) {
                    // Grant temporary read permission to the content URI
                    resultIntent.addFlags(
                        Intent.FLAG_GRANT_READ_URI_PERMISSION);
                }
                ...
             }
             ...
        });
    ...
    }

Thận trọng: Gọi setFlags() là cách duy nhất để cấp quyền truy cập tạm thời vào các tệp của bạn một cách an toàn. Tránh gọi phương thức Context.grantUriPermission() cho URI nội dung của tệp, vì phương thức này cấp quyền truy cập mà bạn chỉ có thể thu hồi bằng cách gọi Context.revokeUriPermission().

Đừng sử dụng Uri.fromFile(). Điều này buộc các ứng dụng nhận phải có quyền READ_EXTERNAL_STORAGE, sẽ không hoạt động nếu bạn đang cố gắng chia sẻ cho người dùng và trong các phiên bản Android thấp hơn 4.4 (API cấp 19), sẽ yêu cầu ứng dụng của bạn phải có WRITE_EXTERNAL_STORAGE. Các mục tiêu chia sẻ thực sự quan trọng, chẳng hạn như ứng dụng Gmail, không có READ_EXTERNAL_STORAGE, khiến lệnh gọi này không thành công. Thay vào đó, bạn có thể sử dụng các quyền URI để cấp cho ứng dụng khác quyền truy cập vào URI cụ thể. Mặc dù quyền URI không hoạt động trên URI file:// như được tạo bởi Uri.fromFile(), nhưng quyền truy cập này vẫn hoạt động trên URI liên kết với Trình cung cấp nội dung. Thay vì triển khai chỉ riêng cho việc này, bạn có thể và nên sử dụng FileProvider như giải thích trong phần Chia sẻ tệp.

Chia sẻ tệp với ứng dụng yêu cầu

Để chia sẻ tệp với ứng dụng đã yêu cầu tệp, hãy chuyển Intent chứa URI nội dung và quyền cho setResult(). Khi Activity bạn vừa xác định hoàn tất, hệ thống sẽ gửi Intent chứa URI nội dung đến ứng dụng khách. Đoạn mã sau đây cho bạn biết cách thực hiện việc này:

Kotlin

    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        // Define a listener that responds to clicks on a file in the ListView
        fileListView.onItemClickListener = AdapterView.OnItemClickListener { _, _, position, _ ->
            ...
            if (fileUri != null) {
                ...
                // Put the Uri and MIME type in the result Intent
                resultIntent.setDataAndType(fileUri, contentResolver.getType(fileUri))
                // Set the result
                setResult(Activity.RESULT_OK, resultIntent)
            } else {
                resultIntent.setDataAndType(null, "")
                setResult(RESULT_CANCELED, resultIntent)
            }
        }
    }

Java

    protected void onCreate(Bundle savedInstanceState) {
        ...
        // Define a listener that responds to clicks on a file in the ListView
        fileListView.setOnItemClickListener(
                new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> adapterView,
                    View view,
                    int position,
                    long rowId) {
                ...
                if (fileUri != null) {
                    ...
                    // Put the Uri and MIME type in the result Intent
                    resultIntent.setDataAndType(
                            fileUri,
                            getContentResolver().getType(fileUri));
                    // Set the result
                    MainActivity.this.setResult(Activity.RESULT_OK,
                            resultIntent);
                    } else {
                        resultIntent.setDataAndType(null, "");
                        MainActivity.this.setResult(RESULT_CANCELED,
                                resultIntent);
                    }
                }
        });

Cung cấp cho người dùng cách để quay lại ứng dụng khách ngay lập tức sau khi họ chọn tệp. Một cách để thực hiện việc này là cung cấp dấu kiểm hoặc nút Xong. Liên kết một phương thức với nút bằng thuộc tính android:onClick của nút đó. Trong phương thức này, hãy gọi finish(). Ví dụ:

Kotlin

    fun onDoneClick(v: View) {
        // Associate a method with the Done button
        finish()
    }

Java

    public void onDoneClick(View v) {
        // Associate a method with the Done button
        finish();
    }

Để biết thêm thông tin liên quan, hãy tham khảo: