هم‌رسانی فایل

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

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

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

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

برای آشنایی با نحوه پیاده‌سازی درخواست فایل در برنامه کارخواه، درس درخواست فایل هم‌رسانی‌شده را ببینید.

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

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

<manifest xmlns:android="http://schemas.android.com/apk/res/and>roid"
    ..<.
        a>pplication
        ...
  <          activity
                android:name=".FileSelectActivity"
           @     android:la>bel="File Se<lector" >
                inte<nt-filter
                    action
                        android:name>="android.intent<.action.PICK"/
                    category
                        android>:name="android.i<ntent.category.DEFAULT"/
                    category
                      >  android:name="<android.intent.category.OPEN>ABLE"/
                <    data android:mimeType=">text/plain"/<
             >       data a<ndroid:mi>meType="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 ima<geFi>les: ArrayFile
    // Array of filenames corresponding to imageFiles
    private lateinit var imageFi<lename>s: ArrayString

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

پاسخ به انتخاب فایل

وقتی کاربری فایل هم‌رسانی‌شده‌ای را انتخاب می‌کند، برنامه شما باید تعیین کند که کدام فایل انتخاب شده است و سپس «نشانی وب محتوا» را برای فایل تولید کند. ازآنجایی‌که Activity فهرست فایل‌های دردسترس را در ListView نمایش می‌دهد، وقتی کاربر روی نام فایلی کلیک می‌کند، سیستم روش onItemClick() را فرا می‌خواند که در آن می‌توانید فایل انتخاب‌شده را دریافت کنید.

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

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

  • اجازه هم‌رسانی فایل بین نمایه‌ها را نمی‌دهد.
  • لازم است برنامه شما در دستگاه‌های دارای Android نسخه ۴.۴ (سطح میانای برنامه‌سازی کاربردی ۱۹) یا پایین‌تر WRITE_EXTERNAL_STORAGE اجازه داشته باشد.
  • برنامه‌های دریافت‌کننده باید اجازه READ_EXTERNAL_STORAGE را داشته باشند، که در هدف‌های هم‌رسانی مهم، مثل Gmail، که این اجازه را ندارند، با خطا مواجه می‌شود.

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

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

تکه‌کد زیر نشان می‌دهد که چگونه فایل انتخاب‌شده را شناسایی کنید و «نشانی وب محتوا» را برای آن دریافت کنید:

کاتلین

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

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

اجازه‌های فایل را اعطا کنید

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

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

فایل را با برنامه درخواست‌کننده هم‌رسانی کنید

برای هم‌رسانی فایل با برنامه‌ای که آن را درخواست کرده است، Intent حاوی نشانی وب محتوا و اجازه‌ها را به setResult() ارسال کنید. وقتی Activity که تعریف کرده‌اید تمام شد، سیستم 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) {
                ...
                // 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);
                    }
                }
        });

به کاربران راهی ارائه دهید تا پس‌از انتخاب فایل، بلافاصله به برنامه کارخواه برگردند. یکی از راه‌های انجام این کار ارائه علامت تأیید یا دکمه انجام شد است. بااستفاده از مشخصه 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();
    }

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