مشاركة ملف

بعد إعداد تطبيقك لمشاركة الملفات باستخدام معرّفات الموارد المنتظمة (URI) للمحتوى، يمكنك الردّ على طلبات التطبيقات الأخرى لهذه الملفات. وتتمثل إحدى طرق الاستجابة لهذه الطلبات في توفير واجهة لاختيار الملفات من تطبيق الخادم يمكن للتطبيقات الأخرى استدعاءها. ويسمح هذا الأسلوب لتطبيق العميل بأن يسمح للمستخدمين باختيار ملف من تطبيق الخادم ثم استلام معرّف الموارد المنتظم (URI) لمحتوى الملف المحدّد.

يوضّح لك هذا الدرس كيفية إنشاء مجموعة ملفات Activity في تطبيقك للاستجابة لطلبات الملفات.

تلقّي طلبات الملفات

لتلقّي طلبات الملفات من تطبيقات العملاء والرد باستخدام معرّف موارد منتظم (URI) للمحتوى، يجب أن يوفّر تطبيقك اختيار ملف Activity. تبدأ تطبيقات العملاء عملية Activity هذه من خلال استدعاء startActivityForResult() مع Intent الذي يتضمّن الإجراء ACTION_PICK. عندما يتصل تطبيق العميل بـ 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
         */
        ...
    }
    ...
}

جافا

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 يمكن للتطبيقات الأخرى قراءته. ويتطلّب ذلك على الأجهزة التي تعمل بالإصدار 6.0 من نظام التشغيل Android (المستوى 23 من واجهة برمجة التطبيقات) والإصدارات الأحدث عناية خاصة بسبب التغييرات في نموذج الأذونات في ذلك الإصدار من Android، لا سيّما منح READ_EXTERNAL_STORAGE إذنًا خطيرًا، والذي قد لا يملكه التطبيق المستقبِل.

بأخذ هذه الاعتبارات في الاعتبار، ننصحك بتجنّب استخدام السمة Uri.fromFile() التي تتضمّن العديد من السلبيات. هذه الطريقة:

  • لا يسمح بمشاركة الملفات بين الملفات الشخصية.
  • يتطلّب أن يكون تطبيقك حاصلاً على إذن WRITE_EXTERNAL_STORAGE على الأجهزة التي تعمل بالإصدار 4.4 من نظام التشغيل Android (مستوى واجهة برمجة التطبيقات 19) أو الإصدارات الأقدم.
  • يتطلّب استلام التطبيقات الحصول على إذن "READ_EXTERNAL_STORAGE"، والذي لن يفشل مع أهداف المشاركة المهمة، مثل Gmail، التي لا تمتلك هذا الإذن.

بدلاً من استخدام Uri.fromFile()، يمكنك استخدام أذونات معرّف الموارد المنتظم (URI) لمنح التطبيقات الأخرى إمكانية الوصول إلى معرّفات موارد منتظمة (URI) محددة. على الرغم من أنّ أذونات معرّف الموارد المنتظم (URI) لا تعمل على معرّفات الموارد المنتظمة (URI) file:// التي أنشأها Uri.fromFile()، فإنّها تعمل على معرّفات الموارد المنتظمة (URI) المرتبطة بمزوّدي المحتوى. ويمكن أن تساعدك واجهة برمجة تطبيقات FileProvider في إنشاء معرّفات الموارد المنتظمة (URI) هذه. تعمل هذه الطريقة أيضًا مع الملفات غير الموجودة في وحدة تخزين خارجية، ولكن في مساحة التخزين المحلية للتطبيق الذي يرسل الغرض.

في onItemClick()، احصل على الكائن File لاسم الملف المحدد وانقله كوسيطة إلى getUriForFile()، إلى جانب مرجع التصديق الذي حددته في العنصر <provider> من أجل FileProvider. إنّ معرّف الموارد المنتظم (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
            }
            ...
        }
        ...
    }

جافا

    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. تكون الأذونات التي تمنحها مؤقتة وتنتهي صلاحيتها تلقائيًا عند الانتهاء من حزمة مهام التطبيق المُستلِم.

يوضِّح لك مقتطف الرمز التالي كيفية ضبط إذن القراءة للملف:

لغة 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)
                ...
            }
            ...
        }
        ...
    }

جافا

    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 (مستوى واجهة برمجة التطبيقات 19)، سيتطلب التطبيق توفُّر WRITE_EXTERNAL_STORAGE. أهداف المشاركة المهمة حقًا، مثل تطبيق Gmail، لا تتضمن READ_EXTERNAL_STORAGE، ما يتسبب في تعذُّر هذا الاتصال. وبدلاً من ذلك، يمكنك استخدام أذونات معرّف الموارد المنتظم (URI) لمنح التطبيقات الأخرى إذن الوصول إلى معرّفات موارد منتظمة (URI) معيّنة. على الرغم من أنّ أذونات معرّف الموارد المنتظم (URI) لا تعمل على معرِّفات الموارد المنتظمة (URI) لـ file:// كما يتم إنشاؤها من خلال Uri.fromFile()، فإنّها تعمل على بروتوكول Uris المرتبط بموفِّري المحتوى. وبدلاً من تنفيذ الطريقة الخاصة بك لهذا الغرض، يمكنك واستخدام FileProvider كما هو موضّح في مشاركة الملفات.

مشاركة الملف مع التطبيق الذي يطلبه

لمشاركة الملف مع التطبيق الذي طلبه، أدخِل Intent الذي يتضمّن معرّف الموارد المنتظم (URI) للمحتوى والأذونات إلى setResult(). عند انتهاء Activity التي حدّدتها للتو، يرسل النظام Intent الذي يتضمن معرّف الموارد المنتظم (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, _ ->
            ...
            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(). على سبيل المثال:

لغة Kotlin

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

للحصول على معلومات إضافية ذات صلة، راجع: