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 với các ứng dụng khác các yêu cầu cho những tệp đó. Một cách để phản hồi những yêu cầu này là cung cấp lựa chọn về tệp giao diện 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 khách hàng cho phép người dùng chọn một tệp từ ứng dụng máy chủ rồi nhận các tệp đã chọn URI nội dung.

Bài học này sẽ hướng dẫn bạn cách 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 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 hãy cung cấp lựa chọn tệp Activity. Ứng dụng khách khởi động quy trình này Activity bằng cách gọi startActivityForResult() bằng Intent chứa thao tác 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 một tệp được chia sẻ.

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

Để thiết lập 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ủa bạn, cùng với một bộ lọc ý định phù hợp với hành động ACTION_PICK và danh mục CATEGORY_DEFAULTCATEGORY_OPENABLE. Đồng thời thêm bộ lọc loại MIME cho các tệp mà ứng dụng của bạn phân phối đến các ứng dụng khác. Đoạn mã sau đây minh hoạ cách chỉ định Activity mới và bộ lọc ý định:

<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 vào 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 điều này Activity 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 lựa chọn tệp

Sau khi người dùng chọn một tệp được chia sẻ, ứng dụng của bạn phải xác định tệp nào được chọn và 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, khi người dùng nhấp vào tên tệp hệ thống sẽ gọi phương thức onItemClick(), trong đó 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 khi lấy URI mà các ứng dụng có thể đọc. Làm việc này trên thiết bị chạy Android 6.0 (API cấp 23) trở lên cần đặc biệt do những thay đổi đối với mô hình quản lý quyền trong phiên bản Android đó, đặc biệt Điều khoản dịch vụ và Chính sách quyền riêng tư của READ_EXTERNAL_STORAGE trở thành quyền nguy hiểm mà ứng dụng nhận có thể thiếu.

Do những cân nhắc này, chúng tôi khuyên bạn nên tránh sử dụng Uri.fromFile(), cho thấy 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ó WRITE_EXTERNAL_STORAGE quyền trên thiết bị chạy Android 4.4 (API cấp 19) trở xuống.
  • Yêu cầu ứng dụng nhận dữ liệu có Quyền READ_EXTERNAL_STORAGE sẽ không thực hiện được các mục tiêu chia sẻ quan trọng, chẳng hạn như Gmail, không có quyền đó.

Thay vì sử dụng Uri.fromFile(), bạn có thể dùng quyền URI để cấp cho ứng dụng khác quyền truy cập vào các URI cụ thể. Mặc dù quyền URI không hoạt động trên các URI file:// được tạo bởi Uri.fromFile(), chúng hoạt động trên các URI được liên kết với Trình cung cấp nội dung. Chiến lược phát hành đĩa đơn 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 trong bộ nhớ ngoài, nhưng ở bộ nhớ cục bộ của ứng dụng gửi ý định.

Trong onItemClick(), hãy nhận Đối tượng File cho tên tệp của tệp đã chọn và truyền tệp đó làm đối số cho getUriForFile(), cùng với cơ quan cấp chứng nhậ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 (như được chỉ định trong siêu dữ liệu XML) và tên của tệp, bao gồm cả tiện ích. Cách FileProvider ánh xạ các thư mục với đường dẫn dựa trên siêu dữ liệu XML được mô tả trong phần này Chỉ định 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à nhận 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 một thư mục mà bạn chỉ định trong tệp siêu dữ liệu chứa phần tử <paths>, như được mô tả trong phần Chỉ định thư mục có thể chia sẻ. Nếu bạn gọi getUriForFile() cho một File trong đường dẫn mà bạn chưa chỉ định, bạn sẽ nhận được 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 ứ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 bằng cách thêm URI nội dung vào Intent, sau đó đặt cờ cấp quyền Intent. Các quyền bạn cấp là tạm thời và hết hạn tự động khi ngăn xếp tác vụ của ứng dụng nhận hoàn tất.

Đoạn mã sau đây cho bạn biết cách đặt quyền đọc cho tệp 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) {
                // 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: Chỉ gọi setFlags() để cấp quyền truy cập vào tệp một cách an toàn thông qua quyền truy cập tạm thời. Tránh gọi điện Phương thức Context.grantUriPermission() cho một URI nội dung của tệp, do phương thức này cấp quyền truy cập mà bạn chỉ có thể thu hồi bằng đang gọi Context.revokeUriPermission().

Đừng sử dụng Uri.fromFile(). Chế độ này buộc nhận ứng dụng để có quyền READ_EXTERNAL_STORAGE, sẽ không hoạt động chút nào nếu bạn cố gắng chia sẻ giữa nhiều 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ó WRITE_EXTERNAL_STORAGE. Và các mục tiêu chia sẻ thực sự quan trọng, chẳng hạn như ứng dụng Gmail, sẽ không 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 quyền URI để cấp cho ứng dụng khác quyền truy cập vào các URI cụ thể. Mặc dù các quyền URI không hoạt động trên các URI file:// được tạo bởi Uri.fromFile(), có thể hoạt động trên URI được liên kết với Trình cung cấp nội dung. Thay vì triển khai công cụ của riêng bạn chỉ cho việc này, bạn có thể và nên dùng FileProvider như được 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à các quyền đối với setResult(). Khi Activity bạn vừa xác định hoàn tất, hệ thống 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 một 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 bằng cách sử dụng nút Thuộc tính android:onClick. 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: