שיתוף קובץ

אחרי שמגדירים את האפליקציה לשיתוף קבצים באמצעות מזהי URI של תוכן, אפשר להשיב לאפליקציות אחרות לבקשות לקבצים האלה. אחת הדרכים להגיב לבקשות האלה היא לבחור קובץ מאפליקציית השרת שאפליקציות אחרות יכולות להפעיל. הגישה הזאת מאפשרת כדי לאפשר למשתמשים לבחור קובץ מאפליקציית השרת ואז לקבל את ב-URI של התוכן.

בשיעור הזה מוסבר איך ליצור אפשרות לבחירת קבצים Activity באפליקציה שמגיב לבקשות לקבצים.

קבלת בקשות לקבצים

כדי לקבל בקשות לקבצים מאפליקציות לקוח ולהגיב עם URI של תוכן, האפליקציה צריכה מציינים בחירת קובץ Activity. אפליקציות לקוח מתחילות את הפעולה הזו Activity על ידי קריאה אל startActivityForResult() עם Intent שמכיל את הפעולה ACTION_PICK כשאפליקציית הלקוח מתקשרת startActivityForResult(), האפליקציה שלך יכולה מחזירה תוצאה לאפליקציית הלקוח, בצורת URI של תוכן עבור הקובץ שהמשתמש בחר.

כדי ללמוד איך להטמיע בקשה לקובץ באפליקציית לקוח, עיינו בשיעור שליחת בקשה לשליחת קובץ משותף.

יצירת פעילות לבחירת קבצים

כדי להגדיר את בחירת הקבצים Activity, צריך להתחיל בציון Activity במניפסט, יחד עם מסנן Intent שתואם לפעולה 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 ומגיבים לבחירה של המשתמש:

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 (רמת 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 שמשויכים לספקי תוכן. API של 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
            }
            ...
        }
        ...
    }

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 של תוכן לקובץ שאתם רוצים לשתף עם אפליקציה אחרת, אתם צריכים: מאפשרות לאפליקציית הלקוח לגשת לקובץ. כדי לאפשר גישה, צריך להעניק הרשאות לאפליקציית הלקוח על ידי הוספת ה-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 (רמת API 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)
            }
        }
    }

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. ב-method, מפעילים 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();
    }

מידע נוסף בנושא זמין במאמרים הבאים: