diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index eb2332ace..39cd25d89 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -42,8 +42,7 @@ - + @@ -430,6 +429,8 @@ + diff --git a/app/src/main/java/org/fdroid/fdroid/AppDetails.java b/app/src/main/java/org/fdroid/fdroid/AppDetails.java index 5ab62ded9..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) { @@ -1555,7 +1568,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/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/RepoXMLHandler.java b/app/src/main/java/org/fdroid/fdroid/RepoXMLHandler.java index 984b99de7..ec4cf5dcd 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; @@ -99,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; @@ -119,38 +120,50 @@ 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 curapk.maxSdkVersion = Apk.SDK_VERSION_MAX_VALUE; } break; - case "added": + 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 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; } 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..406966308 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(".")) { @@ -192,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() { @@ -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/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; - } -} 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/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(); + } +} 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/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) { 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..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,10 +14,14 @@ 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; +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 +33,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,11 +67,18 @@ 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"; 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"; @@ -125,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; @@ -160,7 +181,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 +209,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 @@ -303,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); @@ -348,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 } @@ -456,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 @@ -476,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/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"); 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(); } } 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); 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 Version

obb.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