مشاركة ملف

بعد إعداد تطبيقك لمشاركة الملفات باستخدام معرّفات الموارد المنتظمة (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
         */
        ...
    }
    ...
}

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

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

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

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

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

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

تذكَّر أنّه يمكنك فقط إنشاء معرّفات موارد منتظمة (URI) للمحتوى للملفات الموجودة في دليل حددته في ملف البيانات الوصفية الذي يحتوي على العنصر <paths>، كما هو موضّح في القسم تحديد الأدلة القابلة للمشاركة. إذا استدعيت السمة getUriForFile() للخاصية File في مسار لم تحدّده، ستتلقّى IllegalArgumentException.

منح الأذونات للملف

الآن بعد أن حصلت على معرّف موارد منتظم (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() هو الطريقة الوحيدة لمنح إذن الوصول إلى ملفاتك بأمان باستخدام أذونات الوصول المؤقتة. وتجنَّب استدعاء طريقة 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 الذي يتضمن معرّف الموارد المنتظم للمحتوى إلى تطبيق العميل. ويوضّح لك مقتطف الرمز التالي كيفية إجراء ذلك:

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

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