קובצי הרחבת APK

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

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

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

סקירה כללית

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

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

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

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

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

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

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

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

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

main או patch
הערך הזה מציין אם הקובץ הוא קובץ הרחבות ראשי או קובץ הרחבות תיקון. לכל קובץ APK יכולים להיות רק קובץ ראשי אחד וקובץ תיקון אחד.
<expansion-version>
זהו מספר שלם שתואם לקוד הגרסה של חבילת ה-APK שאליה הרחבה משויכת בפעם הראשונה (הוא תואם לערך של android:versionCode באפליקציה).

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

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

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

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

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

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

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

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

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

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

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

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

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

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

הוספת קובצי הרחבה של 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. לכן, גם אם האפליקציה שלכם זמינה בחינם למשתמשים, עליכם לכלול את ספריית אימות הרישיון (LVL) כדי להשתמש בקובצי הרחבה של APK. כמובן, אם האפליקציה שלכם בחינם, אין צורך לאכוף אימות רישיון – פשוט צריך לבקש מהספרייה להחזיר את כתובת ה-URL של קובצי ההרחבה.

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

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

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

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

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

מידע על ספריית Downloader

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

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

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

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

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

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

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

  • חבילת Google Play Licensing Library
  • חבילת ספריית ההרחבה של APK ב-Google Play

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

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

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

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

  1. עוברים לספרייה <sdk>/tools/.
  2. מריצים את הפקודה android update project עם האפשרות --library כדי להוסיף את LVL ואת ספריית Downloader לפרויקט. לדוגמה:
    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 Expansion כוללת אפליקציה לדוגמה שמראה איך להשתמש בספריית Downloader באפליקציה. הדוגמה משתמשת בספרייה שלישית שזמינה בחבילת Apk Expansion שנקראת APK Expansion Zip Library. אם אתם מתכננים להשתמש בקובצי ZIP לקובצי ההרחבה, מומלץ להוסיף לאפליקציה גם את ספריית ה-ZIP של קובצי ההרחבה ל-APK. מידע נוסף זמין בקטע שימוש בספריית ה-ZIP של קובצי ההרחבה ל-APK שבהמשך.

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

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

<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.

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

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

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

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

getPublicKey()
הפונקציה הזו צריכה להחזיר מחרוזת שמכילה את המפתח הציבורי של RSA בקידוד Base64 לחשבון בעל התוכן הדיגיטלי. המפתח הזה זמין בדף הפרופיל ב-Play Console (מידע נוסף זמין במאמר הגדרה לצורכי רישוי).
getSALT()
היא צריכה להחזיר מערך של בייטים אקראיים שמערכת הרישוי Policy משתמשת בהם כדי ליצור Obfuscator. המלח מבטיח שהקובץ 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 למפתח הציבורי שמשויך לחשבון בעל התוכן הדיגיטלי. המפתח מופיע במסוף הפיתוח בקטע 'פרטי הפרופיל'. צריך לעשות זאת גם כשבודקים את ההורדות.

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

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

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

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

פשוט צריך לשנות את השיטה 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>

התחלת ההורדה

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

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

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

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

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

    לדוגמה, האפליקציה לדוגמה שסופקה בחבילת ה-Apk Expansion קוראת לשיטה הבאה ב-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. כשה-method startDownloadServiceIfRequired() מחזיר משהו אחר מ-NO_DOWNLOAD_REQUIRED, יוצרים מופע של IStub על ידי קריאה ל-DownloaderClientMarshaller.CreateStub(IDownloaderClient client, Class<?> downloaderService). ה-IStub מספק קישור בין הפעילות לבין שירות ההורדה, כך שהפעילות מקבלת קריאות חזרה (callbacks) לגבי התקדמות ההורדה.

    כדי ליצור מופע של 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 יהיה אחד מכמה ערכים אפשריים שצוינו על ידי אחת מהקבועות STATE_* של הכיתה IDownloaderClient.

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

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

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

כמה שיטות ציבוריות לממשק 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.

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

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

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

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

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

אחרי שקובצי ההרחבה של ה-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 כדי לקבל את קוד הגרסה.

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

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

חבילת ה-Apk Expansion של Google Market כוללת ספרייה שנקראת 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)
מספק InputStream לקריאת קובץ בקובץ ה-ZIP. השדה assetPath חייב להיות הנתיב לקובץ הרצוי, ביחס לשורש של תוכן קובץ ה-ZIP.
getAssetFileDescriptor(String assetPath)
מספק AssetFileDescriptor לקובץ בקובץ ה-ZIP. השדה assetPath חייב להיות הנתיב לקובץ הרצוי, ביחס לשורש של תוכן קובץ ה-ZIP. האפשרות הזו שימושית לממשקי API מסוימים של Android שדורשים AssetFileDescriptor, כמו חלק מממשקי ה-API של MediaPlayer.
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 של האפליקציה ומספר הגרסה של קובץ ההרחבה הראשי ושל קובץ ההרחבה של התיקון.

אם אתם מעדיפים לקרוא מקובץ הרחבה ספציפי, תוכלו להשתמש ב-constructor של 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 Expansion כוללת ספרייה לשימוש בקובצי ZIP כקובצי הרחבה, שממזגת את הנתונים מקובץ התיקון לקובץ ההרחבה הראשי כדי שתוכלו לקרוא בקלות את כל הנתונים בקובץ ההרחבה.