From cd9582c9902dd4ac9218acfd69872f3eebcd3d93 Mon Sep 17 00:00:00 2001
From: Hans-Christoph Steiner
Date: Tue, 28 Jun 2016 00:07:50 +0200
Subject: [PATCH 1/9] support "APK Extension" files aka .obb for large apps and
games
OBB files are used in apps that need more than 100 megs to work well. This
is apps like MAPS.ME or games that put map info, media, etc. into the OBB
file. Also, OBB files provide a mechanism to deliver large data blobs that
do not need to be part of the APK. For example, a game's assets do not
need to change often, so they can be shipped as an OBB, then APK updates do
not need to include all those assets for each update.
https://developer.android.com/google/play/expansion-files.html
---
.../org/fdroid/fdroid/RepoXMLHandler.java | 13 +++
.../main/java/org/fdroid/fdroid/data/Apk.java | 94 ++++++++++++++++++-
.../main/java/org/fdroid/fdroid/data/App.java | 43 ++++++++-
.../java/org/fdroid/fdroid/data/DBHelper.java | 32 ++++++-
.../java/org/fdroid/fdroid/data/Schema.java | 6 ++
.../fdroid/updater/RepoXMLHandlerTest.java | 38 ++++++++
.../main.1101613.obb.main.twoversions.obb | 1 +
.../main.1101615.obb.main.twoversions.obb | 1 +
.../main.1434483388.obb.main.oldversion.obb | 1 +
.../main.1619.obb.mainpatch.current.obb | 1 +
app/src/test/resources/obbIndex.xml | 1 +
.../patch.1619.obb.mainpatch.current.obb | 1 +
12 files changed, 227 insertions(+), 5 deletions(-)
create mode 100644 app/src/test/resources/main.1101613.obb.main.twoversions.obb
create mode 100644 app/src/test/resources/main.1101615.obb.main.twoversions.obb
create mode 100644 app/src/test/resources/main.1434483388.obb.main.oldversion.obb
create mode 100644 app/src/test/resources/main.1619.obb.mainpatch.current.obb
create mode 100644 app/src/test/resources/obbIndex.xml
create mode 100644 app/src/test/resources/patch.1619.obb.mainpatch.current.obb
diff --git a/app/src/main/java/org/fdroid/fdroid/RepoXMLHandler.java b/app/src/main/java/org/fdroid/fdroid/RepoXMLHandler.java
index 984b99de7..bb7d53580 100644
--- a/app/src/main/java/org/fdroid/fdroid/RepoXMLHandler.java
+++ b/app/src/main/java/org/fdroid/fdroid/RepoXMLHandler.java
@@ -26,6 +26,7 @@ import org.fdroid.fdroid.data.Apk;
import org.fdroid.fdroid.data.App;
import org.fdroid.fdroid.data.Repo;
import org.fdroid.fdroid.data.RepoPushRequest;
+import org.fdroid.fdroid.data.Schema.ApkTable;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;
@@ -141,6 +142,18 @@ public class RepoXMLHandler extends DefaultHandler {
curapk.maxSdkVersion = Apk.SDK_VERSION_MAX_VALUE;
}
break;
+ case ApkTable.Cols.OBB_MAIN_FILE:
+ curapk.obbMainFile = str;
+ break;
+ case ApkTable.Cols.OBB_MAIN_FILE_SHA256:
+ curapk.obbMainFileSha256 = str;
+ break;
+ case ApkTable.Cols.OBB_PATCH_FILE:
+ curapk.obbPatchFile = str;
+ break;
+ case ApkTable.Cols.OBB_PATCH_FILE_SHA256:
+ curapk.obbPatchFileSha256 = str;
+ break;
case "added":
curapk.added = Utils.parseDate(str, null);
break;
diff --git a/app/src/main/java/org/fdroid/fdroid/data/Apk.java b/app/src/main/java/org/fdroid/fdroid/data/Apk.java
index 5580196ca..592644699 100644
--- a/app/src/main/java/org/fdroid/fdroid/data/Apk.java
+++ b/app/src/main/java/org/fdroid/fdroid/data/Apk.java
@@ -10,6 +10,7 @@ import android.os.Parcelable;
import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.data.Schema.ApkTable.Cols;
+import java.io.File;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashSet;
@@ -30,6 +31,10 @@ public class Apk extends ValueObject implements Comparable, Parcelable {
public int minSdkVersion = SDK_VERSION_MIN_VALUE; // 0 if unknown
public int targetSdkVersion = SDK_VERSION_MIN_VALUE; // 0 if unknown
public int maxSdkVersion = SDK_VERSION_MAX_VALUE; // "infinity" if not set
+ public String obbMainFile;
+ public String obbMainFileSha256;
+ public String obbPatchFile;
+ public String obbPatchFileSha256;
public Date added;
public String[] permissions; // null if empty or
// unknown
@@ -106,6 +111,18 @@ public class Apk extends ValueObject implements Comparable, Parcelable {
case Cols.MAX_SDK_VERSION:
maxSdkVersion = cursor.getInt(i);
break;
+ case Cols.OBB_MAIN_FILE:
+ obbMainFile = cursor.getString(i);
+ break;
+ case Cols.OBB_MAIN_FILE_SHA256:
+ obbMainFileSha256 = cursor.getString(i);
+ break;
+ case Cols.OBB_PATCH_FILE:
+ obbPatchFile = cursor.getString(i);
+ break;
+ case Cols.OBB_PATCH_FILE_SHA256:
+ obbPatchFileSha256 = cursor.getString(i);
+ break;
case Cols.NAME:
apkName = cursor.getString(i);
break;
@@ -146,13 +163,73 @@ public class Apk extends ValueObject implements Comparable, Parcelable {
}
}
- public String getUrl() {
+ private void checkRepoAddress() {
if (repoAddress == null || apkName == null) {
throw new IllegalStateException("Apk needs to have both Schema.ApkTable.Cols.REPO_ADDRESS and Schema.ApkTable.Cols.NAME set in order to calculate URL.");
}
+ }
+
+ public String getUrl() {
+ checkRepoAddress();
return repoAddress + "/" + apkName.replace(" ", "%20");
}
+ /**
+ * Get the URL to download the main expansion file, the primary
+ * expansion file for additional resources required by your application.
+ * The filename will always have the format:
+ * "main.versionCode.packageName.obb"
+ *
+ * @return a URL to download the OBB file that matches this APK
+ * @see #getPatchObbUrl()
+ * @see APK Expansion Files
+ */
+ public String getMainObbUrl() {
+ if (repoAddress == null || obbMainFile == null) {
+ return null;
+ }
+ checkRepoAddress();
+ return repoAddress + "/" + obbMainFile;
+ }
+
+ /**
+ * Get the URL to download the optional patch expansion file, which
+ * is intended for small updates to the main expansion file.
+ * The filename will always have the format:
+ * "patch.versionCode.packageName.obb"
+ *
+ * @return a URL to download the OBB file that matches this APK
+ * @see #getMainObbUrl()
+ * @see APK Expansion Files
+ */
+ public String getPatchObbUrl() {
+ if (repoAddress == null || obbPatchFile == null) {
+ return null;
+ }
+ checkRepoAddress();
+ return repoAddress + "/" + obbPatchFile;
+ }
+
+ /**
+ * Get the local {@link File} to the "main" OBB file.
+ */
+ public File getMainObbFile() {
+ if (obbMainFile == null) {
+ return null;
+ }
+ return new File(App.getObbDir(packageName), obbMainFile);
+ }
+
+ /**
+ * Get the local {@link File} to the "patch" OBB file.
+ */
+ public File getPatchObbFile() {
+ if (obbPatchFile == null) {
+ return null;
+ }
+ return new File(App.getObbDir(packageName), obbPatchFile);
+ }
+
public ArrayList getFullPermissionList() {
if (this.permissions == null) {
return new ArrayList<>();
@@ -180,7 +257,8 @@ public class Apk extends ValueObject implements Comparable, Parcelable {
* FDroid just includes the constant name in the apk list, so we prefix it
* with "android.permission."
*
- * see https://gitlab.com/fdroid/fdroidserver/blob/master/fdroidserver/update.py#L535#
+ * @see
+ * More info into index - size, permissions, features, sdk version
*/
private static String fdroidToAndroidPermission(String permission) {
if (!permission.contains(".")) {
@@ -210,6 +288,10 @@ public class Apk extends ValueObject implements Comparable, Parcelable {
values.put(Cols.MIN_SDK_VERSION, minSdkVersion);
values.put(Cols.TARGET_SDK_VERSION, targetSdkVersion);
values.put(Cols.MAX_SDK_VERSION, maxSdkVersion);
+ values.put(Cols.OBB_MAIN_FILE, obbMainFile);
+ values.put(Cols.OBB_MAIN_FILE_SHA256, obbMainFileSha256);
+ values.put(Cols.OBB_PATCH_FILE, obbPatchFile);
+ values.put(Cols.OBB_PATCH_FILE_SHA256, obbPatchFileSha256);
values.put(Cols.ADDED_DATE, Utils.formatDate(added, ""));
values.put(Cols.PERMISSIONS, Utils.serializeCommaSeparatedString(permissions));
values.put(Cols.FEATURES, Utils.serializeCommaSeparatedString(features));
@@ -245,6 +327,10 @@ public class Apk extends ValueObject implements Comparable, Parcelable {
dest.writeInt(this.minSdkVersion);
dest.writeInt(this.targetSdkVersion);
dest.writeInt(this.maxSdkVersion);
+ dest.writeString(this.obbMainFile);
+ dest.writeString(this.obbMainFileSha256);
+ dest.writeString(this.obbPatchFile);
+ dest.writeString(this.obbPatchFileSha256);
dest.writeLong(this.added != null ? this.added.getTime() : -1);
dest.writeStringArray(this.permissions);
dest.writeStringArray(this.features);
@@ -271,6 +357,10 @@ public class Apk extends ValueObject implements Comparable, Parcelable {
this.minSdkVersion = in.readInt();
this.targetSdkVersion = in.readInt();
this.maxSdkVersion = in.readInt();
+ this.obbMainFile = in.readString();
+ this.obbMainFileSha256 = in.readString();
+ this.obbPatchFile = in.readString();
+ this.obbPatchFileSha256 = in.readString();
long tmpAdded = in.readLong();
this.added = tmpAdded == -1 ? null : new Date(tmpAdded);
this.permissions = in.createStringArray();
diff --git a/app/src/main/java/org/fdroid/fdroid/data/App.java b/app/src/main/java/org/fdroid/fdroid/data/App.java
index 611072bf9..e7d96ba1d 100644
--- a/app/src/main/java/org/fdroid/fdroid/data/App.java
+++ b/app/src/main/java/org/fdroid/fdroid/data/App.java
@@ -9,22 +9,27 @@ import android.content.pm.PackageManager;
import android.content.res.AssetManager;
import android.content.res.XmlResourceParser;
import android.database.Cursor;
+import android.os.Environment;
import android.os.Parcel;
import android.os.Parcelable;
import android.text.TextUtils;
import android.util.Log;
+import org.apache.commons.io.filefilter.RegexFileFilter;
import org.fdroid.fdroid.AppFilter;
import org.fdroid.fdroid.FDroidApp;
import org.fdroid.fdroid.Utils;
+import org.fdroid.fdroid.data.Schema.AppMetadataTable.Cols;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import java.io.File;
+import java.io.FileFilter;
import java.io.IOException;
import java.io.InputStream;
import java.security.cert.Certificate;
import java.security.cert.CertificateEncodingException;
+import java.util.Arrays;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashSet;
@@ -33,8 +38,6 @@ import java.util.jar.JarFile;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
-import org.fdroid.fdroid.data.Schema.AppMetadataTable.Cols;
-
public class App extends ValueObject implements Comparable, Parcelable {
private static final String TAG = "App";
@@ -272,6 +275,17 @@ public class App extends ValueObject implements Comparable, Parcelable {
initApkFromApkFile(context, this.installedApk, packageInfo, apkFile);
}
+ /**
+ * Get the directory where APK Expansion Files aka OBB files are stored for the app as
+ * specified by {@code packageName}.
+ *
+ * @see APK Expansion Files
+ */
+ public static File getObbDir(String packageName) {
+ return new File(Environment.getExternalStorageDirectory().getAbsolutePath()
+ + "/Android/obb/" + packageName);
+ }
+
private void setFromPackageInfo(PackageManager pm, PackageInfo packageInfo) {
this.packageName = packageInfo.packageName;
@@ -324,6 +338,29 @@ public class App extends ValueObject implements Comparable, Parcelable {
initInstalledApk(context, apk, packageInfo, apkFile);
}
+ public static void initInstalledObbFiles(Apk apk) {
+ File obbdir = getObbDir(apk.packageName);
+ FileFilter filter = new RegexFileFilter("(main|patch)\\.[0-9-][0-9]*\\." + apk.packageName + "\\.obb");
+ File[] files = obbdir.listFiles(filter);
+ if (files == null) {
+ return;
+ }
+ Arrays.sort(files);
+ for (File f : files) {
+ String filename = f.getName();
+ String[] segments = filename.split("\\.");
+ if (Integer.parseInt(segments[1]) <= apk.versionCode) {
+ if ("main".equals(segments[0])) {
+ apk.obbMainFile = filename;
+ apk.obbMainFileSha256 = Utils.getBinaryHash(f, apk.hashType);
+ } else if ("patch".equals(segments[0])) {
+ apk.obbPatchFile = filename;
+ apk.obbPatchFileSha256 = Utils.getBinaryHash(f, apk.hashType);
+ }
+ }
+ }
+ }
+
private void initInstalledApk(Context context, Apk apk, PackageInfo packageInfo, SanitizedFile apkFile)
throws IOException, CertificateEncodingException {
apk.compatible = true;
@@ -339,6 +376,8 @@ public class App extends ValueObject implements Comparable, Parcelable {
apk.apkName = apk.packageName + "_" + apk.versionCode + ".apk";
apk.installedFile = apkFile;
+ initInstalledObbFiles(apk);
+
JarFile apkJar = new JarFile(apkFile);
HashSet abis = new HashSet<>(3);
Pattern pattern = Pattern.compile("^lib/([a-z0-9-]+)/.*");
diff --git a/app/src/main/java/org/fdroid/fdroid/data/DBHelper.java b/app/src/main/java/org/fdroid/fdroid/data/DBHelper.java
index 9c238295d..51270d448 100644
--- a/app/src/main/java/org/fdroid/fdroid/data/DBHelper.java
+++ b/app/src/main/java/org/fdroid/fdroid/data/DBHelper.java
@@ -92,6 +92,10 @@ class DBHelper extends SQLiteOpenHelper {
+ ApkTable.Cols.MIN_SDK_VERSION + " integer, "
+ ApkTable.Cols.TARGET_SDK_VERSION + " integer, "
+ ApkTable.Cols.MAX_SDK_VERSION + " integer, "
+ + ApkTable.Cols.OBB_MAIN_FILE + " string, "
+ + ApkTable.Cols.OBB_MAIN_FILE_SHA256 + " string, "
+ + ApkTable.Cols.OBB_PATCH_FILE + " string, "
+ + ApkTable.Cols.OBB_PATCH_FILE_SHA256 + " string, "
+ ApkTable.Cols.PERMISSIONS + " string, "
+ ApkTable.Cols.FEATURES + " string, "
+ ApkTable.Cols.NATIVE_CODE + " string, "
@@ -154,7 +158,7 @@ class DBHelper extends SQLiteOpenHelper {
+ " );";
private static final String DROP_TABLE_INSTALLED_APP = "DROP TABLE " + InstalledAppTable.NAME + ";";
- private static final int DB_VERSION = 63;
+ private static final int DB_VERSION = 64;
private final Context context;
@@ -354,6 +358,24 @@ class DBHelper extends SQLiteOpenHelper {
lowerCaseApkHashes(db, oldVersion);
supportRepoPushRequests(db, oldVersion);
migrateToPackageTable(db, oldVersion);
+ addObbFiles(db, oldVersion);
+ }
+
+ private void addObbFiles(SQLiteDatabase db, int oldVersion) {
+ if (oldVersion >= 64) {
+ return;
+ }
+ Utils.debugLog(TAG, "Adding " + ApkTable.Cols.OBB_MAIN_FILE
+ + ", " + ApkTable.Cols.OBB_PATCH_FILE
+ + ", and hash columns to " + ApkTable.NAME);
+ db.execSQL("alter table " + ApkTable.NAME + " add column "
+ + ApkTable.Cols.OBB_MAIN_FILE + " string");
+ db.execSQL("alter table " + ApkTable.NAME + " add column "
+ + ApkTable.Cols.OBB_MAIN_FILE_SHA256 + " string");
+ db.execSQL("alter table " + ApkTable.NAME + " add column "
+ + ApkTable.Cols.OBB_PATCH_FILE + " string");
+ db.execSQL("alter table " + ApkTable.NAME + " add column "
+ + ApkTable.Cols.OBB_PATCH_FILE_SHA256 + " string");
}
private void migrateToPackageTable(SQLiteDatabase db, int oldVersion) {
@@ -477,6 +499,10 @@ class DBHelper extends SQLiteOpenHelper {
+ ApkTable.Cols.MIN_SDK_VERSION + " integer, "
+ ApkTable.Cols.TARGET_SDK_VERSION + " integer, "
+ ApkTable.Cols.MAX_SDK_VERSION + " integer, "
+ + ApkTable.Cols.OBB_MAIN_FILE + " string, "
+ + ApkTable.Cols.OBB_MAIN_FILE_SHA256 + " string, "
+ + ApkTable.Cols.OBB_PATCH_FILE + " string, "
+ + ApkTable.Cols.OBB_PATCH_FILE_SHA256 + " string, "
+ ApkTable.Cols.PERMISSIONS + " string, "
+ ApkTable.Cols.FEATURES + " string, "
+ ApkTable.Cols.NATIVE_CODE + " string, "
@@ -502,6 +528,10 @@ class DBHelper extends SQLiteOpenHelper {
ApkTable.Cols.MIN_SDK_VERSION,
ApkTable.Cols.TARGET_SDK_VERSION,
ApkTable.Cols.MAX_SDK_VERSION,
+ ApkTable.Cols.OBB_MAIN_FILE,
+ ApkTable.Cols.OBB_MAIN_FILE_SHA256,
+ ApkTable.Cols.OBB_PATCH_FILE,
+ ApkTable.Cols.OBB_PATCH_FILE_SHA256,
ApkTable.Cols.PERMISSIONS,
ApkTable.Cols.FEATURES,
ApkTable.Cols.NATIVE_CODE,
diff --git a/app/src/main/java/org/fdroid/fdroid/data/Schema.java b/app/src/main/java/org/fdroid/fdroid/data/Schema.java
index f14cb1484..b910c107f 100644
--- a/app/src/main/java/org/fdroid/fdroid/data/Schema.java
+++ b/app/src/main/java/org/fdroid/fdroid/data/Schema.java
@@ -165,6 +165,10 @@ public interface Schema {
String MIN_SDK_VERSION = "minSdkVersion";
String TARGET_SDK_VERSION = "targetSdkVersion";
String MAX_SDK_VERSION = "maxSdkVersion";
+ String OBB_MAIN_FILE = "obbMainFile";
+ String OBB_MAIN_FILE_SHA256 = "obbMainFileSha256";
+ String OBB_PATCH_FILE = "obbPatchFile";
+ String OBB_PATCH_FILE_SHA256 = "obbPatchFileSha256";
String PERMISSIONS = "permissions";
String FEATURES = "features";
String NATIVE_CODE = "nativecode";
@@ -188,6 +192,7 @@ public interface Schema {
String[] ALL_COLS = {
APP_ID, VERSION_NAME, REPO_ID, HASH, VERSION_CODE, NAME,
SIZE, SIGNATURE, SOURCE_NAME, MIN_SDK_VERSION, TARGET_SDK_VERSION, MAX_SDK_VERSION,
+ OBB_MAIN_FILE, OBB_MAIN_FILE_SHA256, OBB_PATCH_FILE, OBB_PATCH_FILE_SHA256,
PERMISSIONS, FEATURES, NATIVE_CODE, HASH_TYPE, ADDED_DATE,
IS_COMPATIBLE, INCOMPATIBLE_REASONS,
};
@@ -198,6 +203,7 @@ public interface Schema {
String[] ALL = {
_ID, APP_ID, Package.PACKAGE_NAME, VERSION_NAME, REPO_ID, HASH, VERSION_CODE, NAME,
SIZE, SIGNATURE, SOURCE_NAME, MIN_SDK_VERSION, TARGET_SDK_VERSION, MAX_SDK_VERSION,
+ OBB_MAIN_FILE, OBB_MAIN_FILE_SHA256, OBB_PATCH_FILE, OBB_PATCH_FILE_SHA256,
PERMISSIONS, FEATURES, NATIVE_CODE, HASH_TYPE, ADDED_DATE,
IS_COMPATIBLE, Repo.VERSION, Repo.ADDRESS, INCOMPATIBLE_REASONS,
};
diff --git a/app/src/test/java/org/fdroid/fdroid/updater/RepoXMLHandlerTest.java b/app/src/test/java/org/fdroid/fdroid/updater/RepoXMLHandlerTest.java
index 12751a40a..193c8dc23 100644
--- a/app/src/test/java/org/fdroid/fdroid/updater/RepoXMLHandlerTest.java
+++ b/app/src/test/java/org/fdroid/fdroid/updater/RepoXMLHandlerTest.java
@@ -26,6 +26,7 @@ import android.support.annotation.NonNull;
import android.text.TextUtils;
import android.util.Log;
+import org.apache.commons.io.FileUtils;
import org.fdroid.fdroid.BuildConfig;
import org.fdroid.fdroid.RepoXMLHandler;
import org.fdroid.fdroid.data.Apk;
@@ -33,15 +34,18 @@ import org.fdroid.fdroid.data.App;
import org.fdroid.fdroid.data.Repo;
import org.fdroid.fdroid.data.RepoPushRequest;
import org.fdroid.fdroid.mock.MockRepo;
+import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricGradleTestRunner;
import org.robolectric.annotation.Config;
+import org.robolectric.shadows.ShadowLog;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import java.io.BufferedInputStream;
+import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
@@ -70,6 +74,32 @@ public class RepoXMLHandlerTest {
private static final String FAKE_SIGNING_CERT = "012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345";
+ @Before
+ public void setUp() {
+ ShadowLog.stream = System.out;
+ }
+
+ @Test
+ public void testObbIndex() throws IOException {
+ writeResourceToObbDir("main.1101613.obb.main.twoversions.obb");
+ writeResourceToObbDir("main.1101615.obb.main.twoversions.obb");
+ writeResourceToObbDir("main.1434483388.obb.main.oldversion.obb");
+ writeResourceToObbDir("main.1619.obb.mainpatch.current.obb");
+ writeResourceToObbDir("patch.1619.obb.mainpatch.current.obb");
+ RepoDetails actualDetails = getFromFile("obbIndex.xml");
+ for (Apk indexApk : actualDetails.apks) {
+ Apk localApk = new Apk();
+ localApk.packageName = indexApk.packageName;
+ localApk.versionCode = indexApk.versionCode;
+ localApk.hashType = indexApk.hashType;
+ App.initInstalledObbFiles(localApk);
+ assertEquals(indexApk.obbMainFile, localApk.obbMainFile);
+ assertEquals(indexApk.obbMainFileSha256, localApk.obbMainFileSha256);
+ assertEquals(indexApk.obbPatchFile, localApk.obbPatchFile);
+ assertEquals(indexApk.obbPatchFileSha256, localApk.obbPatchFileSha256);
+ }
+ }
+
@Test
public void testSimpleIndex() {
Repo expectedRepo = new Repo();
@@ -871,4 +901,12 @@ public class RepoXMLHandlerTest {
}
}
+ private void writeResourceToObbDir(String assetName) throws IOException {
+ InputStream input = getClass().getClassLoader().getResourceAsStream(assetName);
+ String packageName = assetName.substring(assetName.indexOf("obb"),
+ assetName.lastIndexOf('.'));
+ File f = new File(App.getObbDir(packageName), assetName);
+ FileUtils.copyToFile(input, f);
+ input.close();
+ }
}
diff --git a/app/src/test/resources/main.1101613.obb.main.twoversions.obb b/app/src/test/resources/main.1101613.obb.main.twoversions.obb
new file mode 100644
index 000000000..421376db9
--- /dev/null
+++ b/app/src/test/resources/main.1101613.obb.main.twoversions.obb
@@ -0,0 +1 @@
+dummy
diff --git a/app/src/test/resources/main.1101615.obb.main.twoversions.obb b/app/src/test/resources/main.1101615.obb.main.twoversions.obb
new file mode 100644
index 000000000..421376db9
--- /dev/null
+++ b/app/src/test/resources/main.1101615.obb.main.twoversions.obb
@@ -0,0 +1 @@
+dummy
diff --git a/app/src/test/resources/main.1434483388.obb.main.oldversion.obb b/app/src/test/resources/main.1434483388.obb.main.oldversion.obb
new file mode 100644
index 000000000..421376db9
--- /dev/null
+++ b/app/src/test/resources/main.1434483388.obb.main.oldversion.obb
@@ -0,0 +1 @@
+dummy
diff --git a/app/src/test/resources/main.1619.obb.mainpatch.current.obb b/app/src/test/resources/main.1619.obb.mainpatch.current.obb
new file mode 100644
index 000000000..421376db9
--- /dev/null
+++ b/app/src/test/resources/main.1619.obb.mainpatch.current.obb
@@ -0,0 +1 @@
+dummy
diff --git a/app/src/test/resources/obbIndex.xml b/app/src/test/resources/obbIndex.xml
new file mode 100644
index 000000000..197d0eddf
--- /dev/null
+++ b/app/src/test/resources/obbIndex.xml
@@ -0,0 +1 @@
+This is a repository of apps to be used with F-Droid. Applications in this repository are either official binaries built by the original application developers, or are binaries built from source by the admin of f-droid.org using the tools on https://gitlab.com/u/fdroid. obb.main.oldversion2016-06-272016-06-27OBB Main Old Versionobb.main.oldversion.1444412523.png<p>No description available</p>GPLv3DevelopmentDevelopmenthttps://github.com/eighthave/urzip1Fi5xUHiAPRKxHvyUGVFGt9extBe8Srdbk999999990.11444412523obb.main.oldversion_1444412523.apk7562a36c9e2b38013b96663cf41f0f290dc7a248a81befa8d89e14f390c94c7fb4964fd759edaa54e65bb476d027688011470418main.1434483388.obb.main.oldversion.obbd3eb539a556352f3f47881d71fb0e5777b2f3e9a4251d283c18c67ce996774b72016-06-27obb.main.twoversions2016-06-272016-06-27OBB Main Two Versionsobb.main.twoversions.1101617.png<p>No description available</p>GPLv3DevelopmentDevelopmenthttps://github.com/eighthave/urzip1Fi5xUHiAPRKxHvyUGVFGt9extBe8Srdbk999999990.11101617obb.main.twoversions_1101617.apk9bc74566f089ef030ac33e7fbd99d92f1a38f363fb499fed138d9e7b774e821cb4964fd759edaa54e65bb476d027688011481418main.1101615.obb.main.twoversions.obbd3eb539a556352f3f47881d71fb0e5777b2f3e9a4251d283c18c67ce996774b72016-06-270.11101615obb.main.twoversions_1101615.apk7b0b7b9ba248e15751a16e3a0e01e1e24cbb673686c38422030cb75d5c33f0bbb4964fd759edaa54e65bb476d027688011480418main.1101615.obb.main.twoversions.obbd3eb539a556352f3f47881d71fb0e5777b2f3e9a4251d283c18c67ce996774b72016-06-270.11101613obb.main.twoversions_1101613.apkcce97a52ff18d843185be7f22ecb1a557c36b7a9f8ba07a8be94e328e00b35dcb4964fd759edaa54e65bb476d027688011477418main.1101613.obb.main.twoversions.obbd3eb539a556352f3f47881d71fb0e5777b2f3e9a4251d283c18c67ce996774b72016-06-27obb.mainpatch.current2016-06-272016-06-27OBB Main/Patch Currentobb.mainpatch.current.1619.png<p>No description available</p>GPLv3DevelopmentDevelopmenthttps://github.com/eighthave/urzip1Fi5xUHiAPRKxHvyUGVFGt9extBe8Srdbk999999990.11619obb.mainpatch.current_1619.apkeda5fc3ecfdac3252717e36bdbc9820865baeef162264af9ba5db7364f0e7a0cb4964fd759edaa54e65bb476d027688011479418main.1619.obb.mainpatch.current.obbd3eb539a556352f3f47881d71fb0e5777b2f3e9a4251d283c18c67ce996774b7patch.1619.obb.mainpatch.current.obbd3eb539a556352f3f47881d71fb0e5777b2f3e9a4251d283c18c67ce996774b72016-06-27info.guardianproject.urzip2016-06-272016-06-27urzip-πÇÇπÇÇ现代汉语通用字-български-عربي1234一个实用工具,获取已安装在您的设备上的应用的有关信息info.guardianproject.urzip.100.png<p>It’s Urzip 是一个获得已安装 APK 相关信息的实用工具。它从您的设备上已安装的所有应用开始,一键触摸即可显示 APK 的指纹,并且提供到达 virustotal.com 和 androidobservatory.org 的快捷链接,让您方便地了解特定 APK 的档案。它还可以让您导出签名证书和生成 ApkSignaturePin Pin 文件供 TrustedIntents 库使用。</p><p>★ Urzip 支持下列语言: Deutsch, English, español, suomi, 日本語, 한국어, Norsk, português (Portugal), Русский, Slovenščina, Türkçe 没看到您的语言?帮忙翻译本应用吧: https://www.transifex.com/projects/p/urzip</p><p>★ 致用户:我们还缺少你喜欢的功能?发现了一个 bug?请告诉我们!我们乐于听取您的意见。请发送电子邮件至: support@guardianproject.info 或者加入我们的聊天室 https://guardianproject.info/contact</p>GPLv3Development,GuardianProjectDevelopmenthttps://dev.guardianproject.info/projects/urziphttps://github.com/guardianproject/urziphttps://dev.guardianproject.info/projects/urzip/issues1Fi5xUHiAPRKxHvyUGVFGt9extBe8Srdbk99999990.1100urzip-πÇÇπÇÇ现代汉语通用字-български-عربي1234.apk15c0ec72c74a3791f42cdb43c57df0fb11a4dbb656851bbb8cf05b26a8372789b4964fd759edaa54e65bb476d0276880114714182016-06-27
\ No newline at end of file
diff --git a/app/src/test/resources/patch.1619.obb.mainpatch.current.obb b/app/src/test/resources/patch.1619.obb.mainpatch.current.obb
new file mode 100644
index 000000000..421376db9
--- /dev/null
+++ b/app/src/test/resources/patch.1619.obb.mainpatch.current.obb
@@ -0,0 +1 @@
+dummy
From bbac03b4d1bbc38f1035f3dced2ce6529a297014 Mon Sep 17 00:00:00 2001
From: Hans-Christoph Steiner
Date: Fri, 30 Sep 2016 11:00:15 +0200
Subject: [PATCH 2/9] use ApkTable column names when parsing XML
This makes it easier to track the relationship between the index XML and
the database tables where that data is ultimately stored and used. There
are a few mismatches between the XML tag and database column names, so
those are just marked with a comment.
This makes it much easier to find all the spots in the code that need
changing when adding new columns/data to the APK table, like the OBB stuff.
In Android Studio, just Ctrl-Click on any table constant definition, and
then it lists all the places its used. Any new data will need to be added
in all of those locations.
---
.../org/fdroid/fdroid/RepoXMLHandler.java | 28 +++++++++----------
1 file changed, 14 insertions(+), 14 deletions(-)
diff --git a/app/src/main/java/org/fdroid/fdroid/RepoXMLHandler.java b/app/src/main/java/org/fdroid/fdroid/RepoXMLHandler.java
index bb7d53580..ec4cf5dcd 100644
--- a/app/src/main/java/org/fdroid/fdroid/RepoXMLHandler.java
+++ b/app/src/main/java/org/fdroid/fdroid/RepoXMLHandler.java
@@ -100,16 +100,16 @@ public class RepoXMLHandler extends DefaultHandler {
final String str = curchars.toString().trim();
if (curapk != null) {
switch (localName) {
- case "version":
+ case ApkTable.Cols.VERSION_NAME:
curapk.versionName = str;
break;
- case "versioncode":
+ case "versioncode": // ApkTable.Cols.VERSION_CODE
curapk.versionCode = Utils.parseInt(str, -1);
break;
- case "size":
+ case ApkTable.Cols.SIZE:
curapk.size = Utils.parseInt(str, 0);
break;
- case "hash":
+ case ApkTable.Cols.HASH:
if (currentApkHashType == null || "md5".equals(currentApkHashType)) {
if (curapk.hash == null) {
curapk.hash = str;
@@ -120,22 +120,22 @@ public class RepoXMLHandler extends DefaultHandler {
curapk.hashType = "SHA-256";
}
break;
- case "sig":
+ case ApkTable.Cols.SIGNATURE:
curapk.sig = str;
break;
- case "srcname":
+ case ApkTable.Cols.SOURCE_NAME:
curapk.srcname = str;
break;
- case "apkname":
+ case "apkname": // ApkTable.Cols.NAME
curapk.apkName = str;
break;
- case "sdkver":
+ case "sdkver": // ApkTable.Cols.MIN_SDK_VERSION
curapk.minSdkVersion = Utils.parseInt(str, Apk.SDK_VERSION_MIN_VALUE);
break;
- case "targetSdkVersion":
+ case ApkTable.Cols.TARGET_SDK_VERSION:
curapk.targetSdkVersion = Utils.parseInt(str, Apk.SDK_VERSION_MIN_VALUE);
break;
- case "maxsdkver":
+ case "maxsdkver": // ApkTable.Cols.MAX_SDK_VERSION
curapk.maxSdkVersion = Utils.parseInt(str, Apk.SDK_VERSION_MAX_VALUE);
if (curapk.maxSdkVersion == 0) {
// before fc0df0dcf4dd0d5f13de82d7cd9254b2b48cb62d, this could be 0
@@ -154,16 +154,16 @@ public class RepoXMLHandler extends DefaultHandler {
case ApkTable.Cols.OBB_PATCH_FILE_SHA256:
curapk.obbPatchFileSha256 = str;
break;
- case "added":
+ case ApkTable.Cols.ADDED_DATE:
curapk.added = Utils.parseDate(str, null);
break;
- case "permissions":
+ case ApkTable.Cols.PERMISSIONS:
curapk.permissions = Utils.parseCommaSeparatedString(str);
break;
- case "features":
+ case ApkTable.Cols.FEATURES:
curapk.features = Utils.parseCommaSeparatedString(str);
break;
- case "nativecode":
+ case ApkTable.Cols.NATIVE_CODE:
curapk.nativecode = Utils.parseCommaSeparatedString(str);
break;
}
From a5e6dad9bf85c83ff20ce7dd0a9a81e2177d99a0 Mon Sep 17 00:00:00 2001
From: Hans-Christoph Steiner
Date: Tue, 28 Jun 2016 21:04:45 +0200
Subject: [PATCH 3/9] allow apps to request OBB download URLs from F-Droid
By sending an Intent to F-Droid, it will reply with the full download URL
to the OBB file, if one exists for the currently installed version of the
requesting app.
---
app/src/main/AndroidManifest.xml | 2 +
.../fdroid/fdroid/data/ObbUrlActivity.java | 69 +++++++++++++++++++
2 files changed, 71 insertions(+)
create mode 100644 app/src/main/java/org/fdroid/fdroid/data/ObbUrlActivity.java
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index eb2332ace..04ad5e943 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -430,6 +430,8 @@
+
diff --git a/app/src/main/java/org/fdroid/fdroid/data/ObbUrlActivity.java b/app/src/main/java/org/fdroid/fdroid/data/ObbUrlActivity.java
new file mode 100644
index 000000000..920483473
--- /dev/null
+++ b/app/src/main/java/org/fdroid/fdroid/data/ObbUrlActivity.java
@@ -0,0 +1,69 @@
+package org.fdroid.fdroid.data;
+
+import android.app.Activity;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.net.Uri;
+import android.os.Bundle;
+
+import org.fdroid.fdroid.Utils;
+
+/**
+ * Replies with the public download URL for the OBB that belongs to the
+ * requesting app/version. If it doesn't know the OBB URL for the requesting
+ * app, the {@code resultCode} will be {@link Activity#RESULT_CANCELED}. The
+ * request must be sent with {@link Activity#startActivityForResult(Intent, int)}
+ * in order to receive a reply, which will include an {@link Intent} with the
+ * URL as data and the SHA-256 hash as a String {@code Intent} extra.
+ */
+public class ObbUrlActivity extends Activity {
+ public static final String TAG = "ObbUrlActivity";
+
+ public static final String ACTION_GET_OBB_MAIN_URL = "org.fdroid.fdroid.action.GET_OBB_MAIN_URL";
+ public static final String ACTION_GET_OBB_PATCH_URL = "org.fdroid.fdroid.action.GET_OBB_PATCH_URL";
+
+ public static final String EXTRA_SHA256 = "org.fdroid.fdroid.extra.SHA256";
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ Intent intent = getIntent();
+ ComponentName componentName = getCallingActivity();
+ setResult(RESULT_CANCELED);
+ if (intent != null && componentName != null) {
+ String action = intent.getAction();
+ String packageName = componentName.getPackageName();
+ Apk apk = null;
+
+ try {
+ PackageManager pm = getPackageManager();
+ PackageInfo packageInfo = pm.getPackageInfo(packageName, 0);
+ apk = ApkProvider.Helper.findApkFromAnyRepo(this, packageName, packageInfo.versionCode);
+ } catch (PackageManager.NameNotFoundException e) {
+ Utils.debugLog(TAG, e.getLocalizedMessage());
+ }
+
+ if (apk == null) {
+ Utils.debugLog(TAG, "got null APK for " + packageName);
+ } else if (ACTION_GET_OBB_MAIN_URL.equals(action)) {
+ String url = apk.getMainObbUrl();
+ if (url != null) {
+ intent.setData(Uri.parse(url));
+ intent.putExtra(EXTRA_SHA256, apk.obbMainFileSha256);
+ }
+ setResult(RESULT_OK, intent);
+ } else if (ACTION_GET_OBB_PATCH_URL.equals(action)) {
+ String url = apk.getPatchObbUrl();
+ if (url != null) {
+ intent.setData(Uri.parse(url));
+ intent.putExtra(EXTRA_SHA256, apk.obbPatchFileSha256);
+ }
+ setResult(RESULT_OK, intent);
+ }
+ }
+ finish();
+ }
+}
From 4c4aef5314d513111d1ac959c8785fb7030ee21b Mon Sep 17 00:00:00 2001
From: Hans-Christoph Steiner
Date: Fri, 30 Sep 2016 12:46:58 +0200
Subject: [PATCH 4/9] refactor into reusable static method for checking file
hashes
This takes the APK file hash checker and turns it into a generic static
utility method for checking that a given file matches a given hash. This
will be needed as F-Droid handles other file types, like OBB and media.
---
.../main/java/org/fdroid/fdroid/Hasher.java | 15 ++++
.../org/fdroid/fdroid/installer/ApkCache.java | 73 +++++++------------
2 files changed, 40 insertions(+), 48 deletions(-)
diff --git a/app/src/main/java/org/fdroid/fdroid/Hasher.java b/app/src/main/java/org/fdroid/fdroid/Hasher.java
index a6bf80635..4f4e34abc 100644
--- a/app/src/main/java/org/fdroid/fdroid/Hasher.java
+++ b/app/src/main/java/org/fdroid/fdroid/Hasher.java
@@ -97,6 +97,21 @@ public class Hasher {
return hashCache.equals(otherHash.toLowerCase(Locale.ENGLISH));
}
+ /**
+ * Checks the file against the provided hash, returning whether it is a match.
+ */
+ public static boolean isFileMatchingHash(File file, String hash, String hashType) {
+ if (!file.exists()) {
+ return false;
+ }
+ try {
+ Hasher hasher = new Hasher(hashType, file);
+ return hasher.match(hash);
+ } catch (NoSuchAlgorithmException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
public static String hex(Certificate cert) {
byte[] encoded;
try {
diff --git a/app/src/main/java/org/fdroid/fdroid/installer/ApkCache.java b/app/src/main/java/org/fdroid/fdroid/installer/ApkCache.java
index 47f6fb32e..c920ba13d 100644
--- a/app/src/main/java/org/fdroid/fdroid/installer/ApkCache.java
+++ b/app/src/main/java/org/fdroid/fdroid/installer/ApkCache.java
@@ -31,7 +31,6 @@ import org.fdroid.fdroid.data.SanitizedFile;
import java.io.File;
import java.io.IOException;
-import java.security.NoSuchAlgorithmException;
public class ApkCache {
@@ -45,50 +44,32 @@ public class ApkCache {
*/
public static SanitizedFile copyApkFromCacheToFiles(Context context, File apkFile, Apk expectedApk)
throws IOException {
- SanitizedFile sanitizedApkFile = null;
+ SanitizedFile sanitizedApkFile = SanitizedFile.knownSanitized(
+ File.createTempFile("install-", ".apk", context.getFilesDir()));
+ FileUtils.copyFile(apkFile, sanitizedApkFile);
- try {
- sanitizedApkFile = SanitizedFile.knownSanitized(
- File.createTempFile("install-", ".apk", context.getFilesDir()));
- FileUtils.copyFile(apkFile, sanitizedApkFile);
+ // verify copied file's hash with expected hash from Apk class
+ if (!Hasher.isFileMatchingHash(sanitizedApkFile, expectedApk.hash, expectedApk.hashType)) {
+ FileUtils.deleteQuietly(apkFile);
+ throw new IOException(apkFile + " failed to verify!");
+ }
- // verify copied file's hash with expected hash from Apk class
- if (!verifyApkFile(sanitizedApkFile, expectedApk.hash, expectedApk.hashType)) {
- FileUtils.deleteQuietly(apkFile);
- throw new IOException(apkFile + " failed to verify!");
- }
-
- return sanitizedApkFile;
- } catch (NoSuchAlgorithmException e) {
- throw new RuntimeException(e);
- } finally {
- // 20 minutes the start of the install process, delete the file
- final File apkToDelete = sanitizedApkFile;
- new Thread() {
- @Override
- public void run() {
- android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_LOWEST);
- try {
- Thread.sleep(1200000);
- } catch (InterruptedException ignored) {
- } finally {
- FileUtils.deleteQuietly(apkToDelete);
- }
+ // 20 minutes the start of the install process, delete the file
+ final File apkToDelete = sanitizedApkFile;
+ new Thread() {
+ @Override
+ public void run() {
+ android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_LOWEST);
+ try {
+ Thread.sleep(1200000);
+ } catch (InterruptedException ignored) {
+ } finally {
+ FileUtils.deleteQuietly(apkToDelete);
}
- }.start();
- }
- }
+ }
+ }.start();
- /**
- * Checks the APK file against the provided hash, returning whether it is a match.
- */
- private static boolean verifyApkFile(File apkFile, String hash, String hashType)
- throws NoSuchAlgorithmException {
- if (!apkFile.exists()) {
- return false;
- }
- Hasher hasher = new Hasher(hashType, apkFile);
- return hasher.match(hash);
+ return sanitizedApkFile;
}
/**
@@ -108,19 +89,15 @@ public class ApkCache {
* Bails out if the file sizes don't match to prevent having to do the work of hashing the file.
*/
public static boolean apkIsCached(File apkFile, Apk apkToCheck) {
- try {
- return apkFile.length() == apkToCheck.size &&
- verifyApkFile(apkFile, apkToCheck.hash, apkToCheck.hashType);
- } catch (NoSuchAlgorithmException e) {
- throw new RuntimeException(e);
- }
+ return apkFile.length() == apkToCheck.size &&
+ Hasher.isFileMatchingHash(apkFile, apkToCheck.hash, apkToCheck.hashType);
}
/**
* This location is only for caching, do not install directly from this location
* because if the file is on the External Storage, any other app could swap out
* the APK while the install was in process, allowing malware to install things.
- * Using {@link Installer#installPackage(Uri, Uri, Apk)}
+ * Using {@link Installer#installPackage(Uri, Uri)}
* is fine since that does the right thing.
*/
public static File getApkCacheDir(Context context) {
From 8affa08d11aa552c518e58e9c35601011248f092 Mon Sep 17 00:00:00 2001
From: Hans-Christoph Steiner
Date: Thu, 6 Oct 2016 17:43:43 +0200
Subject: [PATCH 5/9] 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
---
app/src/main/AndroidManifest.xml | 3 +-
.../installer/InstallManagerService.java | 82 ++++++++++++++++++-
.../fdroid/installer/InstallerService.java | 39 ++++++++-
3 files changed, 117 insertions(+), 7 deletions(-)
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();
}
}
From b8162a1a916fb1ee989a9cb906f8da760304fe99 Mon Sep 17 00:00:00 2001
From: Hans-Christoph Steiner
Date: Fri, 30 Sep 2016 11:34:02 +0200
Subject: [PATCH 6/9] InstallManagerService.cancel() to handle all cancellation
Now that there are also OBB downloads, there needs to be a central cancel
method provided by InstallManagerService.
---
.../java/org/fdroid/fdroid/AppDetails.java | 2 +-
.../installer/InstallManagerService.java | 42 +++++++++++++++----
.../fdroid/fdroid/net/DownloaderService.java | 19 +++------
3 files changed, 42 insertions(+), 21 deletions(-)
diff --git a/app/src/main/java/org/fdroid/fdroid/AppDetails.java b/app/src/main/java/org/fdroid/fdroid/AppDetails.java
index 5ab62ded9..8eae7d2c7 100644
--- a/app/src/main/java/org/fdroid/fdroid/AppDetails.java
+++ b/app/src/main/java/org/fdroid/fdroid/AppDetails.java
@@ -1555,7 +1555,7 @@ public class AppDetails extends AppCompatActivity {
return;
}
- DownloaderService.cancel(getContext(), appDetails.activeDownloadUrlString);
+ InstallManagerService.cancel(getContext(), appDetails.activeDownloadUrlString);
}
public void updateViews() {
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 70b40c8e3..226aafd6e 100644
--- a/app/src/main/java/org/fdroid/fdroid/installer/InstallManagerService.java
+++ b/app/src/main/java/org/fdroid/fdroid/installer/InstallManagerService.java
@@ -14,6 +14,7 @@ import android.net.Uri;
import android.os.IBinder;
import android.support.v4.app.NotificationCompat;
import android.support.v4.app.TaskStackBuilder;
+import android.support.v4.content.IntentCompat;
import android.support.v4.content.LocalBroadcastManager;
import android.text.TextUtils;
@@ -77,6 +78,7 @@ public class InstallManagerService extends Service {
private static final String TAG = "InstallManagerService";
private static final String ACTION_INSTALL = "org.fdroid.fdroid.installer.action.INSTALL";
+ private static final String ACTION_CANCEL = "org.fdroid.fdroid.installer.action.CANCEL";
private static final String EXTRA_APP = "org.fdroid.fdroid.installer.extra.APP";
private static final String EXTRA_APK = "org.fdroid.fdroid.installer.extra.APK";
@@ -136,17 +138,25 @@ public class InstallManagerService extends Service {
public int onStartCommand(Intent intent, int flags, int startId) {
Utils.debugLog(TAG, "onStartCommand " + intent);
- if (!ACTION_INSTALL.equals(intent.getAction())) {
- Utils.debugLog(TAG, "Ignoring " + intent + " as it is not an " + ACTION_INSTALL + " intent");
- return START_NOT_STICKY;
- }
-
String urlString = intent.getDataString();
if (TextUtils.isEmpty(urlString)) {
Utils.debugLog(TAG, "empty urlString, nothing to do");
return START_NOT_STICKY;
}
+ String action = intent.getAction();
+ if (ACTION_CANCEL.equals(action)) {
+ DownloaderService.cancel(this, urlString);
+ Apk apk = getApkFromActive(urlString);
+ DownloaderService.cancel(this, apk.getPatchObbUrl());
+ DownloaderService.cancel(this, apk.getMainObbUrl());
+ cancelNotification(urlString);
+ return START_NOT_STICKY;
+ } else if (!ACTION_INSTALL.equals(action)) {
+ Utils.debugLog(TAG, "Ignoring " + intent + " as it is not an " + ACTION_INSTALL + " intent");
+ return START_NOT_STICKY;
+ }
+
if (!intent.hasExtra(EXTRA_APP) || !intent.hasExtra(EXTRA_APK)) {
Utils.debugLog(TAG, urlString + " did not include both an App and Apk instance, ignoring");
return START_NOT_STICKY;
@@ -381,7 +391,7 @@ public class InstallManagerService extends Service {
.setContentIntent(getAppDetailsIntent(downloadUrlId, apk))
.setContentTitle(getString(R.string.downloading_apk, getAppName(apk)))
.addAction(R.drawable.ic_cancel_black_24dp, getString(R.string.cancel),
- DownloaderService.getCancelPendingIntent(this, urlString))
+ getCancelPendingIntent(urlString))
.setSmallIcon(android.R.drawable.stat_sys_download)
.setContentText(urlString)
.setProgress(100, 0, true);
@@ -426,7 +436,7 @@ public class InstallManagerService extends Service {
if (TextUtils.isEmpty(name) || name.equals(new App().name)) {
ContentResolver resolver = getContentResolver();
App app = AppProvider.Helper.findSpecificApp(resolver, apk.packageName, apk.repo,
- new String[] {Schema.AppMetadataTable.Cols.NAME});
+ new String[]{Schema.AppMetadataTable.Cols.NAME});
if (app == null || TextUtils.isEmpty(app.name)) {
return; // do not have a name to display, so leave notification as is
}
@@ -534,6 +544,17 @@ public class InstallManagerService extends Service {
return apk;
}
+ private PendingIntent getCancelPendingIntent(String urlString) {
+ Intent intent = new Intent(this, InstallManagerService.class)
+ .setData(Uri.parse(urlString))
+ .setAction(ACTION_CANCEL)
+ .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | IntentCompat.FLAG_ACTIVITY_CLEAR_TASK);
+ return PendingIntent.getService(this,
+ urlString.hashCode(),
+ intent,
+ PendingIntent.FLAG_UPDATE_CURRENT);
+ }
+
/**
* Install an APK, checking the cache and downloading if necessary before starting the process.
* All notifications are sent as an {@link Intent} via local broadcasts to be received by
@@ -554,6 +575,13 @@ public class InstallManagerService extends Service {
context.startService(intent);
}
+ public static void cancel(Context context, String urlString) {
+ Intent intent = new Intent(context, InstallManagerService.class);
+ intent.setAction(ACTION_CANCEL);
+ intent.setData(Uri.parse(urlString));
+ context.startService(intent);
+ }
+
/**
* Returns a {@link Set} of the {@code urlString}s that are currently active.
* {@code urlString}s are used as unique IDs throughout the
diff --git a/app/src/main/java/org/fdroid/fdroid/net/DownloaderService.java b/app/src/main/java/org/fdroid/fdroid/net/DownloaderService.java
index 57588dd7c..0dfab580b 100644
--- a/app/src/main/java/org/fdroid/fdroid/net/DownloaderService.java
+++ b/app/src/main/java/org/fdroid/fdroid/net/DownloaderService.java
@@ -17,7 +17,6 @@
package org.fdroid.fdroid.net;
-import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
@@ -30,7 +29,6 @@ import android.os.Looper;
import android.os.Message;
import android.os.PatternMatcher;
import android.os.Process;
-import android.support.v4.content.IntentCompat;
import android.support.v4.content.LocalBroadcastManager;
import android.text.TextUtils;
@@ -154,17 +152,6 @@ public class DownloaderService extends Service {
return START_REDELIVER_INTENT; // if killed before completion, retry Intent
}
- public static PendingIntent getCancelPendingIntent(Context context, String urlString) {
- Intent cancelIntent = new Intent(context.getApplicationContext(), DownloaderService.class)
- .setData(Uri.parse(urlString))
- .setAction(ACTION_CANCEL)
- .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | IntentCompat.FLAG_ACTIVITY_CLEAR_TASK);
- return PendingIntent.getService(context.getApplicationContext(),
- urlString.hashCode(),
- cancelIntent,
- PendingIntent.FLAG_UPDATE_CURRENT);
- }
-
@Override
public void onDestroy() {
Utils.debugLog(TAG, "Destroying downloader service. Will move to background and stop our Looper.");
@@ -258,6 +245,9 @@ public class DownloaderService extends Service {
* @see #cancel(Context, String)
*/
public static void queue(Context context, String urlString) {
+ if (TextUtils.isEmpty(urlString)) {
+ return;
+ }
Utils.debugLog(TAG, "Preparing " + urlString + " to go into the download queue");
Intent intent = new Intent(context, DownloaderService.class);
intent.setAction(ACTION_QUEUE);
@@ -275,6 +265,9 @@ public class DownloaderService extends Service {
* @see #queue(Context, String)
*/
public static void cancel(Context context, String urlString) {
+ if (TextUtils.isEmpty(urlString)) {
+ return;
+ }
Utils.debugLog(TAG, "Preparing cancellation of " + urlString + " download");
Intent intent = new Intent(context, DownloaderService.class);
intent.setAction(ACTION_CANCEL);
From e1a6c931c69290635ecfe447d8551f577596a818 Mon Sep 17 00:00:00 2001
From: Hans-Christoph Steiner
Date: Wed, 5 Oct 2016 21:35:15 +0200
Subject: [PATCH 7/9] make sure uninstall process has an Apk instance
If a user clicks install, then uninstall on AppDetails, then there was not
yet a chance to refresh the App instance, and therefore app.installedApk
will still be null. This is really just a workaround for now, because
AppDetails needs a full refactoring.
---
app/src/main/java/org/fdroid/fdroid/AppDetails.java | 13 +++++++++++++
1 file changed, 13 insertions(+)
diff --git a/app/src/main/java/org/fdroid/fdroid/AppDetails.java b/app/src/main/java/org/fdroid/fdroid/AppDetails.java
index 8eae7d2c7..da54273ea 100644
--- a/app/src/main/java/org/fdroid/fdroid/AppDetails.java
+++ b/app/src/main/java/org/fdroid/fdroid/AppDetails.java
@@ -29,6 +29,7 @@ import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
+import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.database.ContentObserver;
import android.graphics.Bitmap;
@@ -964,6 +965,18 @@ public class AppDetails extends AppCompatActivity {
*/
private void uninstallApk() {
Apk apk = app.installedApk;
+ if (apk == null) {
+ // TODO ideally, app would be refreshed immediately after install, then this
+ // workaround would be unnecessary
+ try {
+ PackageInfo pi = packageManager.getPackageInfo(app.packageName, 0);
+ apk = ApkProvider.Helper.findApkFromAnyRepo(this, pi.packageName, pi.versionCode);
+ app.installedApk = apk;
+ } catch (PackageManager.NameNotFoundException e) {
+ e.printStackTrace();
+ return; // not installed
+ }
+ }
Installer installer = InstallerFactory.create(this, apk);
Intent intent = installer.getUninstallScreen();
if (intent != null) {
From 470145e61149be02352798103b9bd9ca5ac6f1eb Mon Sep 17 00:00:00 2001
From: Hans-Christoph Steiner
Date: Thu, 6 Oct 2016 15:33:25 +0200
Subject: [PATCH 8/9] remove unused ContentValuesCursor class
This was replaced entirely by making Apk implement Parcelable
---
.../fdroid/data/ContentValuesCursor.java | 93 -------------------
1 file changed, 93 deletions(-)
delete mode 100644 app/src/main/java/org/fdroid/fdroid/data/ContentValuesCursor.java
diff --git a/app/src/main/java/org/fdroid/fdroid/data/ContentValuesCursor.java b/app/src/main/java/org/fdroid/fdroid/data/ContentValuesCursor.java
deleted file mode 100644
index 3c64c7957..000000000
--- a/app/src/main/java/org/fdroid/fdroid/data/ContentValuesCursor.java
+++ /dev/null
@@ -1,93 +0,0 @@
-package org.fdroid.fdroid.data;
-
-import android.content.ContentValues;
-import android.database.AbstractCursor;
-import android.database.Cursor;
-import android.os.Bundle;
-
-import java.util.Map;
-
-/**
- * In order to keep {@link App#App(Cursor)} and {@link Apk#Apk(Cursor)} as
- * efficient as possible, this wrapper class is used to instantiate {@code App}
- * and {@code Apk} from {@link App#toContentValues()} and
- * {@link Apk#toContentValues()} included as extras {@link Bundle}s in the
- * {@link android.content.Intent} that starts
- * {@link org.fdroid.fdroid.installer.InstallManagerService}
- *
- * This implemented to throw an {@link IllegalArgumentException} if the types
- * do not match what they are expected to be so that things fail fast. So that
- * means only types used in {@link App#toContentValues()} and
- * {@link Apk#toContentValues()} are implemented.
- */
-class ContentValuesCursor extends AbstractCursor {
-
- private final String[] keys;
- private final Object[] values;
-
- ContentValuesCursor(ContentValues contentValues) {
- super();
- keys = new String[contentValues.size()];
- values = new Object[contentValues.size()];
- int i = 0;
- for (Map.Entry entry : contentValues.valueSet()) {
- keys[i] = entry.getKey();
- values[i] = entry.getValue();
- i++;
- }
- moveToFirst();
- }
-
- @Override
- public int getCount() {
- return 1;
- }
-
- @Override
- public String[] getColumnNames() {
- return keys;
- }
-
- @Override
- public String getString(int i) {
- return (String) values[i];
- }
-
- @Override
- public int getInt(int i) {
- if (values[i] instanceof Long) {
- return ((Long) values[i]).intValue();
- } else if (values[i] instanceof Integer) {
- return (int) values[i];
- }
- throw new IllegalArgumentException("unimplemented");
- }
-
- @Override
- public long getLong(int i) {
- if (values[i] instanceof Long) {
- return (Long) values[i];
- }
- throw new IllegalArgumentException("Value is not a Long");
- }
-
- @Override
- public short getShort(int i) {
- throw new IllegalArgumentException("unimplemented");
- }
-
- @Override
- public float getFloat(int i) {
- throw new IllegalArgumentException("unimplemented");
- }
-
- @Override
- public double getDouble(int i) {
- throw new IllegalArgumentException("unimplemented");
- }
-
- @Override
- public boolean isNull(int i) {
- return values[i] == null;
- }
-}
From 65c4087b05cc5ec387d5e7785247c78dae1f7bda Mon Sep 17 00:00:00 2001
From: Hans-Christoph Steiner
Date: Thu, 6 Oct 2016 19:45:16 +0200
Subject: [PATCH 9/9] improved Apk.toString() for easier debugging
---
app/src/main/java/org/fdroid/fdroid/data/Apk.java | 2 +-
.../java/org/fdroid/fdroid/installer/InstallerFactory.java | 4 ++--
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/app/src/main/java/org/fdroid/fdroid/data/Apk.java b/app/src/main/java/org/fdroid/fdroid/data/Apk.java
index 592644699..406966308 100644
--- a/app/src/main/java/org/fdroid/fdroid/data/Apk.java
+++ b/app/src/main/java/org/fdroid/fdroid/data/Apk.java
@@ -270,7 +270,7 @@ public class Apk extends ValueObject implements Comparable, Parcelable {
@Override
public String toString() {
- return packageName + " (version " + versionCode + ")";
+ return toContentValues().toString();
}
public ContentValues toContentValues() {
diff --git a/app/src/main/java/org/fdroid/fdroid/installer/InstallerFactory.java b/app/src/main/java/org/fdroid/fdroid/installer/InstallerFactory.java
index 47c9bc74c..fffb47fab 100644
--- a/app/src/main/java/org/fdroid/fdroid/installer/InstallerFactory.java
+++ b/app/src/main/java/org/fdroid/fdroid/installer/InstallerFactory.java
@@ -41,12 +41,12 @@ public class InstallerFactory {
*/
public static Installer create(Context context, Apk apk) {
if (apk == null || TextUtils.isEmpty(apk.packageName)) {
- throw new IllegalArgumentException("packageName must not be empty!");
+ throw new IllegalArgumentException("Apk.packageName must not be empty: " + apk);
}
Installer installer;
if (apk.packageName.equals(PrivilegedInstaller.PRIVILEGED_EXTENSION_PACKAGE_NAME)) {
- // special case for "F-Droid Privileged Extension"
+ // special case for installing "Privileged Extension" with root
installer = new ExtensionInstaller(context, apk);
} else if (PrivilegedInstaller.isDefault(context)) {
Utils.debugLog(TAG, "privileged extension correctly installed -> PrivilegedInstaller");