به اشتراک گذاری یک فایل

هنگامی که برنامه خود را برای اشتراک گذاری فایل ها با استفاده از URI های محتوا تنظیم کردید، می توانید به درخواست های دیگر برنامه ها برای آن فایل ها پاسخ دهید. یکی از راه‌های پاسخ به این درخواست‌ها، ارائه یک رابط انتخاب فایل از برنامه سرور است که سایر برنامه‌ها می‌توانند آن را فراخوانی کنند. این رویکرد به یک برنامه مشتری اجازه می دهد تا به کاربران اجازه دهد فایلی را از برنامه سرور انتخاب کنند و سپس URI محتوای فایل انتخابی را دریافت کنند.

این درس به شما نشان می‌دهد که چگونه یک Activity انتخاب فایل در برنامه خود ایجاد کنید که به درخواست‌ها برای فایل‌ها پاسخ می‌دهد.

دریافت درخواست های فایل

برای دریافت درخواست فایل‌ها از برنامه‌های سرویس گیرنده و پاسخ دادن با URI محتوا، برنامه شما باید یک Activity انتخاب فایل ارائه دهد. برنامه های سرویس گیرنده این Activity با فراخوانی startActivityForResult() با یک Intent حاوی عمل ACTION_PICK شروع می کنند. هنگامی که برنامه مشتری startActivityForResult() فراخوانی می‌کند، برنامه شما می‌تواند نتیجه‌ای را در قالب یک URI محتوا برای فایلی که کاربر انتخاب کرده است، به برنامه مشتری بازگرداند.

برای یادگیری نحوه اجرای درخواست برای یک فایل در برنامه مشتری، به درس درخواست فایل اشتراکی مراجعه کنید.

ایجاد یک فعالیت انتخاب فایل

برای تنظیم Activity انتخاب فایل، با مشخص کردن Activity در مانیفست خود، همراه با یک فیلتر هدف که با عملکرد ACTION_PICK و دسته‌های CATEGORY_DEFAULT و CATEGORY_OPENABLE مطابقت دارد، شروع کنید. همچنین فیلترهای نوع MIME را برای فایل‌هایی که برنامه‌تان ارائه می‌کند به برنامه‌های دیگر اضافه کنید. قطعه زیر به شما نشان می دهد که چگونه فیلتر Activity و intent جدید را مشخص کنید:

<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 و پاسخ به انتخاب کاربر را نشان می دهد:

کاتلین

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

جاوا

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() را فرا می‌خواند که در آن می‌توانید فایل انتخابی را دریافت کنید.

هنگام استفاده از intent برای ارسال URI یک فایل از یک برنامه به برنامه دیگر، باید مراقب باشید که یک URI دریافت کنید که سایر برنامه ها بتوانند آن را بخوانند. انجام این کار در دستگاه‌های دارای Android 6.0 (سطح API 23) و نسخه‌های بعدی به مراقبت ویژه نیاز دارد زیرا تغییراتی در مدل مجوزها در آن نسخه از Android ایجاد شده است، به‌ویژه اینکه READ_EXTERNAL_STORAGE به یک مجوز خطرناک تبدیل شده است که ممکن است برنامه دریافت‌کننده فاقد آن باشد.

با در نظر گرفتن این ملاحظات، توصیه می‌کنیم از استفاده از Uri.fromFile() که دارای چندین اشکال است اجتناب کنید. این روش:

  • به اشتراک گذاری فایل در نمایه ها اجازه نمی دهد.
  • نیاز دارد که برنامه شما دارای مجوز WRITE_EXTERNAL_STORAGE در دستگاه‌های دارای Android نسخه 4.4 (سطح API 19) یا پایین‌تر باشد.
  • مستلزم آن است که برنامه های دریافت کننده مجوز READ_EXTERNAL_STORAGE را داشته باشند، که در اهداف اشتراک گذاری مهم مانند Gmail که این مجوز را ندارند، با شکست مواجه می شوند.

به جای استفاده از Uri.fromFile() ، می‌توانید از مجوزهای URI برای دسترسی سایر برنامه‌ها به URI‌های خاص استفاده کنید. در حالی که مجوزهای URI روی URI های file:// تولید شده توسط Uri.fromFile() کار نمی کنند، اما روی URI های مرتبط با ارائه دهندگان محتوا کار می کنند. FileProvider API می تواند به شما در ایجاد چنین URI کمک کند. این رویکرد همچنین با فایل‌هایی کار می‌کند که در حافظه خارجی نیستند، بلکه در حافظه محلی برنامه ارسال کننده قصد هستند.

در onItemClick() یک شی File برای نام فایل فایل انتخابی دریافت کنید و آن را به عنوان آرگومان به getUriForFile() ارسال کنید، همراه با مرجعی که در عنصر <provider> برای FileProvider مشخص کرده اید. URI محتوای حاصله شامل اعتبار، یک بخش مسیر مربوط به فهرست فایل (همانطور که در فراداده XML مشخص شده است) و نام فایل شامل پسوند آن است. چگونه FileProvider دایرکتوری‌ها را به بخش‌های مسیر بر اساس فراداده XML نگاشت می‌کند در بخش تعیین فهرست‌های قابل اشتراک‌گذاری توضیح داده شده است.

قطعه زیر به شما نشان می دهد که چگونه فایل انتخاب شده را شناسایی کنید و یک URI محتوا برای آن دریافت کنید:

کاتلین

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

جاوا

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

به یاد داشته باشید که فقط می‌توانید URI محتوا برای فایل‌هایی تولید کنید که در فهرستی قرار دارند که در فایل فراداده‌ای که حاوی عنصر <paths> است، مشخص شده‌اند، همانطور که در بخش تعیین فهرست‌های قابل اشتراک‌گذاری توضیح داده شد. اگر getUriForFile() برای File در مسیری که مشخص نکرده اید فراخوانی کنید، یک IllegalArgumentException دریافت خواهید کرد.

اعطای مجوز برای فایل

اکنون که یک URI محتوا برای فایلی دارید که می‌خواهید با برنامه دیگری به اشتراک بگذارید، باید به برنامه مشتری اجازه دسترسی به فایل را بدهید. برای اجازه دسترسی، با افزودن URI محتوا به Intent و سپس تنظیم پرچم‌های مجوز روی Intent ، به برنامه مشتری مجوز بدهید. مجوزهایی که اعطا می کنید موقتی هستند و با پایان یافتن پشته وظایف برنامه دریافت کننده به طور خودکار منقضی می شوند.

قطعه کد زیر نحوه تنظیم مجوز خواندن برای فایل را به شما نشان می دهد:

کاتلین

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

جاوا

    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() تنها راه برای اعطای دسترسی ایمن به فایل های شما با استفاده از مجوزهای دسترسی موقت است. از فراخوانی متد Context.grantUriPermission() برای URI محتوای فایل خودداری کنید، زیرا این روش به شما اجازه دسترسی می دهد که فقط با فراخوانی Context.revokeUriPermission() می توانید آن را لغو کنید.

از Uri.fromFile() استفاده نکنید. این برنامه‌های دریافت‌کننده را مجبور می‌کند مجوز READ_EXTERNAL_STORAGE را داشته باشند، اگر می‌خواهید بین کاربران اشتراک‌گذاری کنید، به هیچ وجه کار نمی‌کند، و در نسخه‌های Android پایین‌تر از 4.4 (سطح API 19)، برنامه شما نیاز به داشتن WRITE_EXTERNAL_STORAGE دارد. و اهداف اشتراک‌گذاری واقعاً مهم، مانند برنامه Gmail، READ_EXTERNAL_STORAGE ندارند، که باعث می‌شود این تماس با شکست مواجه شود. درعوض، می‌توانید از مجوزهای URI برای دسترسی سایر برنامه‌ها به URIهای خاص استفاده کنید. در حالی که مجوزهای URI همانطور که توسط Uri.fromFile() ایجاد می‌شود، روی URIهای file:// کار نمی‌کنند، اما روی URIهای مرتبط با ارائه‌دهندگان محتوا کار می‌کنند. به جای اجرای برنامه خود فقط برای این، می توانید و باید از FileProvider همانطور که در اشتراک گذاری فایل توضیح داده شده است استفاده کنید.

فایل را با برنامه درخواست کننده به اشتراک بگذارید

برای اشتراک‌گذاری فایل با برنامه‌ای که آن را درخواست کرده است، Intent حاوی URI محتوا و مجوزها را به setResult() ارسال کنید. هنگامی که Activity که به تازگی تعریف کرده اید به پایان رسید، سیستم Intent حاوی URI محتوا را به برنامه مشتری ارسال می کند. قطعه کد زیر نحوه انجام این کار را به شما نشان می دهد:

کاتلین

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

جاوا

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

راهی را در اختیار کاربران قرار دهید که پس از انتخاب فایل، فوراً به برنامه مشتری بازگردند. یکی از راه‌های انجام این کار، ارائه یک تیک یا دکمه Done است. یک روش را با استفاده از ویژگی android:onClick دکمه با دکمه مرتبط کنید. در متد، finish() فراخوانی کنید. به عنوان مثال:

کاتلین

    fun onDoneClick(v: View) {
        // Associate a method with the Done button
        finish()
    }

جاوا

    public void onDoneClick(View v) {
        // Associate a method with the Done button
        finish();
    }

برای اطلاعات بیشتر مرتبط به این موضوع مراجعه کنید: