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_DEFAULT
và CATEGORY_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. { _, _, 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. { _, _, 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. { _, _, 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: