Người dùng ngày nay ưu tiên sử dụng các thiết bị di động (điện thoại, máy tính bảng), thay vì dùng các thiết bị để bàn (như laptop, desktop), đồng thời sử dụng ngày càng nhiều hơn các dịch vụ lưu trữ đám mây (cloud storage) đã buộc Google phải giới thiệu Storage Access Framework như là một phần của Android 4.4 SDK.
Storage Access Framework
Từ quan điểm người dùng, Storage Access Framework cung cấp một giao diện người dùng trực quan cho phép người dùng có thể tìm kiếm, tạo hay xóa các tập tin (văn bản, âm thanh, hình ảnh,v.v.) được lưu trữ trên các dịch vụ từ ứng dụng Android. Các dịch vụ lưu trữ gọi là document providers và giao diện cho phép người dùng tương tác ở trên gọi là picker.
Các dịch vụ lưu trữ (hay document providers) có thể là các dịch vụ đám mây (cloud-based services) hay có thể là các dịch vụ lưu trữ cục bộ trên cùng thiết bị chạy ứng dụng Android.
Với một tập các Intent được tích hợp trong Android 4.4, các nhà phát triển ứng dụng Android có thể làm việc với các dịch vụ lưu trữ này chỉ với một vài dòng mã.
Làm việc với Storage Access Framework
Android 4.4 giới thiệu một tập các Intent được thiết kế để tích hợp các đặc trưng của Storage Access Framework vào trong các ứng dụng Android. Các Intent này thể hiện giao diện Storage Access Framework đến người dùng (picker) và trả về kết quả tương tác đến ứng dụng thông qua lời gọi phương thức onActivityResult() của Activity thực thi Intent. Khi phương thức onActivityResult() được gọi, nó sẽ được chuyển Uri của tập tin được chọn và một giá trị xác nhận thành công hay thao tác khác.
Các intent của Storage Access Framework bao gồm:
- ACTION_OPEN_DOCUMENT: Cho phép người dùng truy cập đến tập tin được lưu trữ thông qua giao diện (picker). Tập tin được chọn sẽ chuyển lại ứng dụng dưới hình thức các đối tượng Uri.
- ACTION_CREATE_DOCUMENT: Cho phép người dùng chọn dịch vụ lưu trữ (document providers), vị trí lưu trữ và tên tập tin mới được tạo. Khi được chọn, tập tin được tạo bởi Storage Access Framework và Uri của tập tin đó được trả lại ứng dụng để xử lý.
Lọc danh sách các tệp
Danh sách các tệp trên giao diện người dùng (picker) có thể được lọc với một vài tùy chọn dùng phương thức addCategory với tham số CATEGORY_OPENABLE hay phương thức setType của lớp Intent. Cân nhắc đoạn mã minh họa sau:
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); intent.addCategory(Intent.CATEGORY_OPENABLE); intent.setType("image/jpeg"); startActivityForResult(intent, OPEN_REQUEST_CODE);
Xử lý kết quả Intent
Khi một Intent trả lại điều khiển đến ứng dụng, Intent này sẽ thực hiện gọi phương thức onActivityResult() của Activity thực thi nó. Tham số phương thức onActivityResult() bao gồm:
- Mã yêu cầu (request code) được chuyển đến Intent lúc Intent bắt đầu thực thi.
- Mã kết quả (result code) xác nhận Intent thành công hay không.
- Một đối tượng chứa Uri của tập tin được chọn.
Đoạn mã sau minh họa cách phương thức onActivityResult xử lý kết quả của Intent ACTION_OPEN_DOCUMENT ở trên:
public void onActivityResult(int requestCode, int resultCode, Intent resultData) { Uri currentUri = null; if (resultCode == Activity.RESULT_OK) { if (requestCode == OPEN_REQUEST_CODE) { if (resultData != null) { currentUri = resultData.getData(); readFileContent(currentUri); } } }
Đoạn mã trên xác nhận Intent thành công, kiểm tra mã yêu cầu có khớp với yêu cầu mở tập tin và trích xuất Uri từ dữ liệu Intent. Uri có thể được dùng để đọc nội dung của tập tin.
Đọc nội dung của một tập tin
Các bước để đọc nội dung tập tin phụ thuộc vào kiểu tập tin đó. Các bước để đọc nội dung của tập tin văn bản (txt) khác với các bước đọc nội dung tập tin ảnh hay âm thanh. Hai ví dụ sau sẽ giúp chúng ta so sánh sự khác biệt trong các bước thực hiện đọc nội dung một tập tin văn bản và một tập tin ảnh.
Đọc nội dung một tập tin ảnh: Tập tin ảnh được gán đến một đối tượng Bitmap bằng cách trích xuất thông tin từ đối tượng Uri và sau đó giải mã hình ảnh trong một thể hiện của lớp BitmapFactory:
ParcelFileDescriptor pFileDescriptor = getContentResolver().openFileDescriptor(uri, "r"); FileDescriptor fileDescriptor = pFileDescriptor.getFileDescriptor(); Bitmap image = BitmapFactory.decodeFileDescriptor(fileDescriptor); pFileDescriptor.close(); myImageView.setImageBitmap(image);
Chúng ta dùng tham số r để xác định tập tin được mở chỉ đọc. Có thể dùng w để xác định tập tin được mở chỉ ghi hay dùng rwt cho phép đọc ghi.
Đọc nội dung tập tin văn bản dùng đối tượng InputStream:
InputStream inputStream = getContentResolver().openInputStream(uri); BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); String readline; while ((readline = reader.readLine()) != null) { // Do something with each line in the file } inputStream.close();
Ghi nội dung đến một tập tin
Các bước ghi nội dung đến tập tin cũng tương tự đọc nội dung tập tin và phụ thuộc vào kiểu tập tin. Ghi nội dung đến tập tin ảnh có thể dùng tham số w ( hay rwt) như đã nêu trên. Ghi nội dung đến tập tin văn bản dùng đối tượng output stream thay vì input stream như sau:
try{ ParcelFileDescriptor pFileDescriptor = this.getContentResolver(). openFileDescriptor(uri, "w"); FileOutputStream fileOutputStream = new FileOutputStream(pFileDescriptor.getFileDescriptor()); String textContent = "Some sample text"; fileOutputStream.write(textContent.getBytes()); fileOutputStream.close(); pFileDescriptor.close(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); }
Đầu tiên, thông tin tập tin được trích xuất từ Uri được gán đến đối tượng ParcelFileDescriptor và yêu cầu quyền ghi đến tập tin. Thông tin tập tin từ đối tượng ParcelFileDescriptor cung cấp tham chiếu đến đối tượng FileOutputStream của tập tin. Nội dung được ghi đến tập tin trước khi các đối tượng ParcelFileDescriptor và FileOutputStream đóng.
Xóa một tập tin
Một tập tin được xóa hay không phụ thuộc vào dịch vụ lưu trữ (document providers) có hỗ trợ việc xóa tập tin hay không. Nếu được xóa, đoạn mã trông như sau:
if (DocumentsContract.deleteDocument(getContentResolver(), uri)) // Deletion was successful else // Deletion failed
Truy cập hợp lệ đến một tập tin
Khi một ứng dụng truy cập đến tập tin qua dịch vụ Storage Access Framework, ứng dụng cần duy trì tính ổn định và hợp lệ cho đến khi thiết bị Android chạy ứng dụng đó khởi động lại. Để đạt tính ổn định trong truy cập tập tin, một ứng dụng Android cần thực hiện một số tùy chọn trong Uri. Đoạn mã sau minh họa cách một ứng dụng Android sử dụng các tùy chọn cho Uri để duy trì khả năng đọc và ghi đến một tập tin một cách ổn định và hợp lệ:
final int takeFlags = intent.getFlags() & (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); getContentResolver().takePersistableUriPermission(fileUri, takeFlags);
Sau khi cài đặt các tùy chọn truy cập, có thể giải phóng các tùy chọn này bằng phương thức releasePersistableUriPermission() như đoạn mã sau:
final int releaseFlags = intent.getFlags() & (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); getContentResolver().releasePersistableUriPermission(fileUri, releaseFlags);
Ứng dụng minh họa dùng Storage Access Framework
Thiết kế giao diện và khai báo mã yêu cầu (request code)
Tạo một ứng dụng Android tên StorageDemo, chọn API 19: Android 4.4 (KitKat)
Trong tập tin activity_main.xml ở chế độ Design, xóa TextView mặc định và thêm 3 Button, 1 Plain Text đến giao diện thiết kế. Thiết lập giá trị thuộc tính Text của các Button lần lượt là New, Open, Save; thiết lập giá trị thuộc tính ID của Plain Text là fileText và để trống thuộc tính Text. Định vị các điều khiển như sau:
Trong tập tin MainActivity.java, khai báo đối tượng TextView (tham chiếu đến Plain Text) và các mã yêu cẩu như sau:
import android.widget.EditText; private static EditText textView; private static final int CREATE_REQUEST_CODE = 40; private static final int OPEN_REQUEST_CODE = 41; private static final int SAVE_REQUEST_CODE = 42; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); textView = (EditText) findViewById(R.id.fileText); }
Tạo tập tin lưu trữ mới
Khi người dùng nhấn nút New, một tập tin văn bản (phần mở rộng txt) được tạo với tên mặc định newText.txt. Để thực hiện được điều này, chúng ta sẽ định nghĩa phương thức newFile() trong lớp MainActivity cho phép ứng dụng kích hoạt intent ACTION_CREATE_DOCUMENT và truyền mã yêu cầu CREATE_REQUEST_CODE như sau:
import android.content.Intent; import android.view.View; import android.app.Activity; public void newFile(View view) { Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT); intent.addCategory(Intent.CATEGORY_OPENABLE); intent.setType("text/plain"); intent.putExtra(Intent.EXTRA_TITLE, "newText.txt"); startActivityForResult(intent, CREATE_REQUEST_CODE); }
Sau khi định nghĩa phương thức newFile, chúng ta cần định nghĩa phương thức onActivityResult như sau:
public void onActivityResult(int requestCode, int resultCode, Intent resultData) { Uri currentUri = null; if (resultCode == Activity.RESULT_OK) { if (requestCode == CREATE_REQUEST_CODE) { if (resultData != null) { textView.setText(""); } } } }
Lưu và mở lại tập tin activity_main.xml trong chế độ Text. Tìm đến phần tử Button có thuộc tính Text là New và thêm thuộc tính Android: onClick với giá trị là phương thức newFile:
<Button ... android:onClick="newFile" android:text="New" .../>
Lưu và hực thi ứng dụng. Nhấn nút New
Một số trình ảo sẽ xuất hiện dịch vụ Google Drive trông như sau:
Trong trường hợp có hỗ trợ Google Drive, có thể đăng nhập và lưu tập tin đến dịch vụ này. Trong trường hợp không hỗ trợ Google Drive (hình thứ nhất), chúng ta chọn Downloads là thư mục cục bộ trên máy ảo:
Thay đổi tên mặc định newText.txt thành tên tập tin bất kỳ, ví dụ ngocminh.txt và nhấn nút Save. Khi nhấn nút Save ứng dụng sẽ lưu tập tin trong thư mục cục bộ Downloads và sẽ trở lại màn hình chính. Chúng ta sẽ viết chức năng mở tập tin nhưng lúc này chúng ta có thể xem tập tin ngocminh.txt bằng cách nhấn lại nút New
Chúng ta đã tạo tập tin lưu trữ mới đến thư mục cục bộ hay dịch vụ đám mây (nếu thiết bị có hỗ trợ). Bước kế tiếp chúng ta sẽ ghi và lưu nội dung đến các tập tin chúng ta đã tạo.
Ghi và lưu nội dung đến tập tin
Để ghi nội dung đến tập tin, chúng ta sẽ định nghĩa phương thức saveFile() trong lớp MainActivity cho phép ứng dụng kích hoạt intent ACTION_OPEN_DOCUMENT và truyền mã yêu cầu SAVE_REQUEST_CODE như sau:
public void saveFile(View view) { Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); intent.addCategory(Intent.CATEGORY_OPENABLE); intent.setType("text/plain"); startActivityForResult(intent, SAVE_REQUEST_CODE); }
Bên cạnh đó, phương thức onActivityResult cũng được cập nhật thêm một số đoạn mã sau:
import android.net.Uri; public void onActivityResult(int requestCode, int resultCode, Intent resultData) { Uri currentUri = null; if (resultCode == Activity.RESULT_OK) { if (requestCode == CREATE_REQUEST_CODE) { if (resultData != null) { textView.setText(""); } } else if (requestCode == SAVE_REQUEST_CODE) { if (resultData != null) { currentUri = resultData.getData(); writeFileContent(currentUri); } } } }
Ở đây chúng ta có gọi phương thức writeFileContent và phương thức này được định nghĩa như sau:
import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import android.os.ParcelFileDescriptor; private void writeFileContent(Uri uri) { try{ ParcelFileDescriptor pfd = this.getContentResolver(). openFileDescriptor(uri, "w"); FileOutputStream fileOutputStream = new FileOutputStream( pfd.getFileDescriptor()); String textContent = textView.getText().toString(); fileOutputStream.write(textContent.getBytes()); fileOutputStream.close(); pfd.close(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } }
Phương thức này sử dụng một số đối tượng như ParcelFileDescriptor hay FileOutputStream đã được đề cập trong phần ghi nội dung đến tập tin ở trên.
Lưu và mở lại tập tin activity_main.xml trong chế độ Text. Tìm đến Button có thuộc tính Text là Save và thêm thuộc tính Android: onClick với giá trị là phương thức saveFile:
<Button ... android:onClick="saveFile" android:text="Save" .../>
Lưu và thực thi ứng dụng. Nhập một vài nội dung trong Plain Text:
Nhấn nút Save sẽ đến danh sách các tập tin chúng ta đã tạo. Chọn ngocminh.txt và nội dung sẽ được lưu đến tập tin này. Ứng dụng sẽ trở lại giao diện chính.
Mở và đọc nội dung tập tin
Chúng ta đã tạo mới và ghi nội dung đến một tập tin. Bây giờ, chúng ta sẽ cho phép ứng dụng mở và đọc nội dung của một tập tin bất kỳ được lưu trong dịch vụ.
Để mở và đọc nội dung của một tập tin bất kỳ, chúng ta sẽ định nghĩa phương thức openFile() trong lớp MainActivity cho phép ứng dụng kích hoạt intent ACTION_OPEN_DOCUMENT và truyền mã yêu cầu OPEN_REQUEST_CODE như sau:
public void openFile(View view) { Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); intent.addCategory(Intent.CATEGORY_OPENABLE); intent.setType("text/plain"); startActivityForResult(intent, OPEN_REQUEST_CODE); }
Phương thức onActivityResult cũng được cập nhật thêm một số đoạn mã sau:
public void onActivityResult(int requestCode, int resultCode, Intent resultData) { Uri currentUri = null; if (resultCode == Activity.RESULT_OK) { if (requestCode == CREATE_REQUEST_CODE) { if (resultData != null) { textView.setText("");} } else if (requestCode == SAVE_REQUEST_CODE) { if (resultData != null) { currentUri = resultData.getData(); writeFileContent(currentUri); } } else if (requestCode == OPEN_REQUEST_CODE) { if (resultData != null) { currentUri = resultData.getData(); try { String content = readFileContent(currentUri); textView.setText(content); } catch (IOException e) { // xử lý lỗi ở đây } } } } }
Nội dung của phương thức readFileContent như sau:
import java.io.BufferedReader; import java.io.InputStream; import java.io.InputStreamReader; private String readFileContent(Uri uri) throws IOException { InputStream inputStream = getContentResolver().openInputStream(uri); BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); StringBuilder stringBuilder = new StringBuilder(); String currentline; while ((currentline = reader.readLine()) != null) { stringBuilder.append(currentline + "\n"); } inputStream.close(); return stringBuilder.toString(); }
Chúng ta dùng đối tượng InputStream và BufferedReader để mở và đọc nội dung tập tin. Nội dung này được lưu trong biến kiể StringBuilder.
Lưu và mở lại tập tin activity_main.xml trong chế độ Text. Tìm đến Button có thuộc tính Text là Open và thêm thuộc tính Android: onClick với giá trị là phương thức openFile:
<Button ... android:onClick="openFile" android:text="Open" .../>
Lưu và thực thi ứng dụng. Nhấn nút Open và chọn tập tin cần đọc nội dung, ví dụ ngocminh.txt, ứng dụng sẽ điều hướng trở lại màn hình chính với nội dung của tập tin trong Plain Text.
Như vậy, chúng ta vừa hoàn thành ứng dụng StorageDemo có thể tạo mới, lưu và mở nội dung một tập tin đến dịch vụ lưu trữ dùng Android Storage Access Framework. Mã nguồn các tập tin ứng dụng StorageDemo có thể xem tại GitHub.
Ý kiến bài viết