將應用程式設定為使用內容 URI 分享檔案後,您就可以回應其他應用程式對這些檔案的要求。如要回應這些要求,其中一種方式是提供伺服器應用程式的檔案選取介面,以便其他應用程式叫用。這個方法可讓用戶端應用程式讓使用者從伺服器應用程式選取檔案,然後接收所選檔案的內容 URI。
本課程將說明如何在應用程式中建立檔案選取 Activity
以回應檔案要求。
接收檔案要求
如要接收用戶端應用程式的檔案要求,並以內容 URI 回應,應用程式應提供檔案選取項目 Activity
。用戶端應用程式會使用包含 ACTION_PICK
動作的 Intent
呼叫 startActivityForResult()
,啟動這個 Activity
。當用戶端應用程式呼叫 startActivityForResult()
時,應用程式會以使用者所選檔案的內容 URI 形式,將結果傳回用戶端應用程式。
如要瞭解如何在用戶端應用程式中實作檔案要求,請參閱「要求共用檔案」課程。
建立檔案選取活動
如要設定檔案選取 Activity
,請先在資訊清單中指定 Activity
,以及符合 ACTION_PICK
和 CATEGORY_DEFAULT
和 CATEGORY_OPENABLE
類別的意圖篩選器。此外,也請為應用程式提供給其他應用程式的檔案新增 MIME 類型篩選器。下列程式碼片段說明如何指定新的 Activity
和意圖篩選器:
<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>
在程式碼中定義檔案選取活動
接下來,請定義 Activity
子類別,在內部儲存空間中顯示應用程式 files/images/
目錄的可用檔案,並讓使用者挑選需要的檔案。下列程式碼片段示範如何定義這個 Activity
並回應使用者的選擇:
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 */ ... } ... }
回應選取的檔案
使用者選取共用檔案後,應用程式必須判斷已選取的檔案,然後產生該檔案的內容 URI。由於 Activity
會顯示 ListView
中的可用檔案清單,因此當使用者點選檔案名稱時,系統會呼叫 onItemClick()
方法,以取得所選檔案。
使用意圖將檔案的 URI 從一個應用程式傳送至另一個應用程式時,您必須謹慎取得其他應用程式可讀取的 URI。如果是搭載 Android 6.0 (API 級別 23) 以上版本的裝置,則需要特別注意,因為該 Android 版本的權限模型有所變更,尤其是 READ_EXTERNAL_STORAGE
造成,且接收應用程式可能缺少的
危險權限。
考量到上述事項時,建議您避免使用 Uri.fromFile()
,因為其中有數個缺點。這個方法:
- 不允許跨設定檔共用檔案。
- 您的應用程式必須在搭載 Android 4.4 (API 級別 19) 以下版本的裝置上具備
WRITE_EXTERNAL_STORAGE
權限。 - 這需要接收應用程式必須具備
READ_EXTERNAL_STORAGE
權限,這樣對於沒有該項權限的重要共用目標 (例如 Gmail) 來說,就無法使用這項權限。
您可以使用 URI 權限授予其他應用程式存取特定 URI 的權限,而不使用 Uri.fromFile()
。雖然 URI 權限不適用於 Uri.fromFile()
產生的 file://
URI,但這些權限適用於與內容供應器相關聯的 URI。FileProvider
API 可協助您建立這類 URI。這個方法也適用於不在外部儲存空間,但在傳送意圖的應用程式本機儲存空間中的檔案。
在 onItemClick()
中,取得所選檔案名稱的 File
物件,並將該物件做為引數傳送至 getUriForFile()
,並一併傳遞您在 FileProvider
的 <provider>
元素中指定的授權。產生的內容 URI 包含授權、對應至檔案目錄的路徑片段 (如 XML 中繼資料中的指定),以及檔案名稱 (包含副檔名)。如需 FileProvider
如何根據 XML 中繼資料將目錄對應至路徑片段,請參閱「指定可共用的目錄」一節。
下列程式碼片段說明如何偵測所選檔案並取得該檔案的內容 URI:
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()); } ... } }); ... }
請記住,只有當檔案的中繼資料檔案包含 <paths>
元素,而目錄包含 指定可分享的目錄 之後,您才能為檔案產生內容 URI。如果在未指定路徑中針對 File
呼叫 getUriForFile()
,您會收到 IllegalArgumentException
。
授予檔案權限
現在您有了要與其他應用程式共用檔案的內容 URI,接下來必須允許用戶端應用程式存取檔案。如要允許存取,請將內容 URI 新增至 Intent
,然後在 Intent
上設定權限標記,將權限授予用戶端應用程式。您授予的權限是暫時性的,而且會在接收應用程式的工作堆疊完成時自動失效。
下列程式碼片段說明如何設定檔案的讀取權限:
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); } ... } ... }); ... }
注意:如要透過臨時存取權安全地授予檔案存取權,呼叫 setFlags()
是唯一的方式。避免針對檔案的內容 URI 呼叫 Context.grantUriPermission()
方法,因為這個方法會授予只有呼叫 Context.revokeUriPermission()
才能撤銷的存取權。
不要使用 Uri.fromFile()
。如果您嘗試與使用者共用內容,這會強制接收應用程式具有 READ_EXTERNAL_STORAGE
權限;而在 Android 4.4 (API 級別 19) 以下版本中,應用程式則需要具備 WRITE_EXTERNAL_STORAGE
。此外,重要的共用目標 (例如 Gmail 應用程式) 沒有 READ_EXTERNAL_STORAGE
,導致這項呼叫失敗。您可以使用 URI 權限,授權其他應用程式存取特定 URI。URI 權限不適用於由 Uri.fromFile()
產生的 file:// URIs,但可在與內容供應器相關聯的 URI 上執行。此時,您可以加上 FileProvider
,如「檔案共用」中所述,而不必自行實作。
與提出要求的應用程式分享檔案
如要與要求該檔案的應用程式分享檔案,請將包含內容 URI 和權限的 Intent
傳遞至 setResult()
。您剛剛定義的 Activity
完成後,系統會將含有內容 URI 的 Intent
傳送至用戶端應用程式。以下程式碼片段說明如何執行此操作:
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); } } });
讓使用者在選擇檔案後,可立即返回用戶端應用程式。其中一種做法是提供勾號或「完成」按鈕。請使用按鈕的 android:onClick
屬性,將方法與按鈕建立關聯。在方法中呼叫 finish()
。例如:
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(); }
如需其他相關資訊,請參閱: