分享檔案

將應用程式設定為使用內容 URI 分享檔案後,您就可以回應其他應用程式對這些檔案的要求。如要回應這些要求,其中一種方式是提供伺服器應用程式的檔案選取介面,以便其他應用程式叫用。這個方法可讓用戶端應用程式讓使用者從伺服器應用程式選取檔案,然後接收所選檔案的內容 URI。

本課程將說明如何在應用程式中建立檔案選取 Activity 以回應檔案要求。

接收檔案要求

如要接收用戶端應用程式的檔案要求,並以內容 URI 回應,應用程式應提供檔案選取項目 Activity。用戶端應用程式會使用包含 ACTION_PICK 動作的 Intent 呼叫 startActivityForResult(),啟動這個 Activity。當用戶端應用程式呼叫 startActivityForResult() 時,應用程式會以使用者所選檔案的內容 URI 形式,將結果傳回用戶端應用程式。

如要瞭解如何在用戶端應用程式中實作檔案要求,請參閱「要求共用檔案」課程。

建立檔案選取活動

如要設定檔案選取 Activity,請先在資訊清單中指定 Activity,以及符合 ACTION_PICKCATEGORY_DEFAULTCATEGORY_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();
    }

如需其他相關資訊,請參閱: