Google Play 規定使用者下載的壓縮 APK 不得超過 100 MB, 這對大部分的應用程式來說,容納所有程式碼和素材資源綽綽有餘。 不過,有些應用程式需要較多空間容納高保真圖像、媒體檔案或其他大型素材資源。在以前,如果應用程式的壓縮下載大小超過 100 MB,您就必須在使用者開啟應用程式時,自行代管和下載額外資源。代管及提供額外檔案可能需支付高額費用,而且使用者體驗通常會不盡理想。為了減輕您的負擔,也讓使用者擁有更好的體驗,Google Play 允許您附加兩個補充 APK 的大型擴充檔案。
Google Play 替您的應用程式代管擴充檔案,為裝置提供擴充檔案也不需收費。擴充檔案會儲存在裝置的共用儲存位置 (SD 卡或可掛接 USB 分割區,也就是「外部」儲存空間),以供應用程式存取。在大多數的裝置上,Google Play 會在下載 APK 時一併下載擴充檔案,所以使用者第一次開啟您的應用程式時已萬事俱備。不過在某些情況下,您的應用程式必須在啟動時從 Google Play 下載這些檔案。
如要避免使用擴充檔案,而且應用程式的壓縮下載大小超過 100 MB,請改用 Android App Bundle 上傳應用程式,這樣就能上傳壓縮下載大小最高為 200 MB 的檔案。此外,由於使用應用程式套件會延後 APK 產生和 Google Play 簽署程序,使用者只需下載最佳化 APK,即可取得執行應用程式所需的程式碼和資源。您不必建構、簽署及管理多個 APK 或擴充檔案,而使用者也能下載更精簡優質的應用程式。
總覽
每次您透過 Google Play 管理中心上傳 APK 時,可以選擇加入一至兩個擴充檔案。每個檔案的大小上限為 2 GB,任何格式皆可,但建議您在下載時使用壓縮檔以便節省頻寬。 就概念上來說,每一個擴充檔案都有不同的功用:
- 主要擴充檔案為應用程式所需額外資源的主要擴充檔案。
- 修補型擴充檔案為選用項目,用於主要擴充檔案的小型更新。
您可以隨心所欲地運用這兩個擴充檔案,但我們建議以主要擴充檔案傳送主要素材資源,且不宜頻繁更新;修補型擴充檔案應為較小的檔案,作用為「修補載體」,於必要時或主要版本發布時更新。
不過,即使您的應用程式更新只需要一個新的修補型擴充檔案,您還是必須在資訊清單中上傳有更新 versionCode
的新 APK (Play 管理中心不允許上傳擴充檔案至現有的 APK)。
注意事項:修補型擴充檔案在語意上與主要擴充檔案相同,您可以隨心所欲地運用每個檔案。
檔案名稱格式
您可以上傳任何格式的擴充檔案 (ZIP、PDF、MP4 等),您也可以使用 JOBB 工具封裝並加密一組資源檔案和後續的修補型檔案。不論檔案類型為何,Google Play 都會視為「opaque binary blobs」,並根據以下方式重新命名檔案:
[main|patch].<expansion-version>.<package-name>.obb
此命名方式分為三部分:
main
或patch
- 指出檔案為主要擴充檔案或修補型擴充檔案。每個 APK 只能有一個主要擴充檔案以及一個修補型擴充檔案。
<expansion-version>
- 此部分為一個與 APK 版本代碼相符的整數,該 APK 為擴充檔案「第一個」相關聯的 APK (這個整數與應用程式的
android:versionCode
值相符)。雖然 Play 管理中心允許新的 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()
取得。
在此目錄中,每個應用程式都不會有超過 2 個擴充檔案,
一個是主要擴充檔案,另一個是修補型擴充檔案 (如有需要)。當您使用新的擴充檔案更新應用程式時,將會覆寫以前的版本。自 Android 4.4 (API 級別 19) 起,應用程式即使沒有外部儲存空間權限,也能讀取 OBB
擴充檔案。不過,部分 Android 6.0 (API 級別 23) 以上版本的執行仍需要權限,因此您必須在應用程式資訊清單中宣告 READ_EXTERNAL_STORAGE
權限,並在執行階段請求權限,如下所示:
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
Android 6 以上版本在執行階段必須請求外部儲存空間權限。 不過,部分 Android 執行不需要讀取 OBB 檔案的權限。下列程式碼片段說明了如何在要求外部儲存空間權限之前,檢查讀取權限:
Kotlin
val obb = File(obb_filename) var open_failed = false try { BufferedReader(FileReader(obb)).also { br -> ReadObbFile(br) } } catch (e: IOException) { open_failed = true } if (open_failed) { // request READ_EXTERNAL_STORAGE permission before reading OBB file ReadObbFileWithPermission() }
Java
File obb = new File(obb_filename); boolean open_failed = false; try { BufferedReader br = new BufferedReader(new FileReader(obb)); open_failed = false; ReadObbFile(br); } catch (IOException e) { open_failed = true; } if (open_failed) { // request READ_EXTERNAL_STORAGE permission before reading OBB file ReadObbFileWithPermission(); }
如果您必須解除封裝擴充檔案的內容,解除封裝後請不要刪除 OBB
擴充檔案,也請不要將解除封裝的資料儲存於同一個目錄,您應該將解除封裝的檔案儲存在 getExternalFilesDir()
指定的目錄。不過如果情況允許,還是建議使用不需解除封裝便可直接從檔案讀取的擴充檔案格式。例如,我們提供了一款程式庫專案叫 APK 擴充程式壓縮程式庫,可以直接從 ZIP 檔案讀取您的資料。
注意事項:有別於 APK 檔案,使用者和其他應用程式可以讀取儲存於共用儲存空間的檔案。
提示:如果要將媒體檔案封裝至 ZIP 檔案,可以在具有偏移和長度控制項的檔案上使用媒體播放呼叫 (例如 MediaPlayer.setDataSource()
和 SoundPool.load()
),而不需要將 ZIP 解除封裝。為確保可正常運作,請不要在建立 ZIP 套件時另外壓縮媒體檔案。舉例來說,使用 zip
工具時,您應該用 -n
選項指定不應壓縮的檔案後置字元:
zip -n .mp4;.ogg main_expansion media_files
下載過程
在大多數情況下,Google Play 會在將 APK 下載到裝置的同時,下載並儲存您的擴充檔案;但有時候 Google Play 無法下載擴充檔案,或使用者可能已刪除先前下載的擴充檔案。為了處理這種情況,您的應用程式必須能在主要活動開始時,使用 Google Play 提供的網址自行下載擴充檔案。
高層級的下載過程如下:
- 使用者選擇從 Google Play 安裝您的應用程式。
- 如果 Google Play 能夠下載擴充檔案 (大部分的裝置都是如此),就會在下載 APK 時一併下載。
如果 Google Play 無法下載擴充檔案,就只會下載 APK。
- 使用者啟動應用程式時,應用程式必須檢查擴充檔案是否已儲存在裝置中。
請注意:如果應用程式啟動時,裝置上卻還沒有擴充檔案,請務必在從 Google Play 下載擴充檔案時納入必要的程式碼。如下方「下載擴充檔案」一節中的說明,我們提供了一款程式庫可充分簡化這項程序,並透過需要最少程式碼的服務執行下載。
開發清單
以下是擴充檔案與應用程式搭配使用時需執行的工作摘要:
- 請先確認應用程式的壓縮下載大小是否需要超過 100 MB。 空間很寶貴,請盡可能將總下載大小維持在最小。如果您的應用程式有多種版本的圖像資源以適用於多種螢幕密度,因而需要大於 100 MB 的空間,建議您發布多個 APK,且每個 APK 中只含有特定螢幕所需的素材資源。如要在發布至 Google Play 時獲得最佳結果,請上傳 Android App Bundle,其中包含應用程式的所有編譯程式碼和資源,但會延後 APK 的產生和 Google Play 簽署程序。
- 請決定哪些資源要與 APK 分開,並將這些資源封裝成一個檔案,做為主要擴充檔案使用。
通常只有在更新主要擴充檔案時,才能使用第二個修補型擴充檔案。不過,如果您的主要擴充檔案資源超過 2 GB 上限,您可以將修補型擴充檔案用於其他素材資源。
- 開發應用程式時,請設定應用程式使用在裝置共用儲存位置的擴充檔案資源。
請記得不可刪除、移動或重新命名擴充檔案。
如果您的應用程式無特定格式要求,建議您先建立擴充檔案的 ZIP 檔案,再透過 APK 擴充程式壓縮程式庫讀取檔案。
- 為應用程式的主要活動加入邏輯,以便於應用程式啟動時檢查擴充檔案是否已在裝置上。如果擴充檔案不在裝置上,請透過 Google Play 的應用程式授權服務請求擴充檔案的網址,再下載並儲存擴充檔案。
如想大量減少您需撰寫的程式碼,並確保下載時有良好的使用者體驗,建議使用下載工具庫執行下載行為。
如果不使用程式庫,而是建立自己的下載服務,請注意您不能變更擴充檔案名稱,並且必須將擴充檔案儲存於適當的儲存位置。
完成應用程式開發後,請依照「測試擴充檔案」指南進行接下來的步驟。
規則及限制
透過 Play 管理中心上傳應用程式時,您可以使用新增 APK 擴充檔案的功能。第一次上傳應用程式或更新使用擴充檔案的應用程式時,請務必注意以下規則及限制:
- 每個擴充檔案不得大於 2 GB。
- 如要從 Google Play 下載您的擴充檔案,使用者必須已經從 Google Play 取得您的應用程式。如果應用程式是透過其他方式安裝,Google Play 就不會提供擴充檔案的網址。
- 在每一次應用程式內下載中,Google Play 為每個檔案提供的網址都不會重複,而且在傳送給應用程式後不久就會到期。
- 如果您以新的 APK 更新應用程式,或為同一個應用程式上傳多個 APK,您可以選擇為舊 APK 上傳的擴充檔案。擴充檔案名稱不會改變,仍保留 APK 接收的版本 (該 APK 為擴充檔案最初相關聯的 APK)。
- 如果您為了向不同裝置提供不同的擴充檔案,而與多個 APK 一併使用擴充檔案,您仍須為每種裝置上傳個別的 APK,以便為每個 APK 提供不重複的
versionCode
值和宣告不同的篩選條件。 - 您無法只透過變更擴充檔案更新應用程式,而是必須上傳新的 APK 才能更新應用程式。如果變更只與擴充檔案中的素材資源相關,則您只需變更
versionCode
(可能還有versionName
) 即可更新 APK。 - 請勿將其他資料存入
obb/
目錄。如果您必須解除封裝某些資料,請將其存入getExternalFilesDir()
指定的位置。 - 請勿刪除或重新命名
.obb
擴充檔案 (除非您正在更新),這麼做會導致 Google Play (或您的應用程式) 重複下載相同的擴充檔案。 - 手動更新擴充檔案時,必須刪除先前的擴充檔案。
下載擴充檔案
在大多數情況下,Google Play 會在安裝或更新 APK 的同時,下載擴充檔案並將其儲存到裝置。這樣一來,在應用程式首次啟動時即可使用擴充檔案。不過在某些情況下,您的應用程式必須提出請求,再自行從 Google Play 應用程式授權服務回應中提供的網址下載擴充檔案。
以下為下載擴充檔案時所需的基本邏輯:
- 應用程式啟動時,請在共用儲存位置 (位於
Android/obb/<package-name>/
目錄) 中尋找擴充檔案。
如果您的應用程式免費 (非付費應用程式),您可能還尚未用到應用程式授權服務。這項服務的主要目的是為了讓您針對應用程式執行授權政策,並確保使用者有權利使用您的應用程式 (使用者已在 Google Play 透過正當管道付費)。為了發揮擴充檔案的功能,我們已提升授權服務,以便回應您的應用程式,並在回應中包含 Google Play 所代管應用程式擴充檔案的網址。因此,即使您的應用程式免費,仍然需要納入「授權驗證庫」 (LVL) 才能使用 APK 擴充檔案。如果您的應用程式免費,當然就不需要執行授權驗證,只需透過驗證庫提出傳回擴充檔案網址的請求。
注意:不論您的應用程式是否免費,只有在使用者從 Google Play 取得應用程式時,Google Play 才會傳回擴充檔案網址。
除了 LVL,您還需要一組代碼,利用這組代碼可透過 HTTP 連線下載擴充檔案,並將檔案儲存在裝置共用儲存空間中的適當位置。您在應用程式中建立這項程序時,請注意以下幾點:
- 裝置可能沒有足夠的空間容納擴充檔案,下載前應先檢查空間是否足夠,如果空間不足則應該提醒使用者。
- 檔案下載應在背景服務中執行,以避免干擾使用者互動,並允許使用者在下載完成時離開應用程式。
- 請求及下載期間可能會發生各種錯誤,請務必妥善處理。
- 在下載期間,網路連線狀況可能會改變,請妥善處理,如果下載中斷,請在恢復正常時繼續下載。
- 在背景中執行下載時,您應該提供通知以顯示下載進度,告知使用者下載完成,並在使用者選取時將使用者導向回應用程式。
為簡化這項工作,我們建立了「下載工具庫」。下載工具庫會透過授權服務請求擴充檔案網址、下載擴充檔案並執行上述所有工作,甚至允許您暫停活動以繼續下載。藉由新增下載工具庫和將一些程式碼掛鉤至您的應用程式,下載擴充檔案的工作就幾乎已經完成編碼。因此,我們建議您使用下載工具庫下載擴充檔案,以便輕鬆替您提供最佳使用者體驗。以下各節中的資訊說明了如何將程式庫整合至您的應用程式。
如果您比較希望自己開發透過 Google Play 網址下載擴充檔案的方式,您必須按照應用程式授權說明文件中的指示提出授權請求,再從回應的附加項目中取得擴充檔案名稱、大小及網址。您應該使用 APKExpansionPolicy
類別 (包含在授權驗證庫中) 做為您的授權政策,此類別從授權服務擷取了擴充檔案名稱、大小及網址。
關於下載工具庫
建議您使用包含在 Google Play APK 擴充檔案庫套件中的下載工具庫,以便一併使用 APK 擴充檔案和您的應用程式,輕鬆替您提供最佳使用者體驗。這個程式庫會在背景服務中下載您的擴充檔案、向使用者顯示下載狀態通知、處理網路連線中斷、在恢復正常時繼續下載,以及執行更多其他操作。
請按照以下指示,透過下載工具庫下載擴充檔案:
- 擴充特殊的
Service
子類別和BroadcastReceiver
子類別,每個子類別只需要幾行程式碼。 - 為主要活動加入一些邏輯,檢查是否已下載擴充檔案;如果尚未下載,啟動下載程序並顯示下載進度使用者介面。
- 執行回呼介面,在您的主要活動中加入幾個接收下載進度更新的方法。
以下各節說明了如何使用下載工具庫設定您的應用程式。
準備使用下載工具庫
如要使用下載工具庫,您需要從 SDK Manager 下載兩個套件,並將合適的程式庫新增至您的應用程式。
首先,開啟 Android SDK Manager (「Tools」>「SDK Manager」),並在「Appearance & Behavior」>「System Settings」>「Android SDK」下方選取「SDK Tools」分頁標籤,以便選取和下載:
- Google Play 授權程式庫套件
- Google Play APK 擴充檔案庫套件
為授權驗證庫和下載工具庫建立一個新的程式庫模組。針對每個程式庫,您要:
- 依序選取「File」>「New」>「New Module」。
- 在「Create New Module」視窗中,選取「Android Library」,然後選取「Next」。
- 指定「應用程式/程式庫名稱」,例如「Google Play 授權程式庫」和「Google Play 下載工具庫」,選擇「Minimum SDK level」,然後選取「Finish」。
- 依序選取「File」>「Project Structure」。
- 選取「Properties」分頁標籤,然後在 「Library Repository」中輸入
<sdk>/extras/google/
目錄中的程式庫 (授權驗證庫為play_licensing/
,下載工具庫則為play_apk_expansion/downloader_library/
)。 - 選取「OK」,即可建立新模組。
注意事項:一定要有授權驗證庫才能使用下載工具庫。請務必將授權驗證庫新增至下載工具庫的專案屬性。
或者,透過指令列更新專案以納入程式庫:
- 將目錄變更為
<sdk>/tools/
目錄。 - 使用
--library
選項執行android update project
,將 LVL 和下載工具庫加入專案。例如:android update project --path ~/Android/MyApp \ --library ~/android_sdk/extras/google/market_licensing \ --library ~/android_sdk/extras/google/market_apk_expansion/downloader_library
將授權驗證庫及下載工具庫新增至您的應用程式後,就可以輕鬆快速地從 Google Play 下載擴充檔案。擴充檔案格式以及從共用儲存空間讀取的方式為兩種不同的執行作業,您應該根據應用程式的需求來考量如何選擇。
提示:APK 擴充程式檔套件包含一個應用程式範例,說明如何在應用程式中使用下載工具庫。這個範例使用 APK 擴充程式檔套件中提供的第三方程式庫,APK 擴充程式 ZIP 程式庫。如果打算為擴充檔案使用 ZIP 檔案,建議您也將 APK 擴充程式 ZIP 程式庫加入應用程式。詳情請參閱「使用 APK 擴充程式 ZIP 程式庫」一節。
宣告使用者權限
如要下載擴充檔案,下載工具庫需要一些權限,您必須在應用程式的資訊清單檔案中宣告這些權限。權限如下:
<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>
注意事項:根據預設,下載工具庫需要 API 級別 4,APK 擴充程式 ZIP 程式庫則需要 API 級別 5。
執行下載服務
下載工具庫提供名稱為 DownloaderService
的 Service
子類別,您應擴充這個類別,以便在背景服務中執行下載。除了為您下載擴充檔案外,DownloaderService
也會:
- 註冊監聽裝置網路連線變化的
BroadcastReceiver
(CONNECTIVITY_ACTION
廣播),以便在必要時 (例如網路中斷) 暫停下載,以及在狀況恢復正常時繼續下載 (取得網路連線)。 - 安排
RTC_WAKEUP
警示,在服務中止時重新嘗試下載。 - 建立顯示下載進度、錯誤或狀態變化的自訂
Notification
。 - 允許應用程式手動暫停及繼續下載。
- 下載擴充檔案前,請確認共用儲存空間已掛接且可以使用、檔案已不存在,以及有足夠的空間。如果有任何一項為否,請通知使用者。
您只需要在應用程式中建立類別來擴充 DownloaderService
類別,並覆寫三個方法即可提供特定應用程式詳細資料:
getPublicKey()
- 這個方法必須傳回字串,該字串為您發布者帳戶的 Base64 編碼 RSA 公開金鑰,可從 Play 管理中心的個人資料頁面取得 (請參閱「設定授權」)。
getSALT()
- 這個方法必須傳回一個隨機位元組陣列,授權
Policy
運用此陣列建立一個Obfuscator
。「SALT」確保經過模糊處理、內含授權資料的SharedPreferences
檔案,是不重複且無法被偵測的檔案。 getAlarmReceiverClassName()
- 這個方法必須在應用程式內傳回
BroadcastReceiver
的類別名稱,您的應用程式則接收應重新下載的警示 (下載服務意外停止時可能會出現)。
例如,以下為 DownloaderService
的完整執行:
Kotlin
// You must use the public key belonging to your publisher account const val BASE64_PUBLIC_KEY = "YourLVLKey" // You should also modify this salt val SALT = byteArrayOf( 1, 42, -12, -1, 54, 98, -100, -12, 43, 2, -8, -4, 9, 5, -106, -107, -33, 45, -1, 84 ) class SampleDownloaderService : DownloaderService() { override fun getPublicKey(): String = BASE64_PUBLIC_KEY override fun getSALT(): ByteArray = SALT override fun getAlarmReceiverClassName(): String = SampleAlarmReceiver::class.java.name }
Java
public class SampleDownloaderService extends DownloaderService { // You must use the public key belonging to your publisher account public static final String BASE64_PUBLIC_KEY = "YourLVLKey"; // You should also modify this salt public static final byte[] SALT = new byte[] { 1, 42, -12, -1, 54, 98, -100, -12, 43, 2, -8, -4, 9, 5, -106, -107, -33, 45, -1, 84 }; @Override public String getPublicKey() { return BASE64_PUBLIC_KEY; } @Override public byte[] getSALT() { return SALT; } @Override public String getAlarmReceiverClassName() { return SampleAlarmReceiver.class.getName(); } }
注意事項:您必須將 BASE64_PUBLIC_KEY
值更新為發布者帳戶的公開金鑰。您可以在「開發人員控制台」中的個人資料下方找到公開金鑰,在測試下載時也需要用到金鑰。
請記得在資訊清單檔案中宣告服務:
<app ...> <service android:name=".SampleDownloaderService" /> ... </app>
執行警示接收器
為了監控檔案下載進度並在必要時重新下載,
DownloaderService
設定RTC_WAKEUP
的鬧鐘
會將Intent
給 BroadcastReceiver
到
應用程式。您必須定義 BroadcastReceiver
才能呼叫 API
下載工具庫,檢查下載狀態並重新啟動
並視需要顯示。
您只需要覆寫 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>
開始下載
您應用程式中的主要活動 (透過啟動器圖示啟動的活動) 負責驗證擴充檔案是否已在裝置上,如果沒有便會開始下載。
如要使用下載工具庫開始下載,請按照下列步驟進行:
- 檢查檔案是否已經下載。
下載工具庫含有一些
Helper
類別中的 API,可協助執行此程序:getExpansionAPKFileName(Context, c, boolean mainFile, int versionCode)
doesFileExist(Context c, String fileName, long fileSize)
例如,APK 擴充檔案套件中提供的範例應用程式會呼叫以下活動中的
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,應用程式就必須開始下載程序。
- 透過呼叫靜態方法
DownloaderClientMarshaller.startDownloadServiceIfRequired(Context c, PendingIntent notificationClient, Class<?> serviceClass)
開始下載。方法使用的參數如下:
context
:應用程式的Context
。notificationClient
:啟動主要活動的PendingIntent
。這個參數用於DownloaderService
建立的Notification
,以顯示下載進度。使用者選取通知後,系統會叫用您提供的PendingIntent
,並且會開啟顯示下載進度的活動 (通常與開始下載程序的活動相同)。serviceClass
:用於執行DownloaderService
的Class
物件,是啟動服務的必要物件,並會視需要開始下載程序。
這個方法會傳回一個整數,指出是否需要進行下載。可能的值為:
NO_DOWNLOAD_REQUIRED
:如果檔案已存在或是正在進行下載,會傳回此值。LVL_CHECK_REQUIRED
:如果需要授權驗證以取得擴充檔案網址,會傳回此值。DOWNLOAD_REQUIRED
:如果已知擴充檔案網址但尚未下載,會傳回此值。
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 }
- 如果
startDownloadServiceIfRequired()
方法傳回NO_DOWNLOAD_REQUIRED
「以外」的任何內容,請呼叫DownloaderClientMarshaller.CreateStub(IDownloaderClient client, Class<?> downloaderService)
來建立IStub
的執行個體。IStub
能將您的活動繫結至下載服務,您的活動便可以接收到下載進度的回呼。如要透過呼叫
CreateStub()
將IStub
執行個體化,您必須將IDownloaderClient
介面執行和DownloaderService
的執行結果傳給後者。下一節「接收下載進度」將說明IDownloaderClient
介面,您通常應該執行Activity
類別,以便能在下載狀態改變時更新活動使用者介面。建議呼叫
CreateStub()
,在startDownloadServiceIfRequired()
開始下載程序後,在活動的onCreate()
方法中將IStub
執行個體化。例如,在先前的
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()
的呼叫,請接著在IStub
上呼叫connect()
,並傳送應用程式的Context
。反之,則應在活動的onStop()
回呼中呼叫disconnect()
。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(); }
在
IStub
上呼叫connect()
即可將活動繫結至DownloaderService
,這樣您的活動會透過IDownloaderClient
介面接收有關下載狀態變更的回呼。
接收下載進度
您必須先執行下載工具庫的 IDownloaderClient
介面,才能接收有關下載進度的更新以及與 DownloaderService
互動。您用來開始下載程序的活動通常應該要執行這個介面,才能顯示下載進度並向服務傳送請求。
以下為 IDownloaderClient
所需的介面方法:
onServiceConnected(Messenger m)
- 您在活動中將
IStub
執行個體化後,會接收到此方法的呼叫,這個呼叫會傳遞一個與DownloaderService
個體連線的Messenger
物件。如要向服務傳送請求 (例如暫停下載和繼續下載),您必須呼叫DownloaderServiceMarshaller.CreateProxy()
以接收連線至服務的IDownloaderService
介面。以下為建議執行範例:
Kotlin
private var remoteService: IDownloaderService? = null ... override fun onServiceConnected(m: Messenger) { remoteService = DownloaderServiceMarshaller.CreateProxy(m).apply { downloaderClientStub?.messenger?.also { messenger -> onClientUpdated(messenger) } } }
Java
private IDownloaderService remoteService; ... @Override public void onServiceConnected(Messenger m) { remoteService = DownloaderServiceMarshaller.CreateProxy(m); remoteService.onClientUpdated(downloaderClientStub.getMessenger()); }
在
IDownloaderService
物件初始化後,您可以將指令傳送至下載工具服務,例如暫停下載和繼續下載 (requestPauseDownload()
和requestContinueDownload()
)。 onDownloadStateChanged(int newState)
- 下載狀態改變時 (例如下載開始或完成),下載服務會呼叫此方法。
newState
值會是IDownloaderClient
類別STATE_*
常數所指定的其中一個可能的值。您可以透過呼叫
Helpers.getDownloaderStringResourceIDFromState()
為每個狀態請求對應的字串,以便提供符合使用者需求的訊息。呼叫後會傳回一個下載工具庫附帶字串的資源 ID。例如,「因為您正在漫遊因此下載暫停」字串與STATE_PAUSED_ROAMING
對應。 onDownloadProgress(DownloadProgressInfo progress)
- 下載服務呼叫此方法以傳送
DownloadProgressInfo
物件,該物件描述各種關於下載進度的資訊,包括預計剩餘時間、目前下載速度、整體進度以及總檔案數,以便您更新下載進度使用者介面。
提示:如需進一步瞭解更新下載進度使用者介面的回呼範例,請參見 APK 擴充檔案套件所提供範例應用程式中的 SampleDownloaderActivity
。
以下為一些 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
,請參閱「新增授權至您的應用程式」說明文件,內文說明了如何執行這類授權政策。
讀取擴充檔案
您的 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
和所需擴充檔案的版本。
判斷擴充檔案版本編號的方式有很多種,其中一個簡單的方法是,透過 APKExpansionPolicy
類別的 getExpansionFileName(int index)
方法查詢擴充檔案名稱,在下載開始時將版本儲存於 SharedPreferences
檔案。當您要存取擴充檔案時,可以透過讀取 SharedPreferences
檔案取得版本編號。
如需進一步瞭解關於從共用儲存空間讀取的資訊,請參閱「資料儲存空間」說明文件。
使用 APK 擴充程式 ZIP 程式庫
Google Market APK 擴充程式套件中有一個程式庫,叫做「APK 擴充程式 ZIP 程式庫」(位於 <sdk>/extras/google/google_market_apk_expansion/zip_file/
)。您可選用這個程式庫,以便在擴充檔案儲存為 ZIP 檔案時讀取檔案。這個程式庫可讓您以虛擬檔案系統的形式,輕鬆讀取 ZIP 擴充檔案中的資源。
APK 擴充程式 ZIP 程式庫包括以下類別和 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)
- 為 ZIP 檔案中的檔案提供
AssetFileDescriptor
。assetPath
必須是所需檔案的路徑,且與 ZIP 檔案內容的根部相對。這個方法對於某些需要AssetFileDescriptor
的 Android API (例如一些MediaPlayer
API) 非常實用。
APEZProvider
- 大部分的應用程式不需要用到。這個類別會定義
ContentProvider
,透過內容整理 ZIP 檔案的資料 提供者Uri
為特定 Android API 提供檔案存取權 需要Uri
存取媒體檔案。舉例來說,如果想要使用VideoView.setVideoURI()
播放影片,這就非常實用。
略過媒體檔案的 ZIP 壓縮功能
如果您使用擴充檔案儲存媒體檔案,ZIP 檔案仍可讓您使用 Android 媒體播放呼叫,藉此提供偏移和時間長度控制項 (例如 MediaPlayer.setDataSource()
和 SoundPool.load()
)。為確保可順利運作,請不要在建立 ZIP 套件時另外壓縮媒體檔案。舉例來說,使用 zip
工具時,您應該用 -n
選項指定不應壓縮的檔案後置字元:
zip -n .mp4;.ogg main_expansion media_files
從 ZIP 檔案讀取
使用 APK 擴充程式 ZIP 程式庫時,從 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);
以上程式碼透過讀取兩個擴充檔案中所有檔案的合併對應,提供主要擴充檔案或修補型擴充檔案中的任一檔案存取權。您只需應用程式 android.content.Context
及主要擴充檔案和修補型擴充檔案的版本號碼,以提供 getAPKExpansionFile()
方法。
如果您比較想要從特定的擴充檔案讀取,可以將 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 之前,您應測試應用程式是否能從共用儲存空間讀取檔案。您只需要將檔案新增至裝置共用儲存空間中的適當位置,並啟動應用程式:
- 在裝置的共用儲存空間中建立合適的目錄,Google Play 會將檔案儲存在該目錄。
舉例來說,如果套件名稱是
com.example.android
,您需要在共用儲存空間中建立目錄Android/obb/com.example.android/
(將測試裝置插入電腦,掛接共用儲存空間,並手動建立此目錄)。 - 手動將擴充檔案新增至上述目錄。請務必重新命名檔案,使其與 Google Play 將使用的檔案名稱格式相符。
舉例來說,無論檔案類型為何,
com.example.android
應用程式的主要擴充檔案都應為main.0300110.com.example.android.obb
。 版本編號可以是任何值,不過請記得:- 主要擴充檔案一律會以
main
開頭,修補型檔案則以patch
開頭。 - 套件名稱必須一律與 Google Play 上附加該檔案的 APK 檔名相符。
- 主要擴充檔案一律會以
- 擴充檔案現已在裝置上,您可以安裝檔案、執行應用程式並測試擴充檔案。
以下是有關處理擴充檔案的一些提示:
- 請勿刪除或重新命名
.obb
擴充檔案 (即使您將資料解除封裝到不同位置),這麼做會導致 Google Play (或您的應用程式) 重複下載相同的擴充檔案。 - 請勿將其他資料存入
obb/
目錄。如果您必須解除封裝某些資料,請將其存入getExternalFilesDir()
指定的位置。
測試檔案下載
有時您的應用程式必須在首次開啟時手動下載擴充檔案,因此請務必測試這項程序,確保您的應用程式可成功查詢網址、下載擴充檔案,並將檔案儲存至裝置。
如要測試應用程式的手動下載程序執行情況,您可以將應用程式發布至內部測試群組,僅供授權測試人員存取。如果一切運作正常,您的應用程式就會在主要活動開始時下載擴充檔案。
注意事項:以前您可以上傳未發布的「草稿」版本,並藉此測試應用程式,但系統現在已不支援這項功能。請改為發布至內部、封閉或公開測試群組。詳情請參閱「不再支援草稿應用程式」。
更新應用程式
您不用重新下載所有原始素材資源就可以更新應用程式,這是在 Google Play 使用擴充檔案的一大優勢。Google Play 允許每個 APK 都可以有兩個擴充檔案,因此第二個檔案可作為「修補型」擴充檔案,用來提供更新和新素材資源。如此一來,使用者就不需要重新下載花費高又占空間的主要擴充檔案。
修補型擴充檔案在技術上與主要擴充檔案相同,而且 Android 系統或 Google Play 都沒有執行主要擴充檔案與修補型程式檔之間的實際修補程序。您的應用程式程式碼必須自行執行必要的修補程式。
如果您的擴充檔案是 ZIP 檔,APK 擴充檔案套件的 APK 擴充程式 ZIP 程式庫可以將修補型擴充檔案與主要擴充檔案合併。
注意事項:即使只有修補型擴充檔案需要變更,您還是必須更新 APK,如此一來 Google Play 才會執行更新作業。如果不需要在應用程式中變更程式碼,您只要在資訊清單中更新 versionCode
。
只要您不在 Play 管理中心變更與 APK 相關聯的主要擴充檔案,先前安裝過您應用程式的使用者就不會下載主要擴充檔案。現有使用者只會收到更新後的 APK 和新的修補型擴充檔案 (保留先前的主要擴充檔案)。
請留意以下幾點關於擴充檔案更新的注意事項:
- 每個應用程式一次只能有兩個擴充檔案,一個是主要擴充檔案,另一個是修補型擴充檔案。更新擴充檔案時,Google Play 會刪除之前的版本 (進行手動更新時,您的應用程式也必須進行此操作)。
- 新增修補型擴充檔案時,Android 系統並不會修補您的應用程式或主要擴充檔案,您必須將應用程式設計為可支援修補程式資料。不過,APK 擴充檔案套件包含 ZIP 擴充檔案的程式庫,可將修補型擴充檔案的資料併入主要擴充檔案,因此您可以輕鬆讀取所有擴充檔案資料。