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:
parent
4c4aef5314
commit
8affa08d11
@ -42,8 +42,7 @@
|
|||||||
<uses-permission android:name="android.permission.BLUETOOTH" />
|
<uses-permission android:name="android.permission.BLUETOOTH" />
|
||||||
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
|
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
|
||||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||||
android:maxSdkVersion="18" />
|
|
||||||
<uses-permission android:name="android.permission.WRITE_SETTINGS" />
|
<uses-permission android:name="android.permission.WRITE_SETTINGS" />
|
||||||
<uses-permission android:name="android.permission.NFC" />
|
<uses-permission android:name="android.permission.NFC" />
|
||||||
|
|
||||||
|
@ -17,7 +17,10 @@ import android.support.v4.app.TaskStackBuilder;
|
|||||||
import android.support.v4.content.LocalBroadcastManager;
|
import android.support.v4.content.LocalBroadcastManager;
|
||||||
import android.text.TextUtils;
|
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.AppDetails;
|
||||||
|
import org.fdroid.fdroid.Hasher;
|
||||||
import org.fdroid.fdroid.R;
|
import org.fdroid.fdroid.R;
|
||||||
import org.fdroid.fdroid.Utils;
|
import org.fdroid.fdroid.Utils;
|
||||||
import org.fdroid.fdroid.compat.PackageManagerCompat;
|
import org.fdroid.fdroid.compat.PackageManagerCompat;
|
||||||
@ -29,6 +32,8 @@ import org.fdroid.fdroid.net.Downloader;
|
|||||||
import org.fdroid.fdroid.net.DownloaderService;
|
import org.fdroid.fdroid.net.DownloaderService;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
import java.io.FileFilter;
|
||||||
|
import java.io.IOException;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
@ -61,6 +66,12 @@ import java.util.Set;
|
|||||||
* </ul></p>
|
* </ul></p>
|
||||||
* The implementations of {@link Uri#toString()} and {@link Intent#getDataString()} both
|
* The implementations of {@link Uri#toString()} and {@link Intent#getDataString()} both
|
||||||
* include caching of the generated {@code String}, so it should be plenty fast.
|
* 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 {
|
public class InstallManagerService extends Service {
|
||||||
private static final String TAG = "InstallManagerService";
|
private static final String TAG = "InstallManagerService";
|
||||||
@ -160,7 +171,9 @@ public class InstallManagerService extends Service {
|
|||||||
NotificationCompat.Builder builder = createNotificationBuilder(urlString, apk);
|
NotificationCompat.Builder builder = createNotificationBuilder(urlString, apk);
|
||||||
notificationManager.notify(urlString.hashCode(), builder.build());
|
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());
|
File apkFilePath = ApkCache.getApkDownloadPath(this, intent.getData());
|
||||||
long apkFileSize = apkFilePath.length();
|
long apkFileSize = apkFilePath.length();
|
||||||
@ -186,7 +199,72 @@ public class InstallManagerService extends Service {
|
|||||||
localBroadcastManager.sendBroadcast(intent);
|
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() {
|
BroadcastReceiver downloadReceiver = new BroadcastReceiver() {
|
||||||
@Override
|
@Override
|
||||||
|
@ -25,22 +25,32 @@ import android.content.Context;
|
|||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.net.Uri;
|
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.Utils;
|
||||||
import org.fdroid.fdroid.data.Apk;
|
import org.fdroid.fdroid.data.Apk;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileFilter;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This service handles the install process of apk files and
|
* This service handles the install process of apk files and
|
||||||
* uninstall process of apps.
|
* uninstall process of apps.
|
||||||
* <p/>
|
* <p>
|
||||||
* This service is based on an IntentService because:
|
* This service is based on an IntentService because:
|
||||||
* - no parallel installs/uninstalls should be allowed,
|
* - no parallel installs/uninstalls should be allowed,
|
||||||
* i.e., runs sequentially
|
* i.e., runs sequentially
|
||||||
* - no cancel operation is needed. Cancelling an installation
|
* - no cancel operation is needed. Cancelling an installation
|
||||||
* would be the same as starting uninstall afterwards
|
* would be the same as starting uninstall afterwards
|
||||||
* <p/>
|
* <p>
|
||||||
* The download URL is only used as the unique ID that represents this
|
* The download URL is only used as the unique ID that represents this
|
||||||
* particular apk throughout the whole install process in
|
* particular apk throughout the whole install process in
|
||||||
* {@link InstallManagerService}.
|
* {@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 class InstallerService extends IntentService {
|
||||||
public static final String TAG = "InstallerService";
|
public static final String TAG = "InstallerService";
|
||||||
@ -54,7 +64,7 @@ public class InstallerService extends IntentService {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onHandleIntent(Intent intent) {
|
protected void onHandleIntent(Intent intent) {
|
||||||
Apk apk = intent.getParcelableExtra(Installer.EXTRA_APK);
|
final Apk apk = intent.getParcelableExtra(Installer.EXTRA_APK);
|
||||||
if (apk == null) {
|
if (apk == null) {
|
||||||
Utils.debugLog(TAG, "ignoring intent with null EXTRA_APK: " + intent);
|
Utils.debugLog(TAG, "ignoring intent with null EXTRA_APK: " + intent);
|
||||||
return;
|
return;
|
||||||
@ -67,6 +77,29 @@ public class InstallerService extends IntentService {
|
|||||||
installer.installPackage(uri, downloadUri);
|
installer.installPackage(uri, downloadUri);
|
||||||
} else if (ACTION_UNINSTALL.equals(intent.getAction())) {
|
} else if (ACTION_UNINSTALL.equals(intent.getAction())) {
|
||||||
installer.uninstallPackage();
|
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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user