קובצי הרחבת APK

ב-Google Play נדרש שקובץ ה-APK הדחוס שהמשתמשים מורידים לא יהיה גדול מ-100MB. ברוב האפליקציות, זה מספיק מקום לכל הקוד והנכסים של האפליקציה. עם זאת, חלק מהאפליקציות צריכות יותר מקום לגרפיקה באיכות גבוהה, לקובצי מדיה או לנכסים גדולים אחרים. בעבר, אם גודל ההורדה הדחוס של האפליקציה חרג מ-100MB, הייתם צריכים לארח ולהוריד את המשאבים הנוספים בעצמכם כשהמשתמש פותח את האפליקציה. אירוח והצגה של הקבצים הנוספים עלולים להיות יקרים, וחוויית המשתמש לרוב לא אידיאלית. כדי להקל עליכם את התהליך הזה ולשפר את חוויית המשתמשים, Google Play מאפשרת לכם לצרף שני קובצי הרחבה גדולים שמשלימים את קובץ ה-APK.

‫Google Play מאחסן את קובצי ההרחבה של האפליקציה ומספק אותם למכשיר ללא עלות. קבצי ההרחבה נשמרים במיקום האחסון המשותף של המכשיר (כרטיס ה-SD או מחיצה שאפשר לחבר ל-USB, שנקרא גם אחסון חיצוני), שבו האפליקציה יכולה לגשת אליהם. ברוב המכשירים, Google Play מוריד את קובצי ההרחבה בו-זמנית עם הורדת ה-APK, כך שהאפליקציה כוללת את כל מה שהיא צריכה כשהמשתמש פותח אותה בפעם הראשונה. עם זאת, במקרים מסוימים, האפליקציה צריכה להוריד את הקבצים מ-Google Play כשהיא מופעלת.

אם אתם רוצים להימנע משימוש בקובצי הרחבה, והגודל של האפליקציה שלכם אחרי דחיסה גדול מ-‎100 MB, אתם צריכים להעלות את האפליקציה באמצעות Android App Bundles, שמאפשרים גודל של עד ‎500 MB אחרי דחיסה. בנוסף, כשמשתמשים בחבילות אפליקציות, יצירת ה-APK והחתימה עליו מתבצעות ב-Google Play. כך המשתמשים מורידים קובצי APK שעברו אופטימיזציה ומכילים רק את הקוד והמשאבים שנדרשים להפעלת האפליקציה. אתם לא צריכים ליצור, לחתום ולנהל כמה קובצי APK או קובצי הרחבה, והמשתמשים מקבלים הורדות קטנות יותר שעברו אופטימיזציה.

סקירה כללית

בכל פעם שמעלים APK באמצעות Google Play Console, יש אפשרות להוסיף קובץ הרחבה אחד או שניים ל-APK. כל קובץ יכול להיות בגודל של עד 2GB, ובכל פורמט שתבחרו. עם זאת, אנחנו ממליצים להשתמש בקובץ דחוס כדי לחסוך ברוחב הפס במהלך ההורדה. מבחינה רעיונית, לכל קובץ הרחבה יש תפקיד שונה:

  • קובץ ההרחבות הראשי הוא קובץ ההרחבות העיקרי של משאבים נוספים שנדרשים לאפליקציה.
  • קובץ ההרחבות patch הוא אופציונלי ומיועד לעדכונים קטנים של קובץ ההרחבות הראשי.

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

עם זאת, גם אם עדכון לאפליקציה דורש רק קובץ הרחבות עדכונים חדש, עדיין צריך להעלות קובץ APK חדש עם versionCode מעודכן במניפסט. (ב-Play Console אי אפשר להעלות קובץ הרחבה ל-APK קיים).

הערה: קובץ הרחבות העדכונים זהה מבחינה סמנטית לקובץ ההרחבות הראשי – אפשר להשתמש בכל אחד מהקבצים איך שרוצים.

פורמט שם הקובץ

כל קובץ הרחבה שמעלים יכול להיות בכל פורמט שתבחרו (ZIP,‏ PDF,‏ MP4 וכו'). אפשר גם להשתמש בכלי JOBB כדי להצפין ולהוסיף קבוצה של קובצי משאבים ותיקונים לקבוצה הזו. לא משנה מה סוג הקובץ, מערכת Google Play מתייחסת אליהם כאובייקטים בינאריים אטומים ומשנה את השם של הקבצים לפי הסכימה הבאה:

[main|patch].<expansion-version>.<package-name>.obb

יש שלושה רכיבים לתוכנית הזו:

main או patch
מציין אם הקובץ הוא קובץ ההרחבות הראשי או קובץ הרחבות התיקונים. לכל APK יכול להיות רק קובץ ראשי אחד וקובץ עדכונים אחד.
<expansion-version>
This is an integer that matches the version code of the APK with which the expansion is first associated (it matches the app's android:versionCode value).

ההדגשה של המילה 'ראשון' חשובה כי למרות שב-Play Console אפשר לעשות שימוש חוזר בקובץ הרחבה שהועלה עם קובץ APK חדש, השם של קובץ ההרחבה לא משתנה – הוא נשאר עם הגרסה שהוקצתה לו כשקובץ ההרחבה הועלה לראשונה.

<package-name>
שם החבילה של האפליקציה בסגנון Java.

לדוגמה, נניח שגרסת ה-APK היא 314159 ושם החבילה הוא com.example.app. אם תעלו קובץ הרחבה ראשי, שם הקובץ ישתנה ל:

main.314159.com.example.app.obb

מיקום אחסון

כשמורידים קובצי הרחבה ל-Google Play במכשיר, הם נשמרים במיקום האחסון המשותף של המערכת. כדי שהאפליקציה תפעל בצורה תקינה, אסור למחוק, להעביר או לשנות את השם של קובצי ההרחבה. אם האפליקציה צריכה לבצע את ההורדה מ-Google Play עצמו, צריך לשמור את הקבצים באותו מיקום בדיוק.

השיטה getObbDir() מחזירה את המיקום הספציפי של קובצי ההרחבה בפורמט הבא:

<shared-storage>/Android/obb/<package-name>/
  • <shared-storage> היא הנתיב לנפח האחסון המשותף, שזמין מ-getExternalStorageDirectory().
  • <package-name> הוא שם החבילה של האפליקציה בסגנון Java, שזמין ב-getPackageName().

בספרייה הזו אף פעם לא יהיו יותר משני קובצי הרחבה לכל אפליקציה. אחד מהם הוא קובץ ההרחבות הראשי והשני הוא קובץ הרחבות העדכונים (אם יש צורך). גרסאות קודמות נדרסות כשמעדכנים את האפליקציה באמצעות קובצי הרחבה חדשים. החל מ-Android 4.4 (רמת API‏ 19), אפליקציות יכולות לקרוא קובצי הרחבה של OBB בלי הרשאת אחסון חיצוני. עם זאת, בחלק מההטמעות של Android 6.0 (רמת API‏ 23) ואילך עדיין נדרשת הרשאה, ולכן תצטרכו להצהיר על ההרשאה READ_EXTERNAL_STORAGE בקובץ מניפסט של אפליקציה ולבקש הרשאה בזמן הריצה באופן הבא:

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

ב-Android בגרסה 6 ואילך, צריך לבקש הרשאה לאחסון חיצוני בזמן הריצה. עם זאת, בחלק מההטמעות של Android לא נדרשת הרשאה לקריאת קובצי OBB. בקטע הקוד הבא אפשר לראות איך בודקים אם יש הרשאת קריאה לפני שמבקשים הרשאה לגישה לאחסון חיצוני:

Kotlin

val obb = File(obb_filename)
var open_failed = false

try {
    BufferedReader(FileReader(obb)).also { br ->
        ReadObbFile(br)
    }
} catch (e: IOException) {
    open_failed = true
}

if (open_failed) {
    // request READ_EXTERNAL_STORAGE permission before reading OBB file
    ReadObbFileWithPermission()
}

Java

File obb = new File(obb_filename);
 boolean open_failed = false;

 try {
     BufferedReader br = new BufferedReader(new FileReader(obb));
     open_failed = false;
     ReadObbFile(br);
 } catch (IOException e) {
     open_failed = true;
 }

 if (open_failed) {
     // request READ_EXTERNAL_STORAGE permission before reading OBB file
     ReadObbFileWithPermission();
 }

אם אתם חייבים לפתוח את התוכן של קובצי ההרחבות, אל תמחקו את קובצי ההרחבות OBB לאחר מכן ואל תשמרו את הנתונים שחולצו באותה ספריה. צריך לשמור את הקבצים שחולצו בספרייה שצוינה על ידי getExternalFilesDir(). עם זאת, אם אפשר, עדיף להשתמש בפורמט קובץ הרחבה שמאפשר לקרוא ישירות מהקובץ, במקום לדרוש פריקה של הנתונים. לדוגמה, סיפקנו פרויקט ספרייה בשם APK Expansion Zip Library שקורא את הנתונים ישירות מקובץ ה-ZIP.

זהירות: בניגוד לקובצי APK, המשתמש ואפליקציות אחרות יכולים לקרוא את כל הקבצים שנשמרים באחסון המשותף.

טיפ: אם אתם אורזים קובצי מדיה ב-ZIP, אתם יכולים להשתמש בקריאות להפעלת מדיה בקבצים עם פקדי היסט ואורך (כמו MediaPlayer.setDataSource() ו-SoundPool.load()) בלי לפתוח את ה-ZIP. כדי שהפעולה הזו תצליח, אסור לבצע דחיסה נוספת של קובצי המדיה כשיוצרים את חבילות ה-ZIP. לדוגמה, כשמשתמשים בכלי zip, צריך להשתמש באפשרות -n כדי לציין את סיומות הקבצים שלא צריך לדחוס:
zip -n .mp4;.ogg main_expansion media_files

תהליך ההורדה

ברוב המקרים, קובצי ההרחבה מורדים ונשמרים ב-Google Play בו-זמנית עם קובץ ה-APK במכשיר. עם זאת, במקרים מסוימים, מערכת Google Play לא יכולה להוריד את קובצי ההרחבה, או שהמשתמש מחק קובצי הרחבה שהורדו בעבר. כדי לטפל במצבים האלה, האפליקציה צריכה להיות מסוגלת להוריד את הקבצים בעצמה כשהפעילות הראשית מתחילה, באמצעות כתובת URL שסופקה על ידי Google Play.

תהליך ההורדה באופן כללי נראה כך:

  1. המשתמש בוחר להתקין את האפליקציה מ-Google Play.
  2. אם אפשר להוריד את קובצי ההרחבה מ-Google Play (ברוב המכשירים), הם יורדים יחד עם קובץ ה-APK.

    אם מערכת Google Play לא מצליחה להוריד את קובצי ההרחבה, היא מורידה רק את קובץ ה-APK.

  3. כשהמשתמש מפעיל את האפליקציה, האפליקציה צריכה לבדוק אם קובצי ההרחבה כבר נשמרו במכשיר.
    1. אם כן, האפליקציה מוכנה לשימוש.
    2. אם לא, האפליקציה צריכה להוריד את קובצי ההרחבה באמצעות HTTP מ-Google Play. האפליקציה שלך צריכה לשלוח בקשה ללקוח Google Play באמצעות שירות רישוי האפליקציות של Google Play, שמגיב עם השם, גודל הקובץ וכתובת ה-URL של כל קובץ הרחבה. אחרי שמקבלים את המידע הזה, מורידים את הקבצים ושומרים אותם במיקום האחסון המתאים.

זהירות: חשוב מאוד לכלול את הקוד הנדרש להורדת קובצי ההרחבה מ-Google Play, למקרה שהקובצים לא נמצאים כבר במכשיר כשהאפליקציה מופעלת. כפי שמוסבר בקטע הבא בנושא הורדת קובצי ההרחבה, העמדנו לרשותך ספרייה שמפשטת מאוד את התהליך הזה ומבצעת את ההורדה משירות עם כמות מינימלית של קוד ממך.

רשימת משימות למפתחים

סיכום המשימות שצריך לבצע כדי להשתמש בקובצי הרחבה באפליקציה:

  1. קודם צריך לקבוע אם גודל ההורדה הדחוס של האפליקציה צריך להיות גדול מ-100MB. המקום באחסון יקר, ולכן כדאי לשמור על גודל ההורדה הכולל קטן ככל האפשר. אם האפליקציה שלכם משתמשת ביותר מ-100MB כדי לספק כמה גרסאות של הנכסים הגרפיים עבור כמה צפיפויות מסך, כדאי לפרסם במקום זאת כמה קובצי APK שכל אחד מהם מכיל רק את הנכסים שנדרשים למסכים שהוא מטרגט. כדי לקבל את התוצאות הטובות ביותר כשמפרסמים ב-Google Play, מומלץ להעלות קובץ Android App Bundle, שכולל את כל הקוד והמשאבים המהודרים של האפליקציה, אבל דוחה את יצירת ה-APK והחתימה שלו ל-Google Play.
  2. קובעים אילו משאבי אפליקציה להפריד מקובץ ה-APK ואורזים אותם בקובץ לשימוש כקובץ ההרחבה הראשי.

    בדרך כלל, כדאי להשתמש בקובץ הרחבות עדכונים רק כשמבצעים עדכונים לקובץ הרחבות ראשי. עם זאת, אם המשאבים חורגים ממגבלת 2GB של קובץ ההרחבות הראשי, אפשר להשתמש בקובץ עדכונים לשאר הנכסים.

  3. מפתחים את האפליקציה כך שהיא תשתמש במשאבים מקובצי ההרחבה במיקום האחסון המשותף במכשיר.

    חשוב לזכור שאסור למחוק, להעביר או לשנות את השם של קובצי ההרחבה.

    אם האפליקציה לא דורשת פורמט ספציפי, מומלץ ליצור קובצי ZIP לקובצי ההרחבה ואז לקרוא אותם באמצעות ספריית ה-ZIP של הרחבת ה-APK.

  4. מוסיפים לפעילות הראשית של האפליקציה לוגיקה שבודקת אם קובצי ההרחבה נמצאים במכשיר בזמן ההפעלה. אם הקבצים לא נמצאים במכשיר, צריך להשתמש בשירות app Licensing של Google Play כדי לבקש כתובות URL לקובצי ההרחבה, ואז להוריד ולשמור אותם.

    כדי לצמצם באופן משמעותי את כמות הקוד שצריך לכתוב ולהבטיח חוויית משתמש טובה במהלך ההורדה, מומלץ להשתמש בספריית Downloader כדי להטמיע את התנהגות ההורדה.

    אם אתם יוצרים שירות הורדה משלכם במקום להשתמש בספרייה, חשוב לדעת שאסור לשנות את השם של קובצי ההרחבה, וצריך לשמור אותם במיקום האחסון המתאים.

אחרי שמסיימים לפתח את האפליקציה, פועלים לפי ההוראות במדריך בנושא בדיקת קובצי ההרחבה.

כללים ומגבלות

הוספה של קובצי הרחבה ל-APK היא תכונה שזמינה כשמעלים את האפליקציה באמצעות Play Console. כשמעלים את האפליקציה בפעם הראשונה או כשמעדכנים אפליקציה שמשתמשת בקובצי הרחבה, חשוב להכיר את הכללים והמגבלות הבאים:

  1. כל קובץ הרחבה יכול להיות בגודל של עד 2GB.
  2. כדי להוריד את קובצי ההרחבה מ-Google Play, המשתמש צריך לרכוש את האפליקציה מ-Google Play. מערכת Google Play לא תספק את כתובות ה-URL של קובצי ההרחבה אם האפליקציה הותקנה באמצעים אחרים.
  3. כשמבצעים את ההורדה מתוך האפליקציה, כתובת ה-URL ש-Google Play מספקת לכל קובץ היא ייחודית לכל הורדה, והתוקף של כל אחת מהן פג זמן קצר אחרי שהיא מועברת לאפליקציה.
  4. אם מעדכנים את האפליקציה באמצעות קובץ APK חדש או מעלים כמה קובצי APK לאותה אפליקציה, אפשר לבחור קובצי הרחבה שהועלו עבור קובץ APK קודם. השם של קובץ ההרחבה לא משתנה – הוא נשאר כמו הגרסה שהתקבלה על ידי קובץ ה-APK שאליו הקובץ שויך במקור.
  5. אם אתם משתמשים בקובצי הרחבה בשילוב עם מספר קובצי APK כדי לספק קובצי הרחבה שונים למכשירים שונים, אתם עדיין צריכים להעלות קובצי APK נפרדים לכל מכשיר כדי לספק ערך versionCode ייחודי ולהגדיר מסננים שונים לכל קובץ APK.
  6. אי אפשר לפרסם עדכון לאפליקציה רק על ידי שינוי קובצי ההרחבה. צריך להעלות קובץ APK חדש כדי לעדכן את האפליקציה. אם השינויים שביצעתם קשורים רק לנכסים בקובצי ההרחבה, אפשר לעדכן את קובץ ה-APK פשוט על ידי שינוי versionCode (ואולי גם versionName).
  7. אל תשמרו נתונים אחרים בספרייה של obb/. אם אתם צריכים לפתוח חלק מהנתונים, שמרו אותם במיקום שצוין על ידי getExternalFilesDir().
  8. אל תמחקו או תשנו את השם של קובץ ההרחבה .obb (אלא אם אתם מבצעים עדכון). פעולה כזו תגרום ל-Google Play (או לאפליקציה עצמה) להוריד את קובץ ההרחבה שוב ושוב.
  9. כשמעדכנים קובץ הרחבה באופן ידני, צריך למחוק את קובץ ההרחבה הקודם.

הורדת קובצי ההרחבה

ברוב המקרים, קובצי ההרחבה מורדים ונשמרים במכשיר על ידי Google Play באותו הזמן שבו קובץ ה-APK מותקן או מתעדכן. כך קובצי ההרחבה יהיו זמינים כשהאפליקציה תופעל בפעם הראשונה. עם זאת, במקרים מסוימים האפליקציה צריכה להוריד את קובצי ההרחבה בעצמה על ידי שליחת בקשה לכתובת URL שמופיעה בתגובה משירות הרישוי לאפליקציות של Google Play.

הלוגיקה הבסיסית שצריך כדי להוריד את קובצי ההרחבה היא כזו:

  1. כשהאפליקציה מופעלת, היא מחפשת את קובצי ההרחבה במיקום האחסון המשותף (בספרייה Android/obb/<package-name>/).
    1. אם קובצי ההרחבה נמצאים שם, הכול בסדר והאפליקציה יכולה להמשיך.
    2. אם קובצי ההרחבה לא נמצאים שם:
      1. שולחים בקשה באמצעות רישוי האפליקציות של Google Play כדי לקבל את שמות הקבצים, הגדלים וכתובות ה-URL של קובצי ההרחבה של האפליקציה.
      2. משתמשים בכתובות ה-URL שסופקו על ידי Google Play כדי להוריד את קובצי ההרחבה ולשמור אותם. חובה לשמור את הקבצים במיקום האחסון המשותף (Android/obb/<package-name>/) ולהשתמש בשם הקובץ המדויק שמופיע בתגובה של Google Play.

        הערה: כתובת ה-URL ש-Google Play מספקת לקובצי ההרחבה שלכם היא ייחודית לכל הורדה, והתוקף שלה פג זמן קצר אחרי שהיא מועברת לאפליקציה.

אם האפליקציה שלך היא בחינם (לא אפליקציה בתשלום), כנראה שלא השתמשת בשירות רישוי אפליקציות. הוא מיועד בעיקר לאכיפת מדיניות הרישוי של האפליקציה שלכם, ולוודא שלמשתמש יש את הזכות להשתמש באפליקציה (הוא שילם עליה בצורה חוקית ב-Google Play). כדי לאפשר את הפונקציונליות של קובץ ההרחבה, שדרגנו את שירות הרישוי כך שיספק תגובה לאפליקציה שלכם, שתכלול את כתובת ה-URL של קובצי ההרחבה של האפליקציה שמתארחים ב-Google Play. לכן, גם אם האפליקציה שלכם מוצעת למשתמשים בחינם, אתם צריכים לכלול את License Verification Library (ספריית אימות הרישיון, LVL) כדי להשתמש בקובצי הרחבה של APK. כמובן, אם האפליקציה שלכם היא בחינם, אתם לא צריכים לאכוף אימות של הרישיון – אתם רק צריכים שהספרייה תבצע את הבקשה שמחזירה את כתובת ה-URL של קובצי ההרחבה.

הערה: בין אם האפליקציה שלכם היא בחינם או לא, מערכת Google Play מחזירה את כתובות ה-URL של קובץ ההרחבה רק אם המשתמש רכש את האפליקציה שלכם מ-Google Play.

בנוסף ל-LVL, צריך להשתמש בקבוצת קוד שמורידה את קובצי ההרחבה דרך חיבור HTTP ושומרת אותם במיקום המתאים באחסון המשותף של המכשיר. כשמטמיעים את התהליך הזה באפליקציה, יש כמה בעיות שכדאי לקחת בחשבון:

  • יכול להיות שאין במכשיר מספיק מקום לקובצי ההרחבה, ולכן צריך לבדוק לפני שמתחילים את ההורדה ולהזהיר את המשתמש אם אין מספיק מקום.
  • הורדות של קבצים צריכות להתבצע בשירות רקע כדי למנוע חסימה של האינטראקציה עם המשתמש, ולאפשר למשתמש לצאת מהאפליקציה בזמן שההורדה מסתיימת.
  • יכולות להתרחש מגוון שגיאות במהלך הבקשה וההורדה, ואתם צריכים לטפל בהן בצורה נכונה.
  • החיבור לרשת יכול להשתנות במהלך ההורדה, ולכן צריך לטפל בשינויים כאלה. אם ההורדה מופסקת, צריך לחדש אותה כשמתאפשר.
  • בזמן שההורדה מתבצעת ברקע, צריך להציג התראה שמציינת את התקדמות ההורדה, מודיעה למשתמש כשהיא מסתיימת ומחזירה את המשתמש לאפליקציה כשבוחרים בה.

כדי לפשט את העבודה הזו בשבילכם, יצרנו את ספריית ההורדות, שמבקשת את כתובות ה-URL של קובצי ההרחבה דרך שירות הרישוי, מורידה את קובצי ההרחבה, מבצעת את כל המשימות שצוינו למעלה ואפילו מאפשרת להשהות את הפעילות ולחדש את ההורדה. אם מוסיפים את ספריית Downloader וכמה ווים של קוד לאפליקציה, כמעט כל העבודה שנדרשת להורדת קובצי ההרחבה כבר מקודדת בשבילכם. לכן, כדי לספק את חוויית המשתמש הטובה ביותר עם מינימום מאמץ מצדכם, מומלץ להשתמש בספריית ההורדות כדי להוריד את קובצי ההרחבה. בקטעים הבאים מוסבר איך לשלב את הספרייה באפליקציה.

אם אתם מעדיפים לפתח פתרון משלכם להורדת קובצי ההרחבה באמצעות כתובות ה-URL של Google Play, אתם צריכים לפעול לפי התיעוד בנושא רישוי לשימוש באפליקציה כדי לבצע בקשת רישיון, ואז לאחזר את השמות, הגדלים וכתובות ה-URL של קובצי ההרחבה מפרטים נוספים על התגובה למודעה. מומלץ להשתמש במחלקה APKExpansionPolicy (שכלולה בספריית אימות הרישיון) כמדיניות הרישוי, שמקבלת את השמות, הגדלים וכתובות ה-URL של קובצי ההרחבה משירות הרישוי.

מידע על ספריית ההורדות

כדי להשתמש בקובצי הרחבה של APK באפליקציה ולספק את חוויית המשתמש הטובה ביותר עם מינימום מאמץ מצדכם, מומלץ להשתמש בספריית ההורדות שכלולה בחבילת ספריית ההרחבות של APK ב-Google Play. הספרייה הזו מורידה את קובצי ההרחבות בשירות הפועל ברקע, מציגה למשתמש התראה עם סטטוס ההורדה, מטפלת באובדן חיבור לרשת, מחדשת את ההורדה כשזה אפשרי ועוד.

כדי להטמיע הורדות של קובצי הרחבות באמצעות ספריית ההורדות, צריך רק:

  • להרחיב מחלקת משנה מיוחדת Service ומחלקת משנה BroadcastReceiver שכל אחת מהן דורשת רק כמה שורות קוד.
  • מוסיפים לוגיקה לפעילות הראשית שבודקת אם קובצי ההרחבה כבר הורדו, ואם לא, מפעילה את תהליך ההורדה ומציגה ממשק משתמש של התקדמות.
  • מטמיעים ממשק קריאה חוזרת עם כמה שיטות בפעילות הראשית שמקבלת עדכונים על התקדמות ההורדה.

בקטעים הבאים מוסבר איך להגדיר את האפליקציה באמצעות ספריית ההורדות.

הכנה לשימוש בספריית ההורדות

כדי להשתמש בספריית ההורדות, צריך להוריד שתי חבילות מ-SDK Manager ולהוסיף את הספריות המתאימות לאפליקציה.

קודם פותחים את Android SDK Manager (Tools (כלים) > SDK Manager), ובקטע Appearance & Behavior (מראה והתנהגות) > System Settings (הגדרות מערכת) > Android SDK, בוחרים בכרטיסייה SDK Tools כדי לבחור ולהוריד:

  • חבילת Google Play Licensing Library
  • חבילת Google Play APK Expansion Library

יוצרים מודול ספרייה חדש לספריית אימות הרישיונות ולספריית ההורדות. לכל ספרייה:

  1. לוחצים על קובץ > חדש > מודול חדש.
  2. בחלון Create New Module (יצירת מודול חדש), בוחרים באפשרות Android Library (ספריית Android) ואז באפשרות Next (הבא).
  3. מציינים שם של אפליקציה או ספרייה, כמו Google Play License Library או Google Play Downloader Library, בוחרים רמת SDK מינימלית ואז לוחצים על סיום.
  4. בוחרים באפשרות File > Project Structure (קובץ > מבנה הפרויקט).
  5. בוחרים בכרטיסייה מאפיינים ובקטע מאגר ספרייה, מזינים את הספרייה מהספרייה <sdk>/extras/google/ (play_licensing/ לספריית אימות הרישיון או play_apk_expansion/downloader_library/ לספריית ההורדה).
  6. לוחצים על אישור כדי ליצור את המודול החדש.

הערה: ספריית ההורדה תלויה בספריית אימות הרישיון. חשוב להוסיף את ספריית אימות הרישיון למאפייני הפרויקט של ספריית ההורדה.

לחלופין, אפשר לעדכן את הפרויקט כך שיכלול את הספריות משורת פקודה:

  1. משנים את הספרייה לספרייה <sdk>/tools/.
  2. מריצים את הפקודה android update project עם האפשרות --library כדי להוסיף לפרויקט את ספריית ה-LVL ואת ספריית ההורדה. לדוגמה:
    android update project --path ~/Android/MyApp \
    --library ~/android_sdk/extras/google/market_licensing \
    --library ~/android_sdk/extras/google/market_apk_expansion/downloader_library
    

אחרי שמוסיפים לאפליקציה את ספריית אימות הרישיונות ואת ספריית ההורדות, אפשר לשלב במהירות את האפשרות להוריד קובצי הרחבה מ-Google Play. הפורמט שבו בוחרים לקובצי ההרחבה והאופן שבו קוראים אותם מנפח האחסון המשותף הם הטמעה נפרדת שצריך לשקול בהתאם לצרכים של האפליקציה.

טיפ: חבילת ההרחבה של ה-APK כוללת אפליקציה לדוגמה שמראה איך להשתמש בספריית ההורדות באפליקציה. הדוגמה משתמשת בספרייה של צד שלישי שזמינה בחבילת ההרחבה של ה-APK שנקראת APK Expansion Zip Library. אם אתם מתכננים להשתמש בקובצי ZIP לקובצי ההרחבה, מומלץ להוסיף לאפליקציה גם את ספריית ה-ZIP של קובצי ההרחבה של ה-APK. מידע נוסף זמין בקטע שימוש בספריית ה-ZIP של קובצי ההרחבה של ה-APK שבהמשך.

הצהרה על הרשאות משתמשים

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

<manifest ...>
    <!-- Required to access Google Play Licensing -->
    <uses-permission android:name="com.android.vending.CHECK_LICENSE" />

    <!-- Required to download files from Google Play -->
    <uses-permission android:name="android.permission.INTERNET" />

    <!-- Required to keep CPU alive while downloading files
        (NOT to keep screen awake) -->
    <uses-permission android:name="android.permission.WAKE_LOCK" />

    <!-- Required to poll the state of the network connection
        and respond to changes -->
    <uses-permission
        android:name="android.permission.ACCESS_NETWORK_STATE" />

    <!-- Required to check whether Wi-Fi is enabled -->
    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>

    <!-- Required to read and write the expansion files on shared storage -->
    <uses-permission
        android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    ...
</manifest>

הערה: כברירת מחדל, Downloader Library דורשת רמת API 4, אבל APK Expansion Zip Library דורשת רמת API 5.

הטמעה של שירות ההורדה

כדי לבצע הורדות ברקע, ספריית ההורדות מספקת מחלקת משנה משלה Service בשם DownloaderService, שצריך להרחיב אותה. בנוסף להורדת קובצי ההרחבה בשבילכם, DownloaderService גם:

  • רושם BroadcastReceiver שמאזין לשינויים בקישוריות לרשת של המכשיר (השידור CONNECTIVITY_ACTION) כדי להשהות את ההורדה כשצריך (למשל, בגלל אובדן קישוריות) ולחדש את ההורדה כשמתאפשר (הושגה קישוריות).
  • מתזמן אזעקה של RTC_WAKEUP כדי לנסות שוב להוריד במקרים שבהם השירות נסגר.
  • יוצרת Notification מותאם אישית שמציג את התקדמות ההורדה ואת כל השגיאות או השינויים בסטטוס.
  • מאפשר לאפליקציה להשהות את ההורדה ולהמשיך אותה באופן ידני.
  • הכלי מוודא שהנפח המשותף מותקן וזמין, שהקבצים לא קיימים כבר ושיש מספיק מקום, וכל זה לפני הורדת קובצי ההרחבה. אם אחד מהתנאים האלה לא מתקיים, המערכת שולחת למשתמש הודעה.

כל מה שצריך לעשות הוא ליצור כיתה באפליקציה שמרחיבה את הכיתה DownloaderService ולשנות שלוש שיטות כדי לספק פרטי האפליקציה ספציפיים:

getPublicKey()
הפונקציה הזו צריכה להחזיר מחרוזת שהיא מפתח RSA ציבורי בקידוד Base64 של חשבון בעל האפליקציה, שזמין בדף הפרופיל ב-Play Console (ראו הגדרה של רישוי).
getSALT()
הפונקציה הזו צריכה להחזיר מערך של בייטים אקראיים שמשמשים את מערכת הרישוי Policy ליצירת Obfuscator. ה-salt מבטיח שהקובץ המוסתר SharedPreferencesשבו נשמרים נתוני הרישוי יהיה ייחודי ובלתי ניתן לגילוי.
getAlarmReceiverClassName()
הפונקציה הזו צריכה להחזיר את שם המחלקה של BroadcastReceiver באפליקציה שאמורה לקבל את ההתראה שמציינת שצריך להפעיל מחדש את ההורדה (מה שעשוי לקרות אם שירות ההורדה מפסיק באופן בלתי צפוי).

לדוגמה, הנה הטמעה מלאה של DownloaderService:

Kotlin

// You must use the public key belonging to your publisher account
const val BASE64_PUBLIC_KEY = "YourLVLKey"
// You should also modify this salt
val SALT = byteArrayOf(
        1, 42, -12, -1, 54, 98, -100, -12, 43, 2,
        -8, -4, 9, 5, -106, -107, -33, 45, -1, 84
)

class SampleDownloaderService : DownloaderService() {

    override fun getPublicKey(): String = BASE64_PUBLIC_KEY

    override fun getSALT(): ByteArray = SALT

    override fun getAlarmReceiverClassName(): String = SampleAlarmReceiver::class.java.name
}

Java

public class SampleDownloaderService extends DownloaderService {
    // You must use the public key belonging to your publisher account
    public static final String BASE64_PUBLIC_KEY = "YourLVLKey";
    // You should also modify this salt
    public static final byte[] SALT = new byte[] { 1, 42, -12, -1, 54, 98,
            -100, -12, 43, 2, -8, -4, 9, 5, -106, -107, -33, 45, -1, 84
    };

    @Override
    public String getPublicKey() {
        return BASE64_PUBLIC_KEY;
    }

    @Override
    public byte[] getSALT() {
        return SALT;
    }

    @Override
    public String getAlarmReceiverClassName() {
        return SampleAlarmReceiver.class.getName();
    }
}

הודעה: צריך לעדכן את הערך BASE64_PUBLIC_KEY כך שיהיה המפתח הציבורי ששייך לחשבון של בעל התוכן הדיגיטלי. אפשר למצוא את המפתח ב-Developer Console בקטע 'פרטי הפרופיל'. הפעולה הזו נדרשת גם כשבודקים את ההורדות.

חשוב להצהיר על השירות בקובץ המניפסט:

<app ...>
    <service android:name=".SampleDownloaderService" />
    ...
</app>

הטמעה של מקלט ההתראות

כדי לעקוב אחרי התקדמות ההורדה של הקבצים ולהפעיל מחדש את ההורדה אם צריך, DownloaderService מתזמן אזעקה של RTC_WAKEUP שמעבירה Intent אל BroadcastReceiver באפליקציה. צריך להגדיר את BroadcastReceiver כדי לקרוא ל-API מספריית ההורדות שבודק את סטטוס ההורדה ומפעיל אותה מחדש אם צריך.

פשוט צריך לשנות את ה-method‏ onReceive() כדי להפעיל את DownloaderClientMarshaller.startDownloadServiceIfRequired().

לדוגמה:

Kotlin

class SampleAlarmReceiver : BroadcastReceiver() {

    override fun onReceive(context: Context, intent: Intent) {
        try {
            DownloaderClientMarshaller.startDownloadServiceIfRequired(
                    context,
                    intent,
                    SampleDownloaderService::class.java
            )
        } catch (e: PackageManager.NameNotFoundException) {
            e.printStackTrace()
        }
    }
}

Java

public class SampleAlarmReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        try {
            DownloaderClientMarshaller.startDownloadServiceIfRequired(context,
                intent, SampleDownloaderService.class);
        } catch (NameNotFoundException e) {
            e.printStackTrace();
        }
    }
}

שימו לב שזו המחלקה שצריך להחזיר את השם שלה בפונקציה getAlarmReceiverClassName() של השירות (ראו את הקטע הקודם).

אל תשכחו להצהיר על המקלט בקובץ המניפסט:

<app ...>
    <receiver android:name=".SampleAlarmReceiver" />
    ...
</app>

התחלת ההורדה

הפעילות הראשית באפליקציה (זו שמופעלת על ידי סמל מרכז האפליקציות) אחראית לאימות של קובצי ההרחבה במכשיר ולהפעלת ההורדה אם הם לא נמצאים בו.

כדי להתחיל את ההורדה באמצעות ספריית ההורדות, צריך לבצע את הפעולות הבאות:

  1. בודקים אם הקבצים הורדו.

    ספריית ההורדה כוללת כמה ממשקי API במחלקה Helper כדי לעזור בתהליך הזה:

    • getExpansionAPKFileName(Context, c, boolean mainFile, int versionCode)
    • doesFileExist(Context c, String fileName, long fileSize)

    לדוגמה, באפליקציה לדוגמה שמופיעה בחבילת ההרחבה של ה-APK, מתבצעת קריאה ל-method הבא ב-method onCreate() של הפעילות כדי לבדוק אם קובצי ההרחבה כבר קיימים במכשיר:

    Kotlin

    fun expansionFilesDelivered(): Boolean {
        xAPKS.forEach { xf ->
            Helpers.getExpansionAPKFileName(this, xf.isBase, xf.fileVersion).also { fileName ->
                if (!Helpers.doesFileExist(this, fileName, xf.fileSize, false))
                    return false
            }
        }
        return true
    }

    Java

    boolean expansionFilesDelivered() {
        for (XAPKFile xf : xAPKS) {
            String fileName = Helpers.getExpansionAPKFileName(this, xf.isBase,
                xf.fileVersion);
            if (!Helpers.doesFileExist(this, fileName, xf.fileSize, false))
                return false;
        }
        return true;
    }

    במקרה הזה, כל אובייקט XAPKFile מכיל את מספר הגרסה ואת גודל הקובץ של קובץ הרחבה מוכר, וערך בוליאני שקובע אם זה קובץ ההרחבה הראשי. (פרטים נוספים מופיעים בכיתה SampleDownloaderActivity של אפליקציית הדוגמה).

    אם השיטה הזו מחזירה false, האפליקציה צריכה להתחיל את ההורדה.

  2. מתחילים את ההורדה על ידי קריאה לשיטה הסטטית DownloaderClientMarshaller.startDownloadServiceIfRequired(Context c, PendingIntent notificationClient, Class<?> serviceClass).

    השיטה מקבלת את הפרמטרים הבאים:

    • context: Context של האפליקציה.
    • notificationClient: PendingIntent כדי להתחיל את הפעילות הראשית. הערך הזה משמש ב-Notification שה-DownloaderService יוצר כדי להציג את התקדמות ההורדה. כשהמשתמש בוחר את ההתראה, המערכת מפעילה את PendingIntent שציינתם כאן, והפעילות שבה מוצג תהליך ההורדה אמורה להיפתח (בדרך כלל זו אותה פעילות שהתחילה את ההורדה).
    • serviceClass: אובייקט Class של ההטמעה של DownloaderService, שנדרש כדי להפעיל את השירות ולהתחיל את ההורדה אם צריך.

    השיטה מחזירה מספר שלם שמציין אם נדרשת הורדה. הערכים האפשריים הם:

    • NO_DOWNLOAD_REQUIRED: מוחזר אם הקבצים כבר קיימים או אם הורדה כבר מתבצעת.
    • LVL_CHECK_REQUIRED: מוחזר אם נדרש אימות רישיון כדי לקבל את כתובות ה-URL של קובץ ההרחבה.
    • DOWNLOAD_REQUIRED: מוחזר אם כתובות ה-URL של קובץ ההרחבה כבר ידועות, אבל לא הורדו.

    ההתנהגות של LVL_CHECK_REQUIRED ושל DOWNLOAD_REQUIRED זהה, ובדרך כלל לא צריך לדאוג לגביהם. בפעילות הראשית שקוראת ל-startDownloadServiceIfRequired(), אפשר פשוט לבדוק אם התגובה היא NO_DOWNLOAD_REQUIRED או לא. אם התשובה היא כל דבר אחר מלבד NO_DOWNLOAD_REQUIRED, ספריית ההורדות מתחילה את ההורדה, ועליך לעדכן את ממשק המשתמש של הפעילות כדי להציג את התקדמות ההורדה (ראו את השלב הבא). אם התשובה היא NO_DOWNLOAD_REQUIRED, הקבצים זמינים והאפליקציה יכולה להתחיל.

    לדוגמה:

    Kotlin

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
    
        // Check if expansion files are available before going any further
        if (!expansionFilesDelivered()) {
            val pendingIntent =
                    // Build an Intent to start this activity from the Notification
                    Intent(this, MainActivity::class.java).apply {
                        flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
                    }.let { notifierIntent ->
                        PendingIntent.getActivity(
                                this,
                                0,
                                notifierIntent,
                                PendingIntent.FLAG_UPDATE_CURRENT
                        )
                    }
    
    
            // Start the download service (if required)
            val startResult: Int = DownloaderClientMarshaller.startDownloadServiceIfRequired(
                    this,
                    pendingIntent,
                    SampleDownloaderService::class.java
            )
            // If download has started, initialize this activity to show
            // download progress
            if (startResult != DownloaderClientMarshaller.NO_DOWNLOAD_REQUIRED) {
                // This is where you do set up to display the download
                // progress (next step)
                ...
                return
            } // If the download wasn't necessary, fall through to start the app
        }
        startApp() // Expansion files are available, start the app
    }

    Java

    @Override
    public void onCreate(Bundle savedInstanceState) {
        // Check if expansion files are available before going any further
        if (!expansionFilesDelivered()) {
            // Build an Intent to start this activity from the Notification
            Intent notifierIntent = new Intent(this, MainActivity.getClass());
            notifierIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK |
                                    Intent.FLAG_ACTIVITY_CLEAR_TOP);
            ...
            PendingIntent pendingIntent = PendingIntent.getActivity(this, 0,
                    notifierIntent, PendingIntent.FLAG_UPDATE_CURRENT);
    
            // Start the download service (if required)
            int startResult =
                DownloaderClientMarshaller.startDownloadServiceIfRequired(this,
                            pendingIntent, SampleDownloaderService.class);
            // If download has started, initialize this activity to show
            // download progress
            if (startResult != DownloaderClientMarshaller.NO_DOWNLOAD_REQUIRED) {
                // This is where you do set up to display the download
                // progress (next step)
                ...
                return;
            } // If the download wasn't necessary, fall through to start the app
        }
        startApp(); // Expansion files are available, start the app
    }
  3. כשהשיטה startDownloadServiceIfRequired() מחזירה ערך שונה מ-NO_DOWNLOAD_REQUIRED, יוצרים מופע של IStub על ידי קריאה ל-DownloaderClientMarshaller.CreateStub(IDownloaderClient client, Class<?> downloaderService). ה-IStub מספק קישור בין הפעילות שלכם לבין שירות ההורדה, כך שהפעילות שלכם מקבלת קריאות חוזרות לגבי התקדמות ההורדה.

    כדי ליצור מופע של IStub על ידי קריאה ל-CreateStub(), צריך להעביר לו הטמעה של הממשק IDownloaderClient וההטמעה של DownloaderService. בקטע הבא בנושא קבלת נתוני התקדמות ההורדה מוסבר על הממשק IDownloaderClient, שבדרך כלל צריך להטמיע במחלקה Activity כדי לעדכן את ממשק המשתמש של הפעילות כשמצב ההורדה משתנה.

    מומלץ להתקשר אל CreateStub() כדי ליצור מופע של IStub במהלך השיטה onCreate() של הפעילות, אחרי ש-startDownloadServiceIfRequired() מתחיל את ההורדה.

    לדוגמה, בדוגמת הקוד הקודמת של onCreate(), אפשר להגיב לתוצאה של startDownloadServiceIfRequired() כך:

    Kotlin

            // Start the download service (if required)
            val startResult = DownloaderClientMarshaller.startDownloadServiceIfRequired(
                    this@MainActivity,
                    pendingIntent,
                    SampleDownloaderService::class.java
            )
            // If download has started, initialize activity to show progress
            if (startResult != DownloaderClientMarshaller.NO_DOWNLOAD_REQUIRED) {
                // Instantiate a member instance of IStub
                downloaderClientStub =
                        DownloaderClientMarshaller.CreateStub(this, SampleDownloaderService::class.java)
                // Inflate layout that shows download progress
                setContentView(R.layout.downloader_ui)
                return
            }

    Java

            // Start the download service (if required)
            int startResult =
                DownloaderClientMarshaller.startDownloadServiceIfRequired(this,
                            pendingIntent, SampleDownloaderService.class);
            // If download has started, initialize activity to show progress
            if (startResult != DownloaderClientMarshaller.NO_DOWNLOAD_REQUIRED) {
                // Instantiate a member instance of IStub
                downloaderClientStub = DownloaderClientMarshaller.CreateStub(this,
                        SampleDownloaderService.class);
                // Inflate layout that shows download progress
                setContentView(R.layout.downloader_ui);
                return;
            }

    אחרי שהשיטה onCreate() מחזירה ערך, הפעילות מקבלת קריאה ל-onResume(), ושם צריך לקרוא ל-connect() ב-IStub ולהעביר אליו את Context של האפליקציה. לעומת זאת, צריך להפעיל את disconnect() בקריאה החוזרת (callback) של הפעילות onStop().

    Kotlin

    override fun onResume() {
        downloaderClientStub?.connect(this)
        super.onResume()
    }
    
    override fun onStop() {
        downloaderClientStub?.disconnect(this)
        super.onStop()
    }

    Java

    @Override
    protected void onResume() {
        if (null != downloaderClientStub) {
            downloaderClientStub.connect(this);
        }
        super.onResume();
    }
    
    @Override
    protected void onStop() {
        if (null != downloaderClientStub) {
            downloaderClientStub.disconnect(this);
        }
        super.onStop();
    }

    הפעלת connect() ב-IStub קושרת את הפעילות שלכם ל-DownloaderService, כך שהפעילות שלכם מקבלת קריאות חוזרות לגבי שינויים במצב ההורדה דרך הממשק IDownloaderClient.

התקדמות ההורדה

כדי לקבל עדכונים לגבי התקדמות ההורדה ולקיים אינטראקציה עם DownloaderService, צריך להטמיע את הממשק IDownloaderClient של ספריית ההורדות. בדרך כלל, הפעילות שמשמשת להתחלת ההורדה צריכה להטמיע את הממשק הזה כדי להציג את התקדמות ההורדה ולשלוח בקשות לשירות.

אלה שיטות הממשק הנדרשות עבור IDownloaderClient:

onServiceConnected(Messenger m)
אחרי שיוצרים מופע של IStub בפעילות, מתקבלת קריאה לשיטה הזו, שמעבירה אובייקט Messenger שמקושר למופע של DownloaderService. כדי לשלוח בקשות לשירות, כמו השהיה והמשך של הורדות, צריך להתקשר אל DownloaderServiceMarshaller.CreateProxy() כדי לקבל את הממשק IDownloaderService שמחובר לשירות.

דוגמה להטמעה מומלצת:

Kotlin

private var remoteService: IDownloaderService? = null
...

override fun onServiceConnected(m: Messenger) {
    remoteService = DownloaderServiceMarshaller.CreateProxy(m).apply {
        downloaderClientStub?.messenger?.also { messenger ->
            onClientUpdated(messenger)
        }
    }
}

Java

private IDownloaderService remoteService;
...

@Override
public void onServiceConnected(Messenger m) {
    remoteService = DownloaderServiceMarshaller.CreateProxy(m);
    remoteService.onClientUpdated(downloaderClientStub.getMessenger());
}

אחרי האתחול של האובייקט IDownloaderService, אפשר לשלוח פקודות לשירות ההורדה, כמו השהיה והמשך של ההורדה (requestPauseDownload() ו-requestContinueDownload()).

onDownloadStateChanged(int newState)
שירות ההורדה קורא לפונקציה הזו כשמתרחש שינוי בסטטוס ההורדה, למשל כשההורדה מתחילה או מסתיימת.

הערך newState יהיה אחד מכמה ערכים אפשריים שצוינו באחת מהקבועים של המחלקה IDownloaderClient.STATE_*

כדי לספק למשתמשים הודעה מועילה, אפשר לבקש מחרוזת תואמת לכל מצב באמצעות קריאה ל-Helpers.getDownloaderStringResourceIDFromState(). הפונקציה הזו מחזירה את מזהה המשאב של אחת מהמחרוזות שצורפו לספריית Downloader. לדוגמה, המחרוזת 'ההורדה מושהית כי אתה נמצא בנדידה' מתאימה ל-STATE_PAUSED_ROAMING.

onDownloadProgress(DownloadProgressInfo progress)
שירות ההורדה קורא לפונקציה הזו כדי לספק אובייקט DownloadProgressInfo, שמתאר מידע שונה על התקדמות ההורדה, כולל הזמן המשוער שנותר, המהירות הנוכחית, ההתקדמות הכוללת והסך הכולל, כדי שתוכלו לעדכן את ממשק המשתמש של התקדמות ההורדה.

טיפ: דוגמאות לקריאות חוזרות (callback) כאלה שמעדכנות את ממשק המשתמש של התקדמות ההורדה מופיעות ב-SampleDownloaderActivity באפליקציה לדוגמה שמסופקת עם חבילת ההרחבה של ה-APK.

אלה כמה שיטות ציבוריות בממשק IDownloaderService שעשויות להיות שימושיות:

requestPauseDownload()
ההורדה מושהית.
requestContinueDownload()
ממשיך הורדה שהושהתה.
setDownloadFlags(int flags)
הגדרה של העדפות המשתמשים לגבי סוגי הרשתות שבהן אפשר להוריד את הקבצים. ההטמעה הנוכחית תומכת בדגל אחד, FLAGS_DOWNLOAD_OVER_CELLULAR, אבל אפשר להוסיף עוד. כברירת מחדל, הדגל הזה לא מופעל, ולכן המשתמש צריך להיות מחובר ל-Wi-Fi כדי להוריד קבצים להרחבה. אפשר לספק העדפת משתמש כדי לאפשר הורדות ברשת הסלולרית. במקרה כזה, אפשר להתקשר למספרים הבאים:

Kotlin

remoteService = DownloaderServiceMarshaller.CreateProxy(m).apply {
    ...
    setDownloadFlags(IDownloaderService.FLAGS_DOWNLOAD_OVER_CELLULAR)
}

Java

remoteService
    .setDownloadFlags(IDownloaderService.FLAGS_DOWNLOAD_OVER_CELLULAR);

שימוש ב-APKExpansionPolicy

אם אתם מחליטים ליצור שירות הורדה משלכם במקום להשתמש בספריית ההורדות של Google Play, אתם עדיין צריכים להשתמש ב-APKExpansionPolicy שמופיע בספריית אימות הרישיון. המחלקה APKExpansionPolicy כמעט זהה למחלקה ServerManagedPolicy (זמינה בספריית אימות הרישיון של Google Play), אבל היא כוללת טיפול נוסף בתוספים של תגובת קובץ ההרחבה של ה-APK.

הערה: אם משתמשים ב-Downloader Library כמו שמתואר בקטע הקודם, הספרייה מבצעת את כל האינטראקציה עם APKExpansionPolicy, כך שלא צריך להשתמש במחלקה הזו ישירות.

המחלקות כוללות שיטות שיעזרו לכם לקבל את המידע הדרוש על קובצי ההרחבה הזמינים:

  • getExpansionURLCount()
  • getExpansionURL(int index)
  • getExpansionFileName(int index)
  • getExpansionFileSize(int index)

למידע נוסף על אופן השימוש ב-APKExpansionPolicy כשלא משתמשים בספריית ההורדות, אפשר לעיין במסמכי הוספת רישוי לאפליקציה, שבהם מוסבר איך להטמיע מדיניות רישוי כמו זו.

קריאת קובץ ההרחבות

אחרי שקובצי ההרחבה של ה-APK נשמרים במכשיר, אופן הקריאה של הקבצים תלוי בסוג הקובץ שבו השתמשתם. כמו שמוסבר בסקירה הכללית, קבצי ההרחבה יכולים להיות מכל סוג שרוצים, אבל השמות שלהם משתנים לפי פורמט מסוים של שמות קבצים והם נשמרים בתיקייה <shared-storage>/Android/obb/<package-name>/.

לא משנה איך קוראים את הקבצים, תמיד צריך קודם לוודא שהאחסון החיצוני זמין לקריאה. יכול להיות שהמשתמש חיבר את האחסון למחשב באמצעות USB או שהוציא את כרטיס ה-SD.

הערה: כשמפעילים את האפליקציה, תמיד צריך לבדוק אם נפח האחסון החיצוני זמין וניתן לקריאה על ידי קריאה ל-getExternalStorageState(). הפונקציה מחזירה אחד מכמה מחרוזות אפשריות שמייצגות את מצב האחסון החיצוני. כדי שהאפליקציה תוכל לקרוא את הערך המוחזר, הוא צריך להיות MEDIA_MOUNTED.

איך מקבלים את שמות הקבצים

כפי שמתואר בסקירה הכללית, קובצי ההרחבה של ה-APK נשמרים בפורמט מסוים של שם הקובץ:

[main|patch].<expansion-version>.<package-name>.obb

כדי לקבל את המיקום והשמות של קובצי ההרחבה, צריך להשתמש בשיטות getExternalStorageDirectory() ו-getPackageName() כדי ליצור את הנתיב לקבצים.

זו שיטה שאפשר להשתמש בה באפליקציה כדי לקבל מערך שמכיל את הנתיב המלא לשני קובצי ההרחבה:

Kotlin

fun getAPKExpansionFiles(ctx: Context, mainVersion: Int, patchVersion: Int): Array<String> {
    val packageName = ctx.packageName
    val ret = mutableListOf<String>()
    if (Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED) {
        // Build the full path to the app's expansion files
        val root = Environment.getExternalStorageDirectory()
        val expPath = File(root.toString() + EXP_PATH + packageName)

        // Check that expansion file path exists
        if (expPath.exists()) {
            if (mainVersion > 0) {
                val strMainPath = "$expPath${File.separator}main.$mainVersion.$packageName.obb"
                val main = File(strMainPath)
                if (main.isFile) {
                    ret += strMainPath
                }
            }
            if (patchVersion > 0) {
                val strPatchPath = "$expPath${File.separator}patch.$mainVersion.$packageName.obb"
                val main = File(strPatchPath)
                if (main.isFile) {
                    ret += strPatchPath
                }
            }
        }
    }
    return ret.toTypedArray()
}

Java

// The shared path to all app expansion files
private final static String EXP_PATH = "/Android/obb/";

static String[] getAPKExpansionFiles(Context ctx, int mainVersion,
      int patchVersion) {
    String packageName = ctx.getPackageName();
    Vector<String> ret = new Vector<String>();
    if (Environment.getExternalStorageState()
          .equals(Environment.MEDIA_MOUNTED)) {
        // Build the full path to the app's expansion files
        File root = Environment.getExternalStorageDirectory();
        File expPath = new File(root.toString() + EXP_PATH + packageName);

        // Check that expansion file path exists
        if (expPath.exists()) {
            if ( mainVersion > 0 ) {
                String strMainPath = expPath + File.separator + "main." +
                        mainVersion + "." + packageName + ".obb";
                File main = new File(strMainPath);
                if ( main.isFile() ) {
                        ret.add(strMainPath);
                }
            }
            if ( patchVersion > 0 ) {
                String strPatchPath = expPath + File.separator + "patch." +
                        mainVersion + "." + packageName + ".obb";
                File main = new File(strPatchPath);
                if ( main.isFile() ) {
                        ret.add(strPatchPath);
                }
            }
        }
    }
    String[] retArray = new String[ret.size()];
    ret.toArray(retArray);
    return retArray;
}

כדי להפעיל את השיטה הזו, מעבירים לה את האפליקציה Contextואת הגרסה של קובץ ההרחבה הרצוי.

יש הרבה דרכים לקבוע את מספר הגרסה של קובץ ההרחבה. דרך פשוטה אחת היא לשמור את הגרסה בקובץ SharedPreferences כשההורדה מתחילה, על ידי שליחת שאילתה לשם קובץ ההרחבה באמצעות השיטה getExpansionFileName(int index) של המחלקה APKExpansionPolicy. אחר כך תוכלו לקרוא את קובץ SharedPreferences כדי לקבל את קוד הגרסה כשאתם רוצים לגשת לקובץ ההרחבה.

מידע נוסף על קריאה מהאחסון המשותף זמין במסמכי התיעוד בנושא אחסון נתונים.

שימוש בספריית ה-Zip של קובץ ההרחבה ל-APK

חבילת ההרחבה של Google Market Apk כוללת ספרייה בשם APK Expansion Zip Library (שנמצאת ב-<sdk>/extras/google/google_market_apk_expansion/zip_file/). זו ספרייה אופציונלית שעוזרת לקרוא את קובצי ההרחבה כששומרים אותם כקובצי ZIP. השימוש בספרייה הזו מאפשר לקרוא בקלות משאבים מקובצי ההרחבה של ZIP כמערכת קבצים וירטואלית.

ספריית ה-Zip של הרחבת ה-APK כוללת את המחלקות וממשקי ה-API הבאים:

APKExpansionSupport
מספק כמה שיטות לגישה לשמות של קובצי הרחבה ולקובצי ZIP:
getAPKExpansionFiles()
אותה שיטה שמוצגת למעלה, שמחזירה את נתיב הקובץ המלא לשני קובצי ההרחבה.
getAPKExpansionZipFile(Context ctx, int mainVersion, int patchVersion)
מחזירה ZipResourceFile שמייצג את הסכום של הקובץ הראשי וקובץ עדכונים. כלומר, אם מציינים גם את mainVersion וגם את patchVersion, הפונקציה מחזירה ZipResourceFile שנותן גישת קריאה לכל הנתונים, כשהנתונים של קובץ התיקון ממוזגים מעל הקובץ הראשי.
ZipResourceFile
מייצג קובץ ZIP באחסון השיתופי ומבצע את כל הפעולות כדי לספק מערכת קבצים וירטואלית שמבוססת על קובצי ה-ZIP. אפשר לקבל מופע באמצעות APKExpansionSupport.getAPKExpansionZipFile() או באמצעות ZipResourceFile על ידי העברת הנתיב לקובץ ההרחבה. המחלקות האלה כוללות מגוון שיטות שימושיות, אבל בדרך כלל לא צריך לגשת לרוב השיטות האלה. שתי שיטות חשובות הן:
getInputStream(String assetPath)
Provides an InputStream to read a file within the ZIP file. ‫assetPath צריך להיות הנתיב לקובץ הרצוי, ביחס לשורש של תוכן קובץ ה-ZIP.
getAssetFileDescriptor(String assetPath)
מספק AssetFileDescriptor לקובץ בתוך קובץ ה-ZIP. הערך assetPath צריך להיות הנתיב לקובץ הרצוי, ביחס לשורש של תוכן קובץ ה-ZIP. האפשרות הזו שימושית לממשקי API מסוימים ל-Android שנדרש בהם AssetFileDescriptor, כמו חלק מממשקי MediaPlayer API.
APEZProvider
ברוב האפליקציות אין צורך להשתמש במחלקה הזו. המחלק הזה מגדיר ContentProvider שמארגן את הנתונים מקובצי ה-ZIP דרך ספק תוכן Uri כדי לספק גישה לקבצים לממשקי API מסוימים של Android שמצפים לגישת Uri לקובצי מדיה. לדוגמה, זה שימושי אם רוצים להפעיל סרטון עם VideoView.setVideoURI().

דילוג על דחיסת קובצי מדיה ב-ZIP

אם אתם משתמשים בקובצי ההרחבה כדי לאחסן קובצי מדיה, עדיין תוכלו להשתמש בקובץ ZIP כדי להשתמש בקריאות להפעלת מדיה ב-Android שמספקות אמצעי בקרה של היסט ואורך (כמו MediaPlayer.setDataSource() ו-SoundPool.load()). כדי שזה יקרה, אסור לבצע דחיסה נוספת של קובצי המדיה כשיוצרים את חבילות ה-ZIP. לדוגמה, כשמשתמשים בכלי zip, צריך להשתמש באפשרות -n כדי לציין את סיומות הקבצים שלא צריך לדחוס:

zip -n .mp4;.ogg main_expansion media_files

קריאה מקובץ ZIP

כשמשתמשים בספריית ה-ZIP של קובצי הרחבה ל-APK, בדרך כלל צריך לבצע את הפעולות הבאות כדי לקרוא קובץ מ-ZIP:

Kotlin

// Get a ZipResourceFile representing a merger of both the main and patch files
val expansionFile =
        APKExpansionSupport.getAPKExpansionZipFile(appContext, mainVersion, patchVersion)

// Get an input stream for a known file inside the expansion file ZIPs
expansionFile.getInputStream(pathToFileInsideZip).use {
    ...
}

Java

// Get a ZipResourceFile representing a merger of both the main and patch files
ZipResourceFile expansionFile =
    APKExpansionSupport.getAPKExpansionZipFile(appContext,
        mainVersion, patchVersion);

// Get an input stream for a known file inside the expansion file ZIPs
InputStream fileStream = expansionFile.getInputStream(pathToFileInsideZip);

הקוד שלמעלה מספק גישה לכל קובץ שקיים בקובץ הרחבות ראשי או בקובץ הרחבות עדכונים, על ידי קריאה ממפה ממוזגת של כל הקבצים משני הקבצים. כדי לספק את השיטה getAPKExpansionFile(), צריך לציין את האפליקציה android.content.Context ואת מספר הגרסה של קובץ ההרחבה הראשי ושל קובץ הרחבות התיקונים.

אם אתם מעדיפים לקרוא מקובץ הרחבה ספציפי, אתם יכולים להשתמש בבונה ZipResourceFile עם הנתיב לקובץ ההרחבה הרצוי:

Kotlin

// Get a ZipResourceFile representing a specific expansion file
val expansionFile = ZipResourceFile(filePathToMyZip)

// Get an input stream for a known file inside the expansion file ZIPs
expansionFile.getInputStream(pathToFileInsideZip).use {
    ...
}

Java

// Get a ZipResourceFile representing a specific expansion file
ZipResourceFile expansionFile = new ZipResourceFile(filePathToMyZip);

// Get an input stream for a known file inside the expansion file ZIPs
InputStream fileStream = expansionFile.getInputStream(pathToFileInsideZip);

למידע נוסף על השימוש בספרייה הזו לקובצי ההרחבה, אפשר לעיין במחלקה SampleDownloaderActivity של אפליקציית הדוגמה, שכוללת קוד נוסף לאימות הקבצים שהורדו באמצעות CRC. חשוב לזכור שאם משתמשים בדוגמה הזו כבסיס להטמעה משלכם, צריך להצהיר על גודל הבייט של קובצי ההרחבה במערך xAPKS.

בדיקת קובצי ההרחבה

לפני שמפרסמים את האפליקציה, צריך לבדוק שני דברים: קריאה של קובצי ההרחבה והורדה של הקבצים.

בדיקת קריאות של קבצים

לפני שמעלים את האפליקציה ל-Google Play, כדאי לבדוק את היכולת של האפליקציה לקרוא את הקבצים מהאחסון המשותף. כל מה שצריך לעשות הוא להוסיף את הקבצים למיקום המתאים באחסון המשותף של המכשיר ולהפעיל את האפליקציה:

  1. במכשיר, יוצרים את הספרייה המתאימה באחסון המשותף שבו Google Play תשמור את הקבצים.

    לדוגמה, אם שם החבילה הוא com.example.android, צריך ליצור את הספרייה Android/obb/com.example.android/ במרחב האחסון המשותף. (מחברים את מכשיר הבדיקה למחשב כדי לטעון את האחסון המשותף וליצור את הספרייה הזו באופן ידני).

  2. מוסיפים ידנית את קובצי ההרחבות לספרייה הזו. חשוב לשנות את השם של הקבצים כך שיתאים לפורמט של שם הקובץ שבו ישתמש Google Play.

    לדוגמה, ללא קשר לסוג הקובץ, קובץ ההרחבות הראשי של אפליקציית com.example.android צריך להיות main.0300110.com.example.android.obb. קוד הגרסה יכול להיות כל ערך שרוצים. חשוב לזכור:

    • קובץ ההרחבות הראשי תמיד מתחיל ב-main וקובץ עדכונים מתחיל ב-patch.
    • שם החבילה תמיד זהה לזה של קובץ ה-APK שאליו הקובץ מצורף ב-Google Play.
  3. עכשיו, אחרי שקובצי ההרחבה נמצאים במכשיר, אפשר להתקין את האפליקציה ולהריץ אותה כדי לבדוק את קובצי ההרחבה.

ריכזנו כאן כמה תזכורות לגבי הטיפול בקובצי ההרחבה:

  • אל תמחקו או תשנו את השם של קובצי ההרחבה .obb (גם אם אתם פורקים את הנתונים למיקום אחר). אם תעשו את זה, קובץ ההרחבה יורד שוב ושוב מ-Google Play (או מהאפליקציה עצמה).
  • אל תשמרו נתונים אחרים בספרייה של obb/. אם אתם צריכים לפתוח חלק מהנתונים, שמרו אותם במיקום שצוין על ידי getExternalFilesDir().

בדיקת הורדות של קבצים

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

כדי לבדוק את ההטמעה של תהליך ההורדה הידנית באפליקציה, אפשר לפרסם אותה במסלול לבדיקה פנימית, כך שהיא תהיה זמינה רק לבודקים מורשים. אם הכול פועל כמו שצריך, האפליקציה אמורה להתחיל להוריד את קובצי ההרחבה ברגע שהפעילות הראשית מתחילה.

הערה: בעבר הייתה אפשרות לבדוק אפליקציה על ידי העלאה של גרסת 'טיוטה' שלא פורסמה. הפונקציונליות הזו כבר לא נתמכת. במקום זאת, צריך לפרסם אותה במסלול הפצה של בדיקה פנימית, בדיקה בקבוצה מוגדרת או בדיקה פתוחה לציבור. מידע נוסף מופיע במאמר בנושא אפליקציות בטיוטה לא נתמכות יותר.

עדכון האפליקציה

אחד היתרונות הגדולים של שימוש בקובצי הרחבה ב-Google Play הוא האפשרות לעדכן את האפליקציה בלי להוריד מחדש את כל הנכסים המקוריים. מכיוון שמערכת Google Play מאפשרת לכם לספק שני קובצי הרחבה עם כל APK, אתם יכולים להשתמש בקובץ השני כ'תיקון' שמספק עדכונים ונכסים חדשים. כך לא צריך להוריד מחדש את קובץ ההרחבה הראשי, שיכול להיות גדול ויקר למשתמשים.

קובץ הרחבות העדכונים זהה מבחינה טכנית לקובץ ההרחבות הראשי, ומערכת Android ו-Google Play לא מבצעות תיקון בפועל בין קובץ ההרחבות הראשי לבין קובץ הרחבות העדכונים. קוד האפליקציה צריך לבצע בעצמו את כל התיקונים הנדרשים.

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

הערה: גם אם אתם צריכים לבצע שינויים רק בקובץ ההרחבה של התיקון, אתם עדיין צריכים לעדכן את קובץ ה-APK כדי ש-Google Play יבצע עדכון. אם לא נדרשים שינויים בקוד האפליקציה, פשוט צריך לעדכן את versionCode בקובץ המניפסט.

כל עוד לא תשנו את קובץ ההרחבה הראשי שמשויך לחבילת ה-APK ב-Play Console, משתמשים שהתקינו בעבר את האפליקציה לא יורידו את קובץ ההרחבה הראשי. משתמשים קיימים מקבלים רק את קובץ ה-APK המעודכן ואת קובץ הרחבות העדכונים החדש (קובץ הרחבות ראשי הקודם נשמר).

כמה בעיות שכדאי לזכור לגבי עדכונים של קובצי הרחבה:

  • יכולים להיות רק שני קובצי הרחבה לאפליקציה בכל זמן נתון. קובץ הרחבות ראשי אחד וקובץ הרחבות תיקונים אחד. במהלך עדכון של קובץ, Google Play מוחק את הגרסה הקודמת (וכך גם האפליקציה שלכם צריכה לעשות כשמבצעים עדכונים ידניים).
  • כשמוסיפים קובץ הרחבות תיקונים, מערכת Android לא מתקנת בפועל את האפליקציה או את קובץ ההרחבות הראשי. אתם צריכים לתכנן את האפליקציה כך שתתמוך בנתוני התיקון. עם זאת, חבילת ההרחבה של ה-APK כוללת ספרייה לשימוש בקובצי ZIP כקובצי הרחבה, שממזגת את הנתונים מקובץ התיקון לקובץ ההרחבה הראשי, כך שתוכלו לקרוא בקלות את כל הנתונים של קובץ ההרחבה.