auto-download and -install any associated OBB files

This implements the APK Extension Files spec for finding, downloading, and
installing OBB files that are extension packs for APKs.

This needs WRITE_EXTERNAL_STORAGE since "installing" OBB files is just
copying them to a specific path on the external storage.

https://developer.android.com/google/play/expansion-files.html
This commit is contained in:
Hans-Christoph Steiner 2016-10-06 17:43:43 +02:00
parent 4c4aef5314
commit 8affa08d11
3 changed files with 117 additions and 7 deletions

View File

@ -42,8 +42,7 @@
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="18" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_SETTINGS" />
<uses-permission android:name="android.permission.NFC" />

View File

@ -17,7 +17,10 @@ import android.support.v4.app.TaskStackBuilder;
import android.support.v4.content.LocalBroadcastManager;
import android.text.TextUtils;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.filefilter.WildcardFileFilter;
import org.fdroid.fdroid.AppDetails;
import org.fdroid.fdroid.Hasher;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.compat.PackageManagerCompat;
@ -29,6 +32,8 @@ import org.fdroid.fdroid.net.Downloader;
import org.fdroid.fdroid.net.DownloaderService;
import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
@ -61,6 +66,12 @@ import java.util.Set;
* </ul></p>
* The implementations of {@link Uri#toString()} and {@link Intent#getDataString()} both
* include caching of the generated {@code String}, so it should be plenty fast.
* <p>
* This also handles downloading OBB "APK Extension" files for any APK that has one
* assigned to it. OBB files are queued up for download before the APK so that they
* are hopefully in place before the APK starts. That is not guaranteed though.
*
* @see <a href="https://developer.android.com/google/play/expansion-files.html">APK Expansion Files</a>
*/
public class InstallManagerService extends Service {
private static final String TAG = "InstallManagerService";
@ -160,7 +171,9 @@ public class InstallManagerService extends Service {
NotificationCompat.Builder builder = createNotificationBuilder(urlString, apk);
notificationManager.notify(urlString.hashCode(), builder.build());
registerDownloaderReceivers(urlString, builder);
registerApkDownloaderReceivers(urlString, builder);
getObb(urlString, apk.getMainObbUrl(), apk.getMainObbFile(), apk.obbMainFileSha256, builder);
getObb(urlString, apk.getPatchObbUrl(), apk.getPatchObbFile(), apk.obbPatchFileSha256, builder);
File apkFilePath = ApkCache.getApkDownloadPath(this, intent.getData());
long apkFileSize = apkFilePath.length();
@ -186,7 +199,72 @@ public class InstallManagerService extends Service {
localBroadcastManager.sendBroadcast(intent);
}
private void registerDownloaderReceivers(String urlString, final NotificationCompat.Builder builder) {
/**
* Check if any OBB files are available, and if so, download and install them. This
* also deletes any obsolete OBB files, per the spec, since there can be only one
* "main" and one "patch" OBB installed at a time.
*
* @see <a href="https://developer.android.com/google/play/expansion-files.html">APK Expansion Files</a>
*/
private void getObb(final String urlString, String obbUrlString,
final File obbDestFile, final String sha256,
final NotificationCompat.Builder builder) {
if (obbDestFile == null || obbDestFile.exists() || TextUtils.isEmpty(obbUrlString)) {
return;
}
final BroadcastReceiver downloadReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (Downloader.ACTION_STARTED.equals(action)) {
Utils.debugLog(TAG, action + " " + intent);
} else if (Downloader.ACTION_PROGRESS.equals(action)) {
int bytesRead = intent.getIntExtra(Downloader.EXTRA_BYTES_READ, 0);
int totalBytes = intent.getIntExtra(Downloader.EXTRA_TOTAL_BYTES, 0);
builder.setProgress(totalBytes, bytesRead, false);
notificationManager.notify(urlString.hashCode(), builder.build());
} else if (Downloader.ACTION_COMPLETE.equals(action)) {
localBroadcastManager.unregisterReceiver(this);
File localFile = new File(intent.getStringExtra(Downloader.EXTRA_DOWNLOAD_PATH));
Uri localApkUri = Uri.fromFile(localFile);
Utils.debugLog(TAG, "OBB download completed " + intent.getDataString()
+ " to " + localApkUri);
try {
if (Hasher.isFileMatchingHash(localFile, sha256, "SHA-256")) {
Utils.debugLog(TAG, "Installing OBB " + localFile + " to " + obbDestFile);
FileUtils.forceMkdirParent(obbDestFile);
FileUtils.copyFile(localFile, obbDestFile);
FileFilter filter = new WildcardFileFilter(
obbDestFile.getName().substring(0, 4) + "*.obb");
for (File f : obbDestFile.getParentFile().listFiles(filter)) {
if (!f.equals(obbDestFile)) {
Utils.debugLog(TAG, "Deleting obsolete OBB " + f);
FileUtils.deleteQuietly(f);
}
}
} else {
Utils.debugLog(TAG, localFile + " deleted, did not match hash: " + sha256);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
FileUtils.deleteQuietly(localFile);
}
} else if (Downloader.ACTION_INTERRUPTED.equals(action)) {
localBroadcastManager.unregisterReceiver(this);
} else {
throw new RuntimeException("intent action not handled!");
}
}
};
DownloaderService.queue(this, obbUrlString);
localBroadcastManager.registerReceiver(downloadReceiver,
DownloaderService.getIntentFilter(obbUrlString));
}
private void registerApkDownloaderReceivers(String urlString, final NotificationCompat.Builder builder) {
BroadcastReceiver downloadReceiver = new BroadcastReceiver() {
@Override

View File

@ -25,22 +25,32 @@ import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.filefilter.WildcardFileFilter;
import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.data.Apk;
import java.io.File;
import java.io.FileFilter;
/**
* This service handles the install process of apk files and
* uninstall process of apps.
* <p/>
* <p>
* This service is based on an IntentService because:
* - no parallel installs/uninstalls should be allowed,
* i.e., runs sequentially
* - no cancel operation is needed. Cancelling an installation
* would be the same as starting uninstall afterwards
* <p/>
* <p>
* The download URL is only used as the unique ID that represents this
* particular apk throughout the whole install process in
* {@link InstallManagerService}.
* <p>
* This also handles deleting any associated OBB files when an app is
* uninstalled, as per the
* <a href="https://developer.android.com/google/play/expansion-files.html">
* APK Expansion Files</a> spec.
*/
public class InstallerService extends IntentService {
public static final String TAG = "InstallerService";
@ -54,7 +64,7 @@ public class InstallerService extends IntentService {
@Override
protected void onHandleIntent(Intent intent) {
Apk apk = intent.getParcelableExtra(Installer.EXTRA_APK);
final Apk apk = intent.getParcelableExtra(Installer.EXTRA_APK);
if (apk == null) {
Utils.debugLog(TAG, "ignoring intent with null EXTRA_APK: " + intent);
return;
@ -67,6 +77,29 @@ public class InstallerService extends IntentService {
installer.installPackage(uri, downloadUri);
} else if (ACTION_UNINSTALL.equals(intent.getAction())) {
installer.uninstallPackage();
new Thread() {
@Override
public void run() {
setPriority(MIN_PRIORITY);
File mainObbFile = apk.getMainObbFile();
if (mainObbFile == null) {
return;
}
File obbDir = mainObbFile.getParentFile();
if (obbDir == null) {
return;
}
FileFilter filter = new WildcardFileFilter("*.obb");
File[] obbFiles = obbDir.listFiles(filter);
if (obbFiles == null) {
return;
}
for (File f : obbFiles) {
Utils.debugLog(TAG, "Uninstalling OBB " + f);
FileUtils.deleteQuietly(f);
}
}
}.start();
}
}