2010年1月30日 星期六

跨行程(Process)的檔案存取

一個Android應用程式有許多種存取資料的方法, Sqllite database是一種方式, 而最原始的方法就是檔案

翻開官方的文件, 在Dev Guide -> Data Storage -> Files 裡的第一段:

You can store files directly on the mobile device or on a removable storage medium. By default, other applications cannot access these files.

一個應用程式可以存取的檔案系統, 除了像是SD Card這種可移除式的媒體外, 就是內建的儲存體, 但使用內建的儲存體有一個缺點, 因為每個Android應用程式如果不是特別指定, 它都會擁有一個專屬的uid (請參考Linux/UNIX的基本文件), 預設開檔的模式是只有自己可以讀取, 因此別的應用程式, 是讀取不到這個檔的, 就算知道檔案的絕對路徑, 這當然是一種安全性的考量, 但有時候針對某些應用我們又希望能夠在不同的應用程式間分享檔案

首先先來看一下如何開啟內建儲存體內的檔案作為寫入跟讀取, 以下是相關的methods:

  • Context.openFileInput
  • Context.openFileOutput
  • Context.deleteFile
  • Context.fileList
  • Context.getFilesDir
  • Context.getFileStreamPath
  • Context.getCacheDir

如果是要開啟檔案作為輸出用, 可以用openFileInput, 透過這個寫入的檔案會在 "/data/data/your_app_package_name/files"底下, 這method可以有兩個參數,

openFileOutput(String name, int mode), 第一個當然不用多說, 第二個有幾種模式

  • MODE_APPEND 開啟檔案作為附加新內容(Append)
  • MODE_PRIVATE 這是預設, 也就是說其他的application (不同的uid), 不可以來存取
  • MODE_WORLD_READABLE 全世界的人都可以來讀, 只要知道路徑檔名
  • MODE_WORLD_WRITABLE 除了開放給所有的人讀也可以寫

從這邊就可以知道, 開MODE_WORLD_WRITABLE, 就可以達到跨行程檔案共用的方式, 但這方法的缺點是, 等於把權限開放給所有人, 只要知道目錄的應用程式都可以來存取, 甚至可以刪除, 完全無法管控, 其實挺危險的

另外一個方式是透過ContentProvider的方式, 這方式因為透過Uri來存取, 可以隱藏實際檔案位置, 而且不需暴露檔案實際位置, 因為是透過ContentProvider所以也加上permission的管控

在ContentProvider裡面寫法很簡單, 只要overwrite "openFile"即可, openFile接受Uri和file mode兩個參數, Uri和檔案的對應關係, 可以由ContentProvider這邊自由決定, file mode是存取模式, API文件裡面寫的是"rwt", r - read, w -write, t - truncates, 但其實這也是可以由實作端控制, 未必一定要照這規則

這個method必須回傳一個ParcelFileDescriptor, 如下

public ParcelFileDescriptor openFile(Uri uri, String mode)
throws FileNotFoundException {
File myFile = new File("/sdcard/abc");
return ParcelFileDescriptor.open(myFile,
ParcelFileDescriptor.MODE_READ_WRITE);
}

在另一個行程需要透過這個ContentProvider 存取檔案也很簡單, 只要利用ContentResolver.openFileDescriptor, 呼叫這個method就可以得到由ContentProvider得回來的ParcelFileDescriptor

可是又要怎存取這個ParcelFileDescriptor呢? 透過ParcelFileDescriptor.getFileDescriptor就可以取得FileDescriptor的實體, 利用這個就可以透過FileInputStream和FileOuputStream來存取了, 如下例

ParcelFileDescriptor remoteFD = resolver.openFileDescriptor("myuri://URI", "rw");
FileOutputStream out = new FileOutputStream(remoteFD.getFileDescriptor());

高階應用:
ParcelFileDescriptor其實也可以來自於Socket, 只要用ParcelFileDescriptor.fromSocket(Socket socket)