ملفات توسيع حزمة APK

يتطلب Google Play ألا يزيد حجم ملف APK المضغوط الذي ينزله المستخدمون عن 100 ميغابايت. وفي معظم التطبيقات، توفّر هذه الميزة مساحة كبيرة لجميع الرموز البرمجية للتطبيق ومواد العرض. ومع ذلك، تحتاج بعض التطبيقات إلى مساحة أكبر للرسومات أو ملفات الوسائط عالية الدقة أو مواد العرض الكبيرة الأخرى. في السابق، إذا كان حجم التنزيل المضغوط لتطبيقك يتجاوز 100 ميغابايت، كان عليك استضافة الموارد الإضافية وتنزيلها بنفسك عندما يفتح المستخدم التطبيق. وقد تكون استضافة الملفات الإضافية وعرضها مكلفّة، وغالبًا ما تكون تجربة المستخدم أقل من المستوى المثالي. لتسهيل هذه العملية وجعلها أكثر متعة للمستخدمين، يسمح لك Google Play بإرفاق ملفَّي توسيع كبيرَين يكمِّلان حزمة APK.

يستضيف Google Play ملفات توسيع تطبيقك ويعرضها على الجهاز بدون أي تكلفة عليك. يتم حفظ ملفات التوسيع في موقع وحدة التخزين المشتركة للجهاز (بطاقة SD أو القسم القابل للتركيب على USB، والمعروف أيضًا باسم وحدة التخزين "الخارجية") حيث يمكن لتطبيقك الوصول إليها. على معظم الأجهزة، ينزِّل Google Play ملفات البيانات الموسّعة في الوقت نفسه الذي يتم فيه تنزيل حزمة APK، وذلك لكي يحصل تطبيقك على كل ما يحتاج إليه عندما يفتحه المستخدم للمرة الأولى. ومع ذلك، في بعض الحالات، يجب على تطبيقك تنزيل الملفات من Google Play عند بدء تشغيله.

إذا أردت تجنُّب استخدام ملفات التوسيع وكان حجم تنزيل التطبيق المضغوط أكبر من 100 ميغابايت، عليك تحميل تطبيقك باستخدام حِزم تطبيقات Android التي تسمح بحجم تنزيل مضغوط يصل إلى 200 ميغابايت. بالإضافة إلى ذلك، لأنّ استخدام حِزم التطبيقات يؤدي إلى تأجيل إنشاء حِزم APK وتوقيعها على Google Play، يتمكّن المستخدمون من تنزيل حِزم APK المحسَّنة باستخدام الرمز والموارد التي يحتاجونها لتشغيل تطبيقك. وليس عليك إنشاء عدة حِزم APK أو ملفات توسيع وتوقيعها وإدارتها، وسيحصل المستخدمون على عمليات تنزيل أصغر حجمًا ومحسَّنة.

نظرة عامة

في كل مرة تحمّل فيها ملف APK باستخدام Google Play Console، يتوفّر لك خيار إضافة ملف أو ملفَّين توسيع إلى حزمة APK. يمكن أن يصل حجم كل ملف إلى 2 غيغابايت ويمكن استخدامه بأي تنسيق تختاره، لكن ننصحك باستخدام ملف مضغوط للحفاظ على معدّل نقل البيانات أثناء عملية التنزيل. من الناحية النظرية، يلعب كل ملف توسيع دورًا مختلفًا:

  • ملف التوسيع الرئيسي هو ملف التوسيع الأساسي للموارد الإضافية التي يتطلبها تطبيقك.
  • ملف التوسيع patch اختياري ومخصص لإجراء التحديثات الصغيرة في ملف التوسيع الرئيسي.

على الرغم من أنه يمكنك استخدام ملفي التوسيع بالطريقة التي تريدها، إلا أننا ننصح بأن يسلّم ملف التوسيع الرئيسي الأصول الأساسية ومن المفترض ألا يتم تحديثها في كثير من الأحيان؛ وينبغي أن يكون ملف توسيع التصحيح أصغر حجمًا وأن يعمل كـ "ناقل تصحيح"، وأن يتم تحديثه بكل إصدار رئيسي أو حسب الضرورة.

ومع ذلك، حتى لو لم يتطلّب تحديث تطبيقك سوى ملف توسيع رمز تصحيح جديد، سيكون عليك تحميل حزمة APK جديدة تتضمّن رمز versionCode مُحدَّث في البيان. (لا تسمح لك أداة Play Console بتحميل ملف توسيع إلى حزمة APK حالية).

ملاحظة: يتشابه ملف توسيع التصحيح دلالياً مع ملف التوسيع الرئيسي، ويمكنك استخدام كل ملف بالطريقة التي تريدها.

تنسيق اسم الملف

يمكن أن يكون كل ملف توسيع تحمّله بأي تنسيق تختاره (ZIP أو PDF أو MP4 أو غير ذلك). يمكنك أيضًا استخدام أداة JOBB لتغليف وتشفير مجموعة من ملفات الموارد والتصحيحات اللاحقة لهذه المجموعة. وبغض النظر عن نوع الملف، يعتبر Google Play هذه الملفات الثنائية الغامضة وتعيد تسمية الملفات باستخدام المخطَّط التالي:

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

هناك ثلاثة مكونات لهذا النظام:

main أو patch
يحدد هذا الإعداد ما إذا كان الملف هو الملف الرئيسي أو ملف توسيع التصحيح. يمكن أن يكون هناك ملف رئيسي واحد وملف تصحيح واحد فقط لكل ملف APK.
<expansion-version>
هذا عدد صحيح يتطابق مع رمز إصدار APK الذي يتم ربط التوسيع أولاً به (يتطابق مع قيمة android:versionCode في التطبيق).

يتم التأكيد على الخيار "First" لأنّه على الرغم من أنّ أداة Play Console تتيح لك إعادة استخدام ملف توسيع تم تحميله مع حزمة APK جديدة، لن يتغيّر اسم ملف التوسيع، لأنّه سيحتفظ بالإصدار الذي تم تطبيقه عليه عند تحميل الملف لأول مرة.

<package-name>
اسم الحزمة بنمط جافا لتطبيقك

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

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

بالنسبة إلى الإصدار 6 من Android والإصدارات الأحدث، يجب طلب إذن مساحة التخزين الخارجية في وقت التشغيل. لا تتطلب بعض عمليات تنفيذ نظام التشغيل 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. حدِّد أولاً ما إذا كان حجم التنزيل المضغوط لتطبيقك يجب أن يزيد عن 100 ميغابايت. إنّ المساحة كبيرة لذا عليك الاحتفاظ بحجم التنزيل الإجمالي صغيرًا قدر الإمكان. إذا كان تطبيقك يستخدم أكثر من 100 ميغابايت لتقديم إصدارات متعددة من أصول الرسومات لشاشات ذات كثافات متعددة، ننصحك بنشر حِزم APK متعددة تتضمّن فقط مواد العرض المطلوبة للشاشات التي يستهدفها التطبيق. للحصول على أفضل النتائج عند النشر على Google Play، يمكنك تحميل مجموعة حزمات تطبيق Android التي تتضمّن كل الرموز والموارد المجمّعة لتطبيقك، ولكن مع تأجيل إنشاء APK والتسجيل إلى Google Play.
  2. حدد موارد التطبيق التي تريد فصلها عن APK واجمعها في ملف لاستخدامه كملف توسيع رئيسي.

    يجب عادةً استخدام ملف توسيع التصحيح الثاني فقط عند إجراء تحديثات على ملف التوسيع الرئيسي. ومع ذلك، إذا تجاوزت مواردك الحدّ الأقصى المسموح به لملف التوسيع الرئيسي، وهو 2 غيغابايت، يمكنك استخدام ملف التصحيح لبقية الأصول.

  3. طوِّر تطبيقك بحيث يستخدم الموارد من ملفات التوسيع في موقع مساحة التخزين المشتركة على الجهاز.

    تذكر أنه يجب عدم حذف ملفات التوسيع أو نقلها أو إعادة تسميتها.

    إذا لم يكن تطبيقك يتطلب تنسيقًا محددًا، نقترح عليك إنشاء ملفات ZIP لملفات التوسيع، ثم قراءتها باستخدام مكتبة APK Expansion Zip.

  4. أضِف منطقًا إلى النشاط الرئيسي لتطبيقك الذي يتحقّق مما إذا كانت ملفات التوسيع متوفّرة على الجهاز عند بدء التشغيل. إذا لم تكن الملفات على الجهاز، يمكنك استخدام خدمة ترخيص التطبيقات من Google Play لطلب عناوين URL لملفات التوسيع، ثم تنزيلها وحفظها.

    للحدّ بشكل كبير من عدد الرموز البرمجية التي يجب كتابتها وضمان تقديم تجربة جيدة للمستخدم أثناء عملية التنزيل، ننصحك باستخدام مكتبة برامج التنزيل لتنفيذ سلوك التنزيل.

    إذا أنشأت خدمة التنزيل الخاصة بك بدلاً من استخدام المكتبة، يُرجى العلم بأنّه يجب عدم تغيير اسم ملفات التوسيع ويجب حفظها في مكان التخزين المناسب.

بعد الانتهاء من تطوير تطبيقك، اتّبِع الدليل اختبار ملفات التوسيع.

القواعد والقيود

تُعد إضافة ملفات توسيع APK ميزة متاحة عند تحميل تطبيقك باستخدام Play Console. عند تحميل تطبيقك لأول مرة أو تحديث تطبيق يستخدم ملفات توسيع، يجب أن تكون على دراية بالقواعد والقيود التالية:

  1. يجب ألا يزيد كل ملف توسيع عن 2 غيغابايت.
  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 وتحفظها في الموقع الصحيح على مساحة التخزين المشتركة للجهاز. وأثناء إنشاء هذا الإجراء في تطبيقك، هناك العديد من المشاكل التي يجب مراعاتها:

  • قد لا يحتوي الجهاز على مساحة كافية لملفات التوسيع، لذلك يجب التحقق قبل بدء التنزيل وتحذير المستخدم في حالة عدم وجود مساحة كافية.
  • يجب أن يتم تنزيل الملفات في خدمة في الخلفية لتجنّب حظر تفاعل المستخدم والسماح للمستخدم بمغادرة التطبيق أثناء اكتمال عملية التنزيل.
  • قد تحدث مجموعة متنوعة من الأخطاء أثناء الطلب والتنزيل اللذين يجب التعامل معهما بحذر.
  • يمكن أن يتغير الاتصال بالشبكة أثناء التنزيل، لذا عليك التعامل مع هذه التغييرات، وفي حال انقطاعها، استأنف عملية التنزيل متى أمكن.
  • أثناء حدوث عملية التنزيل في الخلفية، يجب تقديم إشعار يشير إلى مستوى تقدُّم عملية التنزيل ويُعلِم المستخدم عند اكتمال عملية التنزيل ويُعيد المستخدم إلى تطبيقك عند اختياره.

لتبسيط هذا العمل، أنشأنا مكتبة عمليات التنزيل، التي تطلب عناوين URL لملف التوسيع من خلال خدمة الترخيص، وتقوم بتنزيل ملفات التوسيع، وتنفيذ جميع المهام المذكورة أعلاه، وحتى تسمح لنشاطك بإيقاف التنزيل المؤقت واستئنافه. من خلال إضافة Downloader Library (مكتبة أدوات التنزيل) وبعض عناصر إضافة الرموز إلى تطبيقك، يتم بالفعل ترميز جميع الأعمال المطلوبة لتنزيل ملفات توسيع البيانات. وبناءً على ذلك، لتوفير أفضل تجربة مستخدم بأقل جهد ممكن نيابةً عنك، ننصحك باستخدام Downloader Library (مكتبة التنزيل) لتنزيل ملفات البيانات الموسّعة. تشرح المعلومات الواردة في الأقسام التالية كيفية دمج المكتبة في تطبيقك.

إذا كنت تفضِّل تطوير حلك الخاص لتنزيل ملفات التوسيع باستخدام عناوين URL لـ Google Play، يجب عليك اتباع مستندات ترخيص التطبيق لتنفيذ طلب ترخيص، ثم استرداد أسماء ملفات التوسيع وأحجامها وعناوين URL من العناصر الإضافية للاستجابة. يجب استخدام الفئة APKExpansionPolicy (المضمّنة في "مكتبة إثبات ملكية الترخيص") كسياسة الترخيص التي تسجِّل أسماء ملفات التوسيع وأحجامها وعناوين URL من خدمة الترخيص.

لمحة عن مكتبة عمليات التنزيل

لاستخدام ملفات توسيع APK مع تطبيقك وتوفير أفضل تجربة للمستخدم بأقل جهد نيابةً عنك، ننصحك باستخدام مكتبة عمليات التنزيل المضمَّنة في حزمة Google Play APK Expansion Library. تعمل هذه المكتبة على تنزيل ملفات التوسيع في خدمة الخلفية، وعرض إشعار للمستخدم بحالة التنزيل، وتتعامل مع فقدان الاتصال بالشبكة، وتستأنف التنزيل إن أمكن، وغير ذلك الكثير.

لتنفيذ عمليات تنزيل ملف التوسيع باستخدام مكتبة التنزيل، كل ما عليك فعله هو:

  • يمكنك تمديد فئة فرعية خاصة Service وفئة فرعية BroadcastReceiver لا يتطلب كلٌّ منها منك سوى بضعة أسطر من الرمز.
  • أضف بعض المنطق إلى نشاطك الرئيسي الذي يتحقق مما إذا كان قد تم تنزيل ملفات التوسيع بالفعل أم لا، وإذا لم يكن قد تم تنزيله، سيتم استدعاء عملية التنزيل وعرض واجهة مستخدم للتقدم.
  • نفِّذ واجهة لمعاودة الاتصال بعدة طرق في نشاطك الرئيسي لتلقى تحديثات بشأن تقدم التنزيل.

توضح الأقسام التالية كيفية إعداد تطبيقك باستخدام مكتبة التنزيل.

التحضير لاستخدام مكتبة عمليات التنزيل

لاستخدام مكتبة التنزيل، يجب تنزيل حزمتين من مدير SDK وإضافة المكتبات المناسبة إلى تطبيقك.

أولاً، افتح Android SDK Manager (الأدوات > SDK Manager)، وضمن المظهر والسلوك > إعدادات النظام > حزمة تطوير البرامج (SDK) لنظام التشغيل Android، اختَر علامة التبويب أدوات SDK لاختيار وتنزيل:

  • حزمة مكتبة ترخيص Google Play
  • حزمة توسيع حزمة APK في Google Play

أنشِئ وحدة مكتبة جديدة لمكتبة التحقّق من الترخيص ومكتبة عمليات التنزيل. لكل مكتبة:

  1. حدد ملف > جديد > وحدة جديدة.
  2. في نافذة إنشاء وحدة جديدة، اختَر مكتبة Android، ثم انقر على التالي.
  3. حدِّد اسم التطبيق/المكتبة، مثل "مكتبة تراخيص Google Play" و "مكتبة تراخيص Google Play"، واختَر الحد الأدنى لمستوى حزمة تطوير البرامج (SDK)، ثم اختَر إنهاء.
  4. حدد ملف > هيكل المشروع.
  5. اختَر علامة التبويب الخصائص وفي مستودع المكتبة، أدخِل المكتبة من الدليل <sdk>/extras/google/ (play_licensing/ للاطّلاع على مكتبة التحقّق من الترخيص أو play_apk_expansion/downloader_library/ لمكتبة عمليات التنزيل).
  6. انقر على حسنًا لإنشاء الوحدة الجديدة.

ملاحظة: تعتمد مكتبة عمليات التنزيل على مكتبة إثبات ملكية التراخيص. احرص على إضافة "مكتبة إثبات ملكية التراخيص" إلى مواقع مشاريع "مكتبة عمليات التنزيل".

أو يمكنك تعديل مشروعك من خلال سطر الأوامر لتضمين المكتبات التالية:

  1. غيِّر الأدلة إلى الدليل <sdk>/tools/.
  2. نفِّذ android update project باستخدام الخيار --library لإضافة كلٍّ من LVL وDownloader Library إلى مشروعك. مثلاً:
    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 (حزمة Apk Expansion) نموذجًا لتطبيق يوضِّح كيفية استخدام أداة Downloader Library (مكتبة التنزيل) في أحد التطبيقات. ويستخدم النموذج مكتبة ثالثة متوفّرة في حزمة Apk Expansion (التي يُطلق عليها اسم APK Expansion Zip Library). إذا كنت تنوي استخدام ملفات ZIP لملفات ZIP، نقترح عليك أيضًا إضافة مكتبة APK Expansion Zip إلى تطبيقك. ولمزيد من المعلومات، يُرجى الاطّلاع على القسم أدناه حول استخدام APK Expansion Zip 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>

ملاحظة: بشكل تلقائي، تتطلّب مكتبة أدوات التنزيل استخدام المستوى 4 من واجهة برمجة التطبيقات، إلا أنّ مكتبة APK Expansion Zip Library تتطلّب المستوى 5 من واجهة برمجة التطبيقات.

تنفيذ خدمة التنزيل

لإجراء عمليات التنزيل في الخلفية، توفّر مكتبة برامج التنزيل فئة 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 لطلب واجهة برمجة تطبيقات من مكتبة أدوات التنزيل التي تتحقّق من حالة عملية التنزيل وتعيد تشغيلها إذا لزم الأمر.

ما عليك سوى إلغاء طريقة 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. تحقَّق مما إذا كان قد تم تنزيل الملفات.

    تتضمّن مكتبة Downloader Library بعض واجهات برمجة التطبيقات في الفئة Helper للمساعدة في هذه العملية:

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

    على سبيل المثال، يستدعي نموذج التطبيق المقدَّم في حزمة Apk Expansion الطريقة التالية في طريقة 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() عند معاودة الاتصال بنشاطك التجاري في 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 الخاصة بمكتبة Downloader Library. عادةً، يجب أن يستخدم النشاط الذي تستخدمه لبدء التنزيل هذه الواجهة لعرض مستوى تقدُّم عملية التنزيل وإرسال الطلبات إلى الخدمة.

في ما يلي طرق الواجهة المطلوبة لـ 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)
تستدعي خدمة التنزيل هذا الإجراء لعرض عنصر DownloadProgressInfo يصف معلومات مختلفة حول مستوى تقدُّم عملية التنزيل، بما في ذلك الوقت المقدّر المتبقي والسرعة الحالية ومستوى التقدّم العام والإجمالي حتى تتمكّن من تعديل واجهة المستخدم الخاصة بمستوى تقدُّم عملية التنزيل.

نصيحة: للحصول على أمثلة على استدعاءات الاتصال هذه التي تُحدّث واجهة مستخدم تقدّم التنزيل، يمكنك الاطّلاع على 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 الإضافية.

ملاحظة: إذا كنت تستخدم 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 Expansion "مكتبة توسيع APK" (موجودة في <sdk>/extras/google/google_market_apk_expansion/zip_file/)، وهي مكتبة اختيارية تساعدك في قراءة ملفات التوسيع عند حفظها كملفات ZIP. يتيح لك استخدام هذه المكتبة قراءة الموارد بسهولة من ملفات توسيع ZIP كنظام ملفات افتراضي.

تشتمل مكتبة APK Expansion Zip Library على الفئات وواجهات برمجة التطبيقات التالية:

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. ويكون ذلك مفيدًا لبعض واجهات برمجة تطبيقات Android التي تتطلّب AssetFileDescriptor، مثل بعض واجهات برمجة تطبيقات MediaPlayer.
APEZProvider
لا تحتاج معظم التطبيقات إلى استخدام هذا الصف. تحدّد هذه الفئة ContentProvider الذي ينظّم البيانات من ملفات ZIP من خلال موفّر المحتوى Uri لتوفير إمكانية الوصول إلى الملفات بالنسبة إلى بعض واجهات برمجة تطبيقات Android التي تتوقع وصول Uri إلى ملفات الوسائط. على سبيل المثال، يُعدّ هذا الإجراء مفيدًا إذا كنت تريد تشغيل فيديو باستخدام VideoView.setVideoURI().

تخطي ضغط ZIP لملفات الوسائط

إذا كنت تستخدم ملفات البيانات الموسّعة لتخزين ملفات الوسائط، يسمح لك ملف ZIP باستخدام مكالمات تشغيل وسائط Android التي توفّر عناصر تحكّم في الإزاحة والطول (مثل MediaPlayer.setDataSource() و SoundPool.load()). ولكي ينجح ذلك، يجب عدم إجراء ضغط إضافي على ملفات الوسائط عند إنشاء حِزم ZIP. على سبيل المثال، عند استخدام أداة zip، يجب استخدام الخيار -n لتحديد لاحقات الملفات التي يجب عدم ضغطها:

zip -n .mp4;.ogg main_expansion media_files

القراءة من ملف ZIP

عند استخدام مكتبة APK Expansion Zip Library، عادةً ما تتطلب قراءة ملف من ملف 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 كملفات توسيع، ستتضمّن مكتبة APK Expansion Zip المضمّنة في حزمة Apk Expansion إمكانية دمج ملف التصحيح مع ملف التوسيع الرئيسي.

ملاحظة: حتى إذا كنت تحتاج فقط إلى إجراء تغييرات على ملف توسيع رمز التصحيح، عليك تحديث حزمة APK حتى يتمكن Google Play من إجراء التحديث. إذا كنت لا تشترط إجراء تغييرات على الرمز في التطبيق، ما عليك سوى تعديل versionCode في البيان.

لن ينزِّل المستخدمون الذين سبق لهم تثبيت تطبيقك ملف التوسيع الرئيسي، ما لم يتم تغيير ملف التوسيع الرئيسي المرتبط بحزمة APK في Play Console. لن يتلقى المستخدمون الحاليون سوى حزمة APK المُحدّثة وملف توسيع رمز التصحيح الجديد (مع الاحتفاظ بملف التوسيع الرئيسي السابق).

في ما يلي بعض المشكلات التي يجب مراعاتها في ما يتعلق بالتحديثات التي يتم إجراؤها على ملفات التوسيع:

  • يمكن إنشاء ملفَّي توسيع فقط لتطبيقك في وقت واحد. ملف توسيع رئيسي واحد وملف توسيع حزمة تصحيح واحد. أثناء تحديث أحد الملفات، يحذف Google Play الإصدار السابق (وكذلك يجب أن يحذف تطبيقك عند إجراء تحديثات يدوية).
  • عند إضافة ملف تصحيح توسيعي، لا يصحّح نظام Android تطبيقك أو ملف التوسيع الرئيسي. يجب تصميم تطبيقك بحيث يتوافق مع بيانات التصحيح. ومع ذلك، تتضمن حزمة Apk Expansion مكتبة لاستخدام ملفات ZIP كملفات توسيع، التي تدمج البيانات من ملف التصحيح في ملف التوسيع الرئيسي حتى تتمكّن بسهولة من قراءة جميع بيانات ملف التوسيع.