Udostępnianie pliku

Gdy skonfigurujesz aplikację do udostępniania plików za pomocą identyfikatorów URI treści, możesz odpowiadać na żądania innych aplikacji dotyczące tych plików. Jednym ze sposobów odpowiedzi na te żądania jest udostępnienie interfejsu wyboru plików z aplikacji serwera, który mogą wywoływać inne aplikacje. To podejście pozwala aplikacji klienckiej zezwolić użytkownikom na wybranie pliku z aplikacji serwera, a następnie otrzymanie identyfikatora URI treści wybranego pliku.

Z tej lekcji dowiesz się, jak utworzyć w aplikacji wybór plików Activity, który odpowiada na prośby o udostępnienie plików.

Odbieranie próśb o dostęp do plików

Aby odbierać żądania plików z aplikacji klienckich i odpowiadać za pomocą identyfikatora URI treści, aplikacja powinna udostępniać plik wyboru Activity. Aplikacje klienckie uruchamiają ten element Activity, wywołując metodę startActivityForResult() przy użyciu polecenia Intent zawierającego działanie ACTION_PICK. Gdy aplikacja kliencka wywołuje metodę startActivityForResult(), aplikacja może zwrócić do aplikacji klienckiej wynik w postaci identyfikatora URI treści dla wybranego pliku.

Aby dowiedzieć się, jak wdrożyć żądanie pliku w aplikacji klienckiej, zapoznaj się z lekcją Wysyłanie prośby o udostępnienie pliku.

Utwórz aktywność związaną z wyborem plików

Aby skonfigurować wybór plików Activity, najpierw podaj w pliku manifestu element Activity oraz filtr intencji pasujący do działania ACTION_PICK oraz kategorii CATEGORY_DEFAULT i CATEGORY_OPENABLE. Dodaj też filtry typów MIME w przypadku plików, które Twoja aplikacja udostępnia innym aplikacjom. Z tego fragmentu kodu dowiesz się, jak określić nowy filtr Activity i filtr intencji:

<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>

Zdefiniuj aktywność związaną z wyborem plików w kodzie

Następnie zdefiniuj podklasę Activity, która wyświetla pliki dostępne z katalogu files/images/ aplikacji w pamięci wewnętrznej i pozwala użytkownikowi wybrać odpowiedni plik. Ten fragment kodu pokazuje, jak zdefiniować obiekt Activity i odpowiedzieć na wybór użytkownika:

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
         */
         ...
    }
    ...
}

Odpowiadanie na wybór pliku

Gdy użytkownik wybierze udostępniony plik, aplikacja musi określić, który plik został wybrany, a następnie wygenerować dla niego identyfikator URI treści. Activity wyświetla listę dostępnych plików w lokalizacji ListView, więc gdy użytkownik kliknie nazwę pliku, system wywoła metodę onItemClick(), dzięki której możesz pobrać wybrany plik.

Jeśli korzystasz z intencji wysyłania identyfikatora URI pliku z jednej aplikacji do drugiej, musisz uzyskać identyfikator URI, który mogą odczytać inne aplikacje. Wymaga to szczególnej ostrożności na urządzeniach z Androidem 6.0 (poziom interfejsu API 23) i nowszym, ze względu na zmiany w modelu uprawnień w danej wersji Androida, a w szczególności do READ_EXTERNAL_STORAGEuprawnień niebezpiecznych, których może brakować aplikacja odbierająca.

Mając to na uwadze, zalecamy unikanie użycia właściwości Uri.fromFile(), która ma kilka wad. Metoda:

  • Nie zezwala na udostępnianie plików między profilami.
  • Wymaga, aby aplikacja miała uprawnienia WRITE_EXTERNAL_STORAGE na urządzeniach z Androidem 4.4 (poziom interfejsu API 19) lub niższym.
  • Wymaga, aby aplikacje odbierające miały uprawnienie READ_EXTERNAL_STORAGE, które nie działa w przypadku ważnych celów udostępniania (takich jak Gmail).

Zamiast korzystać z Uri.fromFile(), możesz użyć uprawnień identyfikatora URI, aby przyznać innym aplikacjom dostęp do określonych identyfikatorów URI. Uprawnienia dotyczące identyfikatorów URI nie działają w przypadku identyfikatorów URI file:// wygenerowanych przez Uri.fromFile(), ale działają na identyfikatorach URI powiązanych z dostawcami treści. W tworzeniu takich identyfikatorów URI może pomóc interfejs API FileProvider. Ta metoda działa też w przypadku plików, które nie znajdują się w pamięci zewnętrznej, ale w pamięci lokalnej aplikacji wysyłającej intencję.

W funkcji onItemClick() pobierz obiekt File na nazwę wybranego pliku i przekaż go jako argument do getUriForFile() wraz z uprawnieniem podanym w elemencie <provider> dotyczącym elementu FileProvider. Powstały identyfikator URI treści zawiera informacje o urzędzie, segment ścieżki odpowiadający katalogowi pliku (określone w metadanych XML) oraz nazwę pliku wraz z jego rozszerzeniem. Sposób, w jaki FileProvider mapuje katalogi na segmenty ścieżek na podstawie metadanych XML, został opisany w sekcji Określanie katalogów udostępnianych.

Poniższy fragment kodu pokazuje, jak wykryć wybrany plik i uzyskać dla niego identyfikator URI treści:

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());
                }
                ...
            }
        });
        ...
    }

Pamiętaj, że identyfikatory URI treści możesz generować tylko dla plików znajdujących się w katalogu określonym w pliku metadanych zawierającym element <paths> zgodnie z opisem w sekcji Określanie katalogów udostępnianych. Jeśli wywołasz getUriForFile() dla elementu File w nieokreślonej ścieżce, otrzymasz IllegalArgumentException.

Przyznawanie uprawnień do pliku

Kiedy masz już identyfikator URI treści pliku, który chcesz udostępnić innej aplikacji, musisz zezwolić aplikacji klienckiej na dostęp do tego pliku. Aby zezwolić na dostęp, przyznaj uprawnienia aplikacji klienckiej, dodając identyfikator URI treści do obiektu Intent, a następnie ustawiając flagi uprawnień w Intent. Przyznane uprawnienia są tymczasowe i wygasają automatycznie po zakończeniu stosu zadań aplikacji odbierającej.

Ten fragment kodu pokazuje, jak ustawić uprawnienia do odczytu pliku:

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);
                }
                ...
             }
             ...
        });
    ...
    }

Uwaga: wywołanie setFlags() to jedyny sposób na bezpieczne przyznanie dostępu do plików za pomocą tymczasowych uprawnień dostępu. Unikaj wywoływania metody Context.grantUriPermission() w celu podania identyfikatora URI treści pliku, ponieważ ta metoda przyznaje dostęp, który możesz odwołać tylko przez wywołanie Context.revokeUriPermission().

Nie używaj Uri.fromFile(). Wymusza to, aby odbierane aplikacje miały uprawnienie READ_EXTERNAL_STORAGE, w ogóle nie będą działać, jeśli będziesz próbować udostępniać treści różnym użytkownikom, a w wersjach Androida starszych niż 4.4 (poziom interfejsu API 19) aplikacja będzie wymagać korzystania z WRITE_EXTERNAL_STORAGE. Szczególnie ważne cele udostępniania, takie jak aplikacja Gmail, nie mają parametru READ_EXTERNAL_STORAGE, co powoduje niepowodzenie wywołania. Zamiast tego możesz użyć uprawnień dotyczących identyfikatorów URI, aby przyznać innym aplikacjom dostęp do określonych identyfikatorów URI. Choć uprawnienia dotyczące identyfikatorów URI nie działają w przypadku identyfikatorów URI file:// generowanych przez usługę Uri.fromFile(), działają one na identyfikatorach URI powiązanych z dostawcami treści. Zamiast implementować własne komponenty, możesz i powinien używać funkcji FileProvider zgodnie z opisem w sekcji Udostępnianie plików.

Udostępnij plik aplikacji, która wysłała żądanie

Aby udostępnić plik aplikacji, która go zażądała, przekaż plik Intent zawierający identyfikator URI treści i uprawnienia do funkcji setResult(). Po zakończeniu zdefiniowanego przed chwilą Activity system wysyła do aplikacji klienckiej identyfikator Intent zawierający identyfikator URI treści. Aby to zrobić:

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);
                    }
                }
        });

Zapewnij użytkownikom możliwość natychmiastowego powrotu do aplikacji klienckiej po wybraniu pliku. Możesz to zrobić, używając znacznika wyboru lub przycisku Gotowe. Powiąż metodę z przyciskiem za pomocą atrybutu android:onClick przycisku. W metodzie wywołaj finish(). Na przykład:

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();
    }

Dodatkowe informacje na ten temat znajdziesz tutaj: