diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 04ad5e943..39cd25d89 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -42,8 +42,7 @@ - + diff --git a/app/src/main/java/org/fdroid/fdroid/installer/InstallManagerService.java b/app/src/main/java/org/fdroid/fdroid/installer/InstallManagerService.java index 63e25b9e3..70b40c8e3 100644 --- a/app/src/main/java/org/fdroid/fdroid/installer/InstallManagerService.java +++ b/app/src/main/java/org/fdroid/fdroid/installer/InstallManagerService.java @@ -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; *

* The implementations of {@link Uri#toString()} and {@link Intent#getDataString()} both * include caching of the generated {@code String}, so it should be plenty fast. + *

+ * 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 APK Expansion Files */ 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 APK Expansion Files + */ + 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 diff --git a/app/src/main/java/org/fdroid/fdroid/installer/InstallerService.java b/app/src/main/java/org/fdroid/fdroid/installer/InstallerService.java index 3784c788c..da2736b05 100644 --- a/app/src/main/java/org/fdroid/fdroid/installer/InstallerService.java +++ b/app/src/main/java/org/fdroid/fdroid/installer/InstallerService.java @@ -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. - *

+ *

* 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 - *

+ *

* The download URL is only used as the unique ID that represents this * particular apk throughout the whole install process in * {@link InstallManagerService}. + *

+ * This also handles deleting any associated OBB files when an app is + * uninstalled, as per the + * + * APK Expansion Files 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(); } }