Using Apk Expansion Files to Upload Large Apk
How to Set Up Android App to Back up Expansion Files
We all know that we cannot brand an Android apk that's more than 100MB. And this is something that every developer should proceed in mind while developing. Every resource matters, you should not add a single resource that is non required. As every MB counts when the user is downloading your app. So the best approach should be to keep the apk as pocket-size as possible. However, some apps may need more infinite for high-allegiance graphics, media files or other large avails. And to cater such applications Google provides the flexibility to add together Expansion Files.
Expansion Files
Expansion files are simply files/folders in archived format(.obb to be precise). Google Play allows you to add two big expansion files to a maximum of 2GB for each file. Google Play hosts the expansion files for your application and serves them to the device at no cost to you lot. The expansion files are stored to the device's shared storage location where your app can access them. On newer devices, Google Play downloads the expansion files at the aforementioned fourth dimension it downloads the apk, and so your application has everything it needs when your user opens it for the offset time. In some of the older devices, we have to write our ain awarding logic to download the files from Google Play.
Google Play hosts the expansion files for your awarding and serves them to the device at no price to you. The expansion files are saved to the device's shared storage location (the SD card or USB-mountable segmentation; likewise known as the "external" storage) where your app tin admission them. On most devices, Google Play downloads the expansion file(s) at the same time it downloads the APK, so your application has everything it needs when the user opens it for the first time. In some cases, however, your application must download the files from Google Play when your application starts.
The following is a flowchart that describes the procedure flow of using Expansion Files.
Expansion File Types
The following are the types of expansion files that you can add to your application at Google Play Developer Console.
- The main expansion file is the primary expansion file for additional resources required by your awarding.
- The patch expansion file is optional and intended for small updates to the main expansion file.
File proper noun format
You can upload any format (Zero, PDF, MP4, etc) every bit an expansion file. Regardless of the file type you upload, Google Play considers them opaque binary blobs and renames the files using the post-obit scheme:
[main|patch].<expansion-version>.<package-name>.obb
Storage Location
When Google Play downloads your expansion files to a device, it saves them to the system's shared storage location. To ensure proper behavior, y'all must not delete, move, or rename the expansion files. In the event that your awarding must perform the download from Google Play itself, y'all must save the files to the exact aforementioned location.
The specific location for your expansion files is:
<shared-storage>/Android/obb/<package-name>/
-
<shared-storage>
is the path to the shared storage space, available fromgetExternalStorageDirectory()
. -
<packet-name>
is your application's Java-fashion package name, available fromgetPackageName()
.
Implementation
Download required packages
To utilise the Downloader Library, y'all need to download two packages from the SDK Manager and add together the appropriate libraries to your application.
First, open the Android SDK Manager, expand Extras and download:
- Google Play Licensing Library package
- Google Play APK Expansion Library packet
Android Manifest
Declare the following permissions in AndroidManifest.xml
<!-- Required to admission Google Play Licensing -->
<uses-permission android:name="com.android.vending.CHECK_LICENSE" /> <!-- Required to download files from Google Play -->
<uses-permission android:proper name="android.permission.Internet" /> <!-- Required to keep CPU alive while downloading files (NOT to proceed screen awake) -->
<uses-permission android:name="android.permission.WAKE_LOCK" /> <!-- Required to poll the land 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:proper name="android.permission.WRITE_EXTERNAL_STORAGE" />
While you are there, let's likewise add the service and broadcast receiver nosotros are going to need to handle the downloads. We will add these classes later to the project. Add together these somewhere inside your application
tags.
<service android:name=".expansion.DownloaderService"/>
<receiver android:proper name=".expansion.DownloaderServiceBroadcastReceiver" />
Integrate Libraries
Downloader Library
To use APK expansion files and provide the best user experience with minimal effort, we will use the Downloader Library that'south included in the Google Play APK Expansion Library package. This library downloads your expansion files in a groundwork service, shows a user notification with the download status, handles network connectivity loss, resumes the download when possible, and more.
Re-create the source for the Downloader Library from<sdk>/extras/google/play_apk_expansion/downloader_library/src
to your project.
Licensing
In order to facilitate the expansion file functionality, the licensing service has been enhanced to provide a response to your application that includes the URL of your application's expansion files that are hosted on Google Play. And then, fifty-fifty if your awarding is free for users, yous need to include the License Verification Library (LVL) to employ APK expansion files. Of class, if your awarding is free, you don't need to enforce license verification — you simply need the library to perform the request that returns the URL of your expansion files.
Copy the sources from <sdk>/extras/google/play_licensing/library/src
to your projection.
Zip File
The Google Market Apk Expansion package includes a library called the APK Expansion Zero Library (located in <sdk>/extras/google/google_market_apk_expansion/zip_file/
). This is an optional library that helps you read your expansion files when they're saved equally Aught files. Using this library allows you to easily read resources from your ZIP expansion files as a virtual file system.
Re-create the sources from <sdk>/extras/google/google_market_apk_expansion/zip_file/
to your project.
Downloader Service
At present, create a new package named expansion
and create two files DownloaderService.java
andDownloaderServiceBroadcastReceiver.java
inside the parcel.
The DownloaderService
handles the downloading of expansion files from the Play Store and informs about the the progress to subscribing activities. Nosotros will take a look at how to configure the activeness to brainstorm downloads in a moment.
Notice: You lot must update the
BASE64_PUBLIC_KEY
value to be the public central belonging to your publisher business relationship. Yous can find the key in the Programmer Console under your profile data. This is necessary even when testing your downloads.
DownloaderService.java
public course DownloaderService extends com.google.android.vending.expansion.downloader.impl.DownloaderService {
public static final Cord BASE64_PUBLIC_KEY = "<<YOUR PUBLIC KEY Hither>>"; // TODO Add public key
private static terminal byte[] Salt = new byte[]{1, iv, -ane, -ane, 14, 42, -79, -21, 13, 2, -8, -11, 62, one, -x, -101, -19, 41, -12, 18};
// TODO Supersede with random numbers of your choice
@Override public String getPublicKey() {
return BASE64_PUBLIC_KEY;
}
@Override public byte[] getSALT() {
return Salt;
}
@Override public String getAlarmReceiverClassName() {
return DownloaderServiceBroadcastReceiver.class.getName();
}
}
The BoradcastReceiver will starting time the download service if the files demand to downloaded.
DownloaderServiceBroadcastReceiver.java public form DownloaderServiceBroadcastReceiver extends android.content.BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
try {
DownloaderClientMarshaller.startDownloadServiceIfRequired(context, intent, DownloaderService.course);
} catch (PackageManager.NameNotFoundException e) {
due east.printStackTrace();
}
}
}
Initiating Downloads
Now, allow'southward see how we tin initiate the downloads when the app starts. Add this somewhere in your master activeness.
private IDownloaderService mRemoteService;
individual IStub mDownloaderClientStub;
private int mState;
individual boolean mCancelValidation; // region Expansion Downloader
individual static course XAPKFile {
public terminal boolean mIsMain;
public last int mFileVersion;
public concluding long mFileSize; XAPKFile(boolean isMain, int fileVersion, long fileSize) {
mIsMain = isMain;
mFileVersion = fileVersion;
mFileSize = fileSize;
}
} private static final XAPKFile[] xAPKS = {
new XAPKFile(
true, // true signifies a primary file
2, // the version of the APK that the file was uploaded against
47529382L // the length of the file in bytes
)
};
static private final bladder SMOOTHING_FACTOR = 0.005f; /**
* Connect the stub to our service on start.
*/
@Override
protected void onStart() {
if (zip != mDownloaderClientStub) {
mDownloaderClientStub.connect(this);
}
super.onStart();
} /**
* Disconnect the stub from our service on cease
*/
@Override
protected void onStop() {
if (nada != mDownloaderClientStub) {
mDownloaderClientStub.disconnect(this);
}
super.onStop();
} /**
* Disquisitional implementation detail. In onServiceConnected nosotros create the
* remote service and marshaler. This is how we laissez passer the client information
* dorsum to the service so the client can be properly notified of changes. Nosotros
* must do this every time we reconnect to the service.
*/
@Override
public void onServiceConnected(Messenger m) {
mRemoteService = DownloaderServiceMarshaller.CreateProxy(m);
mRemoteService.onClientUpdated(mDownloaderClientStub.getMessenger());
} /**
* The download state should trigger changes in the UI --- it may be useful
* to show the state as being indeterminate at times. This sample can be
* considered a guideline.
*/
@Override
public void onDownloadStateChanged(int newState) {
setState(newState);
boolean showDashboard = true;
boolean showCellMessage = false;
boolean paused;
boolean indeterminate;
switch (newState) {
case IDownloaderClient.STATE_IDLE:
// STATE_IDLE means the service is listening, so information technology'south
// safe to starting time making calls via mRemoteService.
paused = faux;
indeterminate = true;
suspension;
instance IDownloaderClient.STATE_CONNECTING:
instance IDownloaderClient.STATE_FETCHING_URL:
showDashboard = true;
paused = false;
indeterminate = truthful;
interruption;
example IDownloaderClient.STATE_DOWNLOADING:
paused = false;
showDashboard = true;
indeterminate = imitation;
intermission; case IDownloaderClient.STATE_FAILED_CANCELED:
case IDownloaderClient.STATE_FAILED:
case IDownloaderClient.STATE_FAILED_FETCHING_URL:
case IDownloaderClient.STATE_FAILED_UNLICENSED:
paused = truthful;
showDashboard = false;
indeterminate = false;
pause;
example IDownloaderClient.STATE_PAUSED_NEED_CELLULAR_PERMISSION:
example IDownloaderClient.STATE_PAUSED_WIFI_DISABLED_NEED_CELLULAR_PERMISSION:
showDashboard = simulated;
paused = true;
indeterminate = imitation;
showCellMessage = true;
intermission; case IDownloaderClient.STATE_PAUSED_BY_REQUEST:
paused = true;
indeterminate = false;
interruption;
instance IDownloaderClient.STATE_PAUSED_ROAMING:
example IDownloaderClient.STATE_PAUSED_SDCARD_UNAVAILABLE:
paused = truthful;
indeterminate = false;
intermission;
case IDownloaderClient.STATE_COMPLETED:
showDashboard = fake;
paused = false;
indeterminate = false;
validateXAPKZipFiles();
return;
default:
paused = true;
indeterminate = true;
showDashboard = truthful;
}
int newDashboardVisibility = showDashboard ? View.VISIBLE : View.GONE;
if (mDownloadViewGroup.getVisibility() != newDashboardVisibility) {
mDownloadViewGroup.setVisibility(newDashboardVisibility);
}
mDownloadProgressBar.setIndeterminate(indeterminate);
} /**
* Sets the state of the various controls based on the progressinfo object
* sent from the downloader service.
*/
@Override
public void onDownloadProgress(DownloadProgressInfo progress) {
mDownloadProgressBar.setMax((int) (progress.mOverallTotal >> 8));
mDownloadProgressBar.setProgress((int) (progress.mOverallProgress >> viii));
mProgressPercentTextView.setText(Long.toString(progress.mOverallProgress * 100 / progress.mOverallTotal) + "%");
} /**
* Go through each of the Expansion APK files and open each as a zip file.
* Calculate the CRC for each file and return imitation if whatsoever fail to match.
*
* @render truthful if XAPKZipFile is successful
*/
void validateXAPKZipFiles() {
AsyncTask<Object, DownloadProgressInfo, Boolean> validationTask = new AsyncTask<Object, DownloadProgressInfo, Boolean>() { @Override
protected void onPreExecute() {
mDownloadViewGroup.setVisibility(View.VISIBLE);
super.onPreExecute();
} @Override
protected Boolean doInBackground(Object... params) {
for (XAPKFile xf : xAPKS) {
String fileName = Helpers.getExpansionAPKFileName(MainActivity.this, xf.mIsMain, xf.mFileVersion);
if (!Helpers.doesFileExist(MainActivity.this, fileName, xf.mFileSize, fake))
return false;
fileName = Helpers.generateSaveFileName(MainActivity.this, fileName);
ZipResourceFile zrf;
byte[] buf = new byte[1024 * 256];
try {
zrf = new ZipResourceFile(fileName);
ZipResourceFile.ZipEntryRO[] entries = zrf.getAllEntries();
/**
* First calculate the total compressed length
*/
long totalCompressedLength = 0;
for (ZipResourceFile.ZipEntryRO entry : entries) {
totalCompressedLength += entry.mCompressedLength;
}
float averageVerifySpeed = 0;
long totalBytesRemaining = totalCompressedLength;
long timeRemaining;
/**
* So calculate a CRC for every file in the Nil file,
* comparison it to what is stored in the Nil directory.
* Note that for compressed Zip files nosotros must excerpt
* the contents to do this comparison.
*/
for (ZipResourceFile.ZipEntryRO entry : entries) {
if (-1 != entry.mCRC32) {
long length = entry.mUncompressedLength;
CRC32 crc = new CRC32();
DataInputStream dis = naught;
endeavor {
dis = new DataInputStream(zrf.getInputStream(entry.mFileName)); long startTime = SystemClock.uptimeMillis();
while (length > 0) {
int seek = (int) (length > buf.length ? buf.length : length);
dis.readFully(buf, 0, seek);
crc.update(buf, 0, seek);
length -= seek;
long currentTime = SystemClock.uptimeMillis();
long timePassed = currentTime - startTime;
if (timePassed > 0) {
float currentSpeedSample = (float) seek / (float) timePassed;
if (0 != averageVerifySpeed) {
averageVerifySpeed = SMOOTHING_FACTOR * currentSpeedSample + (1 - SMOOTHING_FACTOR) * averageVerifySpeed;
} else {
averageVerifySpeed = currentSpeedSample;
}
totalBytesRemaining -= seek;
timeRemaining = (long) (totalBytesRemaining / averageVerifySpeed);
this.publishProgress(new DownloadProgressInfo(totalCompressedLength, totalCompressedLength - totalBytesRemaining, timeRemaining, averageVerifySpeed));
}
startTime = currentTime;
if (mCancelValidation)
return true;
}
if (crc.getValue() != entry.mCRC32) {
Log.e(Constants.TAG, "CRC does not match for entry: " + entry.mFileName);
Log.e(Constants.TAG, "In file: " + entry.getZipFileName());
render false;
}
} finally {
if (null != dis) {
dis.close();
}
}
}
}
} catch (IOException e) {
e.printStackTrace();
return false;
}
}
render true;
} @Override
protected void onProgressUpdate(DownloadProgressInfo... values) {
onDownloadProgress(values[0]);
super.onProgressUpdate(values);
} @Override
protected void onPostExecute(Boolean result) {
if (result) {
mDownloadViewGroup.setVisibility(View.GONE);
} else {
mDownloadViewGroup.setVisibility(View.VISIBLE);
}
super.onPostExecute(result);
} };
validationTask.execute(new Object());
} boolean expansionFilesDelivered() {
for (XAPKFile xf : xAPKS) {
Cord fileName = Helpers.getExpansionAPKFileName(this, xf.mIsMain, xf.mFileVersion);
if (!Helpers.doesFileExist(this, fileName, xf.mFileSize, false))
return false;
}
return true;
} private void setState(int newState) {
if (mState != newState) {
mState = newState;
}
} @Override
protected void onDestroy() {
this.mCancelValidation = true;
super.onDestroy();
}
// endregion
Add this to the stop of onCreate
in your master activity to start the downloads.
mDownloaderClientStub = DownloaderClientMarshaller.CreateStub(this, DownloaderService.grade); /**
* Before we practice anything, are the files we expect already here and
* delivered (presumably by Market) For gratuitous titles, this is probably
* worth doing. (so no Market asking is necessary)
*/
if (!expansionFilesDelivered()) { attempt {
Intent launchIntent = MainActivity.this.getIntent();
Intent intentToLaunchThisActivityFromNotification = new Intent(MainActivity.this, MainActivity.this.getClass());
intentToLaunchThisActivityFromNotification.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
intentToLaunchThisActivityFromNotification.setAction(launchIntent.getAction()); if (launchIntent.getCategories() != null) {
for (String category : launchIntent.getCategories()) {
intentToLaunchThisActivityFromNotification.addCategory(category);
}
} // Build PendingIntent used to open this activity from
// Notification
PendingIntent pendingIntent = PendingIntent.getActivity(MainActivity.this, 0, intentToLaunchThisActivityFromNotification, PendingIntent.FLAG_UPDATE_CURRENT);
// Request to beginning the download
int startResult = DownloaderClientMarshaller.startDownloadServiceIfRequired(this, pendingIntent, DownloaderService.class); if (startResult != DownloaderClientMarshaller.NO_DOWNLOAD_REQUIRED) {
// The DownloaderService has started downloading the files, show progress
initializeDownloadUI();
return;
} // otherwise, download not needed then we fall through to the app
} catch (PackageManager.NameNotFoundException e) {
Log.e(TAG, "Cannot find packet!", e);
}
} else {
validateXAPKZipFiles();
}
This assumes that your layout is something like this:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" tools:context=".MainActivity"> <!-- DOWNLOAD PROGRESS -->
<RelativeLayout
android:id="@+id/downloadViewGroup"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:visibility="gone"> <TextView
android:id="@+id/downloadTextView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:gravity="center" android:lines="ii" android:padding="10dp" android:text="@string/wait_file_download" /> <ProgressBar
android:id="@+id/downloadProgressBar"
mode="@android:fashion/Widget.ProgressBar.Horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/downloadTextView
android:padding="10dp" /> <TextView
android:id="@+id/downloadProgressPercentTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/downloadProgressBar"
android:layout_centerHorizontal="true"
android:lines="ii"
android:padding="10dp"
tools:text="10%" /> </RelativeLayout> <!-- YOUR MAIN CONTENT HERE --> <LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"> </LinearLayout> </LinearLayout>
If everything is all right, this should download the expansion files from Google Play if ever the user deletes them from their location or tampers with them.
Using the expansion files
At present, to the final part of this tutorial, how to use the expansion files that you merely downloaded. Let's assume we take some music files inside the expansion file and we want to play them using a media role player. We need to utilise the APKExpansionSupport
class to obtain a reference to the expansion file and and then open the music asset from the file.
// Get a ZipResourceFile representing a merger of both the main and patch files
try {
ZipResourceFile expansionFile = APKExpansionSupport.getAPKExpansionZipFile(this, 2, 0);
AssetFileDescriptor afd = expansionFile.getAssetFileDescriptor("path-to-music-from-expansion.mp3"); endeavor {
mMediaPlayer.setDataSource(afd.getFileDescriptor(), afd.getStartOffset(), afd.getDeclaredLength());
} catch (IllegalArgumentException | IllegalStateException | IOException east) {
Log.west(TAG, "Failed to update information source for media histrion", east);
} effort {
mMediaPlayer.prepareAsync();
} grab (IllegalStateException e) {
Log.westward(TAG, "Failed to prepare media thespian", e);
}
mState = Land.Preparing; try {
afd.close();
} take hold of (IOException e) {
Log.d(TAG, "Failed to close asset file descriptor", e);
}
} catch (IOException east) {
Log.westward(TAG, "Failed to find expansion file", e);
}
Reading media files from a Cipher
If you're using your expansion files to store media files, a ZIP file notwithstanding allows you to employ Android media playback calls that provide offset and length controls (such equally
MediaPlayer.setDataSource()
andSoundPool.load()
). In lodge for this to work, y'all must not perform additional pinch on the media files when creating the Zip packages. For example, when using the zilch tool, you should use the -north option to specify the file suffixes that should non exist compressed:
zip -n .mp4;.ogg main_expansion media_files
Submitting to play shop
When submitting to the play store, you demand to upload your expansion files after uploading the apk. Currently Google Play doesn't allow submitting an expansion file with the beginning versino of your app. When submitting a new app with expansion files, you must submit an app with build version 1 without an expansion file and and then another one with build version 2 along with the expansion file.
Source: https://medium.com/successivetech/how-to-set-up-android-app-to-support-expansion-files-6dff9e535d76
0 Response to "Using Apk Expansion Files to Upload Large Apk"
Post a Comment