diff --git a/app/build.gradle b/app/build.gradle
index b99c16ec1..c4a97632e 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -189,6 +189,16 @@ android {
}
}
+ sourceSets {
+ test {
+ java.srcDirs += "$projectDir/src/testShared/java"
+ }
+
+ androidTest {
+ java.srcDirs += "$projectDir/src/testShared/java"
+ }
+ }
+
lintOptions {
checkReleaseBuilds false
abortOnError true
diff --git a/app/src/androidTest/assets/extendedPerms.xml b/app/src/androidTest/assets/extendedPerms.xml
new file mode 100644
index 000000000..a573dfa28
--- /dev/null
+++ b/app/src/androidTest/assets/extendedPerms.xml
@@ -0,0 +1,125 @@
+
+
+
+
+ This is just a test of the extended permissions attributes.
+
+
+
+
+ at.bitfire.davdroid
+ 2013-10-13
+ 2016-06-26
+ DAVdroid
+ Contacts and Calendar sync
+ at.bitfire.davdroid.107.png
+ apk generated from urzip to test extended permissions
+ GPLv3
+ Internet
+ Internet
+ https://davdroid.bitfire.at/
+ https://davdroid.bitfire.at/source/
+ https://davdroid.bitfire.at/forums/
+ https://gitlab.com/bitfireAT/davdroid/tags
+ https://davdroid.bitfire.at/donate/
+ 1KSCy7RHztKuhW9fLLaUYqdwdC2iwbejZU
+ 2100160
+ 1.1.1.2
+ 107
+
+ 1.3.2-FAKE
+ 117
+ org.fdroid.extendedpermissionstest.apk
+ at.bitfire.davdroid_116_src.tar.gz
+ f1aa02257e99c167d2ea9b0e9525c3ce7c181fe2e7f4dd00b65dd81ed2e27a62
+
+ 03542175324d067b4c36582242f8aecc
+ 3298864
+ 14
+ 23
+ 2016-09-22
+
+ READ_EXTERNAL_STORAGE,WRITE_SYNC_SETTINGS,ACCESS_NETWORK_STATE,WRITE_EXTERNAL_STORAGE,WRITE_CONTACTS,ACCESS_WIFI_STATE,REQUEST_IGNORE_BATTERY_OPTIMIZATIONS,WRITE_CALENDAR,READ_CONTACTS,READ_SYNC_SETTINGS,MANAGE_ACCOUNTS,INTERNET,AUTHENTICATE_ACCOUNTS,GET_ACCOUNTS,READ_CALENDAR,READ_SYNC_STATS
+
+
+
+
+
+
+
+
+
+
+ 1.3.1-ose
+ 116
+ at.bitfire.davdroid_116.apk
+ at.bitfire.davdroid_116_src.tar.gz
+ f1aa02257e99c167d2ea9b0e9525c3ce7c181fe2e7f4dd00b65dd81ed2e27a62
+
+ 03542175324d067b4c36582242f8aecc
+ 3298864
+ 14
+ 24
+ 2016-09-21
+
+ READ_EXTERNAL_STORAGE,WRITE_SYNC_SETTINGS,ACCESS_NETWORK_STATE,WRITE_EXTERNAL_STORAGE,org.dmfs.permission.READ_TASKS,WRITE_CONTACTS,ACCESS_WIFI_STATE,REQUEST_IGNORE_BATTERY_OPTIMIZATIONS,WRITE_CALENDAR,READ_CONTACTS,READ_SYNC_SETTINGS,MANAGE_ACCOUNTS,INTERNET,AUTHENTICATE_ACCOUNTS,GET_ACCOUNTS,READ_CALENDAR,org.dmfs.permission.WRITE_TASKS
+
+
+
+
+
+
+
+
+ 1.1.1.2
+ 107
+ at.bitfire.davdroid_107.apk
+ at.bitfire.davdroid_107_src.tar.gz
+ 9a616f2e97bf8cf012baf896f95667dea4e3ce3252b31c5715073638a9fcc3d4
+
+ 03542175324d067b4c36582242f8aecc
+ 3134363
+ 14
+ 23
+ 2016-06-26
+
+ org.dmfs.permission.READ_TASKS,READ_EXTERNAL_STORAGE,WRITE_CONTACTS,GET_ACCOUNTS,AUTHENTICATE_ACCOUNTS,WRITE_EXTERNAL_STORAGE,READ_CALENDAR,ACCESS_WIFI_STATE,org.dmfs.permission.WRITE_TASKS,ACCESS_NETWORK_STATE,WRITE_CALENDAR,READ_CONTACTS,READ_SYNC_SETTINGS,INTERNET,MANAGE_ACCOUNTS,WRITE_SYNC_SETTINGS
+
+
+
+ 1.1.1.1
+ 105
+ at.bitfire.davdroid_105.apk
+ at.bitfire.davdroid_105_src.tar.gz
+ 4a0408c61536a1cc1028cea4273adbde2e57dfa2b12d93c3b52f4c3d095e2849
+
+ 03542175324d067b4c36582242f8aecc
+ 3131567
+ 14
+ 23
+ 2016-06-24
+
+ org.dmfs.permission.READ_TASKS,READ_EXTERNAL_STORAGE,WRITE_CONTACTS,GET_ACCOUNTS,AUTHENTICATE_ACCOUNTS,WRITE_EXTERNAL_STORAGE,READ_CALENDAR,ACCESS_WIFI_STATE,org.dmfs.permission.WRITE_TASKS,ACCESS_NETWORK_STATE,WRITE_CALENDAR,READ_CONTACTS,READ_SYNC_SETTINGS,INTERNET,MANAGE_ACCOUNTS,WRITE_SYNC_SETTINGS
+
+
+
+ 1.1.1
+ 104
+ at.bitfire.davdroid_104.apk
+ at.bitfire.davdroid_104_src.tar.gz
+ 09ba34996177efe8b1498a93fe6521ab84efab3bccb0f42449116e80b59e5b56
+
+ 03542175324d067b4c36582242f8aecc
+ 3131367
+ 14
+ 23
+ 2016-06-22
+
+ org.dmfs.permission.READ_TASKS,READ_EXTERNAL_STORAGE,WRITE_CONTACTS,GET_ACCOUNTS,AUTHENTICATE_ACCOUNTS,WRITE_EXTERNAL_STORAGE,READ_CALENDAR,ACCESS_WIFI_STATE,org.dmfs.permission.WRITE_TASKS,ACCESS_NETWORK_STATE,WRITE_CALENDAR,READ_CONTACTS,READ_SYNC_SETTINGS,INTERNET,MANAGE_ACCOUNTS,WRITE_SYNC_SETTINGS
+
+
+
+
+
diff --git a/app/src/androidTest/assets/org.fdroid.extendedpermissionstest.apk b/app/src/androidTest/assets/org.fdroid.extendedpermissionstest.apk
new file mode 100644
index 000000000..9e2377dc2
Binary files /dev/null and b/app/src/androidTest/assets/org.fdroid.extendedpermissionstest.apk differ
diff --git a/app/src/androidTest/java/org/fdroid/fdroid/installer/ApkVerifierTest.java b/app/src/androidTest/java/org/fdroid/fdroid/installer/ApkVerifierTest.java
index d88380634..28bfffdea 100644
--- a/app/src/androidTest/java/org/fdroid/fdroid/installer/ApkVerifierTest.java
+++ b/app/src/androidTest/java/org/fdroid/fdroid/installer/ApkVerifierTest.java
@@ -21,18 +21,32 @@ package org.fdroid.fdroid.installer;
import android.app.Instrumentation;
import android.net.Uri;
+import android.os.Build;
+import android.support.annotation.NonNull;
import android.support.test.InstrumentationRegistry;
import android.support.test.runner.AndroidJUnit4;
+import android.util.Log;
import org.fdroid.fdroid.AssetUtils;
+import org.fdroid.fdroid.RepoXMLHandler;
+import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.compat.FileCompatTest;
import org.fdroid.fdroid.data.Apk;
+import org.fdroid.fdroid.data.Repo;
+import org.fdroid.fdroid.mock.RepoDetails;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
@@ -40,10 +54,7 @@ import static org.junit.Assert.fail;
* This test checks the ApkVerifier by parsing a repo from permissionsRepo.xml
* and checking the listed permissions against the ones specified in apks' AndroidManifest,
* which have been specifically generated for this test.
- * - the apk file name must match the package name in the xml
- * - the versionName of listed apks inside the repo have either a good or bad outcome.
- * this must be defined in GOOD_VERSION_NAMES and BAD_VERSION_NAMES.
- *
+ *
* NOTE: This androidTest cannot run as a Robolectric test because the
* required methods from PackageManger are not included in Robolectric's Android API.
* java.lang.NoClassDefFoundError: java/util/jar/StrictJarFile
@@ -51,11 +62,14 @@ import static org.junit.Assert.fail;
*/
@RunWith(AndroidJUnit4.class)
public class ApkVerifierTest {
+ public static final String TAG = "ApkVerifierTest";
Instrumentation instrumentation;
File sdk14Apk;
File minMaxApk;
+ private File extendedPermissionsApk;
+ private File extendedPermsXml;
@Before
public void setUp() {
@@ -71,8 +85,18 @@ public class ApkVerifierTest {
"org.fdroid.permissions.minmax.apk",
dir
);
+ extendedPermissionsApk = AssetUtils.copyAssetToDir(instrumentation.getContext(),
+ "org.fdroid.extendedpermissionstest.apk",
+ dir
+ );
+ extendedPermsXml = AssetUtils.copyAssetToDir(instrumentation.getContext(),
+ "extendedPerms.xml",
+ dir
+ );
assertTrue(sdk14Apk.exists());
assertTrue(minMaxApk.exists());
+ assertTrue(extendedPermissionsApk.exists());
+ assertTrue(extendedPermsXml.exists());
}
@Test
@@ -80,7 +104,7 @@ public class ApkVerifierTest {
Apk apk = new Apk();
apk.packageName = "org.fdroid.permissions.sdk14";
apk.targetSdkVersion = 14;
- apk.permissions = new String[]{
+ String[] noPrefixPermissions = new String[]{
"AUTHENTICATE_ACCOUNTS",
"MANAGE_ACCOUNTS",
"READ_PROFILE",
@@ -98,9 +122,12 @@ public class ApkVerifierTest {
"WRITE_CALL_LOG", // implied-permission!
"READ_CALL_LOG", // implied-permission!
};
+ for (int i = 0; i < noPrefixPermissions.length; i++) {
+ noPrefixPermissions[i] = RepoXMLHandler.fdroidToAndroidPermission(noPrefixPermissions[i]);
+ }
+ apk.requestedPermissions = noPrefixPermissions;
Uri uri = Uri.fromFile(sdk14Apk);
-
ApkVerifier apkVerifier = new ApkVerifier(instrumentation.getContext(), uri, apk);
try {
@@ -111,12 +138,37 @@ public class ApkVerifierTest {
}
}
+ @Test(expected = ApkVerifier.ApkPermissionUnequalException.class)
+ public void testWithMinMax()
+ throws ApkVerifier.ApkPermissionUnequalException, ApkVerifier.ApkVerificationException {
+ Apk apk = new Apk();
+ apk.packageName = "org.fdroid.permissions.minmax";
+ apk.targetSdkVersion = 24;
+ ArrayList permissionsList = new ArrayList<>();
+ permissionsList.add("android.permission.READ_CALENDAR");
+ if (Build.VERSION.SDK_INT <= 18) {
+ permissionsList.add("android.permission.WRITE_EXTERNAL_STORAGE");
+ }
+ if (Build.VERSION.SDK_INT >= 23) {
+ permissionsList.add("android.permission.ACCESS_FINE_LOCATION");
+ }
+ apk.requestedPermissions = permissionsList.toArray(new String[permissionsList.size()]);
+
+ Uri uri = Uri.fromFile(minMaxApk);
+ ApkVerifier apkVerifier = new ApkVerifier(instrumentation.getContext(), uri, apk);
+ apkVerifier.verifyApk();
+
+ permissionsList.add("ADDITIONAL_PERMISSION");
+ apk.requestedPermissions = permissionsList.toArray(new String[permissionsList.size()]);
+ apkVerifier.verifyApk();
+ }
+
@Test
public void testWithPrefix() {
Apk apk = new Apk();
apk.packageName = "org.fdroid.permissions.sdk14";
apk.targetSdkVersion = 14;
- apk.permissions = new String[]{
+ apk.requestedPermissions = new String[]{
"android.permission.AUTHENTICATE_ACCOUNTS",
"android.permission.MANAGE_ACCOUNTS",
"android.permission.READ_PROFILE",
@@ -151,12 +203,13 @@ public class ApkVerifierTest {
* Additional permissions are okay. The user is simply
* warned about a permission that is not used inside the apk
*/
- @Test
- public void testAdditionalPermission() {
+ @Test(expected = ApkVerifier.ApkPermissionUnequalException.class)
+ public void testAdditionalPermission()
+ throws ApkVerifier.ApkPermissionUnequalException, ApkVerifier.ApkVerificationException {
Apk apk = new Apk();
apk.packageName = "org.fdroid.permissions.sdk14";
apk.targetSdkVersion = 14;
- apk.permissions = new String[]{
+ apk.requestedPermissions = new String[]{
"android.permission.AUTHENTICATE_ACCOUNTS",
"android.permission.MANAGE_ACCOUNTS",
"android.permission.READ_PROFILE",
@@ -173,19 +226,12 @@ public class ApkVerifierTest {
"android.permission.WRITE_SYNC_SETTINGS",
"android.permission.WRITE_CALL_LOG", // implied-permission!
"android.permission.READ_CALL_LOG", // implied-permission!
- "NEW_PERMISSION",
+ "android.permission.FAKE_NEW_PERMISSION",
};
Uri uri = Uri.fromFile(sdk14Apk);
-
ApkVerifier apkVerifier = new ApkVerifier(instrumentation.getContext(), uri, apk);
-
- try {
- apkVerifier.verifyApk();
- } catch (ApkVerifier.ApkVerificationException | ApkVerifier.ApkPermissionUnequalException e) {
- e.printStackTrace();
- fail(e.getMessage());
- }
+ apkVerifier.verifyApk();
}
/**
@@ -197,7 +243,7 @@ public class ApkVerifierTest {
Apk apk = new Apk();
apk.packageName = "org.fdroid.permissions.sdk14";
apk.targetSdkVersion = 14;
- apk.permissions = new String[]{
+ apk.requestedPermissions = new String[]{
//"android.permission.AUTHENTICATE_ACCOUNTS",
"android.permission.MANAGE_ACCOUNTS",
"android.permission.READ_PROFILE",
@@ -231,4 +277,73 @@ public class ApkVerifierTest {
}
}
+ @Test
+ public void testExtendedPerms() throws IOException,
+ ApkVerifier.ApkPermissionUnequalException, ApkVerifier.ApkVerificationException {
+ RepoDetails actualDetails = getFromFile(extendedPermsXml);
+ HashSet expectedSet = new HashSet<>(Arrays.asList(new String[]{
+ "android.permission.ACCESS_NETWORK_STATE",
+ "android.permission.ACCESS_WIFI_STATE",
+ "android.permission.INTERNET",
+ "android.permission.READ_SYNC_STATS",
+ "android.permission.READ_SYNC_SETTINGS",
+ "android.permission.WRITE_SYNC_SETTINGS",
+ "android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS",
+ "android.permission.READ_CONTACTS",
+ "android.permission.WRITE_CONTACTS",
+ "android.permission.READ_CALENDAR",
+ "android.permission.WRITE_CALENDAR",
+ }));
+ if (Build.VERSION.SDK_INT <= 18) {
+ expectedSet.add("android.permission.READ_EXTERNAL_STORAGE");
+ expectedSet.add("android.permission.WRITE_EXTERNAL_STORAGE");
+ }
+ if (Build.VERSION.SDK_INT <= 22) {
+ expectedSet.add("android.permission.GET_ACCOUNTS");
+ expectedSet.add("android.permission.AUTHENTICATE_ACCOUNTS");
+ expectedSet.add("android.permission.MANAGE_ACCOUNTS");
+ }
+ if (Build.VERSION.SDK_INT >= 23) {
+ expectedSet.add("android.permission.CAMERA");
+ if (Build.VERSION.SDK_INT <= 23) {
+ expectedSet.add("android.permission.CALL_PHONE");
+ }
+ }
+ Apk apk = actualDetails.apks.get(0);
+ HashSet actualSet = new HashSet<>(Arrays.asList(apk.requestedPermissions));
+ for (String permission : expectedSet) {
+ if (!actualSet.contains(permission)) {
+ Log.i(TAG, permission + " in expected but not actual! (android-"
+ + Build.VERSION.SDK_INT + ")");
+ }
+ }
+ for (String permission : actualSet) {
+ if (!expectedSet.contains(permission)) {
+ Log.i(TAG, permission + " in actual but not expected! (android-"
+ + Build.VERSION.SDK_INT + ")");
+ }
+ }
+ String[] expectedPermissions = expectedSet.toArray(new String[expectedSet.size()]);
+ assertTrue(ApkVerifier.requestedPermissionsEqual(expectedPermissions, apk.requestedPermissions));
+
+ String[] badPermissions = Arrays.copyOf(expectedPermissions, expectedPermissions.length + 1);
+ assertFalse(ApkVerifier.requestedPermissionsEqual(badPermissions, apk.requestedPermissions));
+ badPermissions[badPermissions.length - 1] = "notarealpermission";
+ assertFalse(ApkVerifier.requestedPermissionsEqual(badPermissions, apk.requestedPermissions));
+
+ Uri uri = Uri.fromFile(extendedPermissionsApk);
+ ApkVerifier apkVerifier = new ApkVerifier(instrumentation.getContext(), uri, apk);
+ apkVerifier.verifyApk();
+ }
+
+ @NonNull
+ private RepoDetails getFromFile(File indexFile) throws IOException {
+ InputStream inputStream = null;
+ try {
+ inputStream = new FileInputStream(indexFile);
+ return RepoDetails.getFromFile(inputStream, Repo.PUSH_REQUEST_IGNORE);
+ } finally {
+ Utils.closeQuietly(inputStream);
+ }
+ }
}
diff --git a/app/src/main/java/org/fdroid/fdroid/RepoXMLHandler.java b/app/src/main/java/org/fdroid/fdroid/RepoXMLHandler.java
index ec4cf5dcd..09a883591 100644
--- a/app/src/main/java/org/fdroid/fdroid/RepoXMLHandler.java
+++ b/app/src/main/java/org/fdroid/fdroid/RepoXMLHandler.java
@@ -19,6 +19,7 @@
package org.fdroid.fdroid;
+import android.os.Build;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
@@ -32,7 +33,9 @@ import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;
import java.util.ArrayList;
+import java.util.HashSet;
import java.util.List;
+import java.util.regex.Pattern;
/**
* Parses the index.xml into Java data structures.
@@ -57,7 +60,14 @@ public class RepoXMLHandler extends DefaultHandler {
private String repoDescription;
private String repoName;
- // the X.509 signing certificate stored in the header of index.xml
+ /**
+ * Set of requested permissions per package/APK
+ */
+ private final HashSet requestedPermissionsSet = new HashSet<>();
+
+ /**
+ * the X.509 signing certificate stored in the header of index.xml
+ */
private String repoSigningCert;
private final StringBuilder curchars = new StringBuilder();
@@ -89,6 +99,9 @@ public class RepoXMLHandler extends DefaultHandler {
if ("application".equals(localName) && curapp != null) {
onApplicationParsed();
} else if ("package".equals(localName) && curapk != null && curapp != null) {
+ int size = requestedPermissionsSet.size();
+ curapk.requestedPermissions = requestedPermissionsSet.toArray(new String[size]);
+ requestedPermissionsSet.clear();
apksList.add(curapk);
curapk = null;
} else if ("repo".equals(localName)) {
@@ -157,8 +170,8 @@ public class RepoXMLHandler extends DefaultHandler {
case ApkTable.Cols.ADDED_DATE:
curapk.added = Utils.parseDate(str, null);
break;
- case ApkTable.Cols.PERMISSIONS:
- curapk.permissions = Utils.parseCommaSeparatedString(str);
+ case "permissions": // together with
+ * More info into index - size, permissions, features, sdk version
+ */
+ public static String fdroidToAndroidPermission(String permission) {
+ if (OLD_FDROID_PERMISSION.matcher(permission).matches()) {
+ return "android.permission." + permission;
+ }
+
+ return permission;
+ }
+
+ private void addRequestedPermission(String permission) {
+ requestedPermissionsSet.add(permission);
+ }
+
+ private void addCommaSeparatedPermissions(String permissions) {
+ String[] array = Utils.parseCommaSeparatedString(permissions);
+ if (array != null) {
+ for (String permission : array) {
+ requestedPermissionsSet.add(fdroidToAndroidPermission(permission));
+ }
+ }
+ }
+
+ private void removeRequestedPermission(String permission) {
+ requestedPermissionsSet.remove(permission);
+ }
+
private void onApplicationParsed() {
receiver.receiveApp(curapp, apksList);
curapp = null;
@@ -308,6 +357,24 @@ public class RepoXMLHandler extends DefaultHandler {
} else if ("hash".equals(localName) && curapk != null) {
currentApkHashType = attributes.getValue("", "type");
+ } else if ("uses-permission".equals(localName) && curapk != null) {
+ String maxSdkVersion = attributes.getValue("maxSdkVersion");
+ if (maxSdkVersion == null || Build.VERSION.SDK_INT <= Integer.valueOf(maxSdkVersion)) {
+ addRequestedPermission(attributes.getValue("name"));
+ } else {
+ removeRequestedPermission(attributes.getValue("name"));
+ }
+ } else if ("uses-permission-sdk-23".equals(localName) && curapk != null) {
+ String maxSdkVersion = attributes.getValue("maxSdkVersion");
+ if (Build.VERSION.SDK_INT >= 23 &&
+ (maxSdkVersion == null || Build.VERSION.SDK_INT <= Integer.valueOf(maxSdkVersion))) {
+ addRequestedPermission(attributes.getValue("name"));
+ } else {
+ removeRequestedPermission(attributes.getValue("name"));
+ }
+ } else if ("uses-feature".equals(localName) && curapk != null) {
+ System.out.println("TODO startElement " + uri + " " + localName + " " + qName);
+ // TODO
}
curchars.setLength(0);
}
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 406966308..f43b0eeb0 100644
--- a/app/src/main/java/org/fdroid/fdroid/data/Apk.java
+++ b/app/src/main/java/org/fdroid/fdroid/data/Apk.java
@@ -7,11 +7,11 @@ import android.os.Build;
import android.os.Parcel;
import android.os.Parcelable;
+import org.fdroid.fdroid.RepoXMLHandler;
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;
@@ -36,8 +36,13 @@ public class Apk extends ValueObject implements Comparable, Parcelable {
public String obbPatchFile;
public String obbPatchFileSha256;
public Date added;
- public String[] permissions; // null if empty or
- // unknown
+ /**
+ * The array of the names of the permissions that this APK requests. This is the
+ * same data as {@link android.content.pm.PackageInfo#requestedPermissions}. Note this
+ * does not mean that all these permissions have been granted, only requested. For
+ * example, a regular app can request a system permission, but it won't be granted it.
+ */
+ public String[] requestedPermissions;
public String[] features; // null if empty or unknown
public String[] nativecode; // null if empty or unknown
@@ -126,8 +131,8 @@ public class Apk extends ValueObject implements Comparable, Parcelable {
case Cols.NAME:
apkName = cursor.getString(i);
break;
- case Cols.PERMISSIONS:
- permissions = Utils.parseCommaSeparatedString(cursor.getString(i));
+ case Cols.REQUESTED_PERMISSIONS:
+ requestedPermissions = convertToRequestedPermissions(cursor.getString(i));
break;
case Cols.NATIVE_CODE:
nativecode = Utils.parseCommaSeparatedString(cursor.getString(i));
@@ -230,44 +235,6 @@ public class Apk extends ValueObject implements Comparable, Parcelable {
return new File(App.getObbDir(packageName), obbPatchFile);
}
- public ArrayList getFullPermissionList() {
- if (this.permissions == null) {
- return new ArrayList<>();
- }
-
- ArrayList permissionsFull = new ArrayList<>();
- for (String perm : this.permissions) {
- permissionsFull.add(fdroidToAndroidPermission(perm));
- }
- return permissionsFull;
- }
-
- public String[] getFullPermissionsArray() {
- ArrayList fullPermissions = getFullPermissionList();
- return fullPermissions.toArray(new String[fullPermissions.size()]);
- }
-
- public HashSet getFullPermissionsSet() {
- return new HashSet<>(getFullPermissionList());
- }
-
- /**
- * It appears that the default Android permissions in android.Manifest.permissions
- * are prefixed with "android.permission." and then the constant name.
- * FDroid just includes the constant name in the apk list, so we prefix it
- * with "android.permission."
- *
- * @see
- * More info into index - size, permissions, features, sdk version
- */
- private static String fdroidToAndroidPermission(String permission) {
- if (!permission.contains(".")) {
- return "android.permission." + permission;
- }
-
- return permission;
- }
-
@Override
public String toString() {
return toContentValues().toString();
@@ -293,7 +260,7 @@ public class Apk extends ValueObject implements Comparable, Parcelable {
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.REQUESTED_PERMISSIONS, Utils.serializeCommaSeparatedString(requestedPermissions));
values.put(Cols.FEATURES, Utils.serializeCommaSeparatedString(features));
values.put(Cols.NATIVE_CODE, Utils.serializeCommaSeparatedString(nativecode));
values.put(Cols.INCOMPATIBLE_REASONS, Utils.serializeCommaSeparatedString(incompatibleReasons));
@@ -332,7 +299,7 @@ public class Apk extends ValueObject implements Comparable, Parcelable {
dest.writeString(this.obbPatchFile);
dest.writeString(this.obbPatchFileSha256);
dest.writeLong(this.added != null ? this.added.getTime() : -1);
- dest.writeStringArray(this.permissions);
+ dest.writeStringArray(this.requestedPermissions);
dest.writeStringArray(this.features);
dest.writeStringArray(this.nativecode);
dest.writeString(this.sig);
@@ -363,7 +330,7 @@ public class Apk extends ValueObject implements Comparable, Parcelable {
this.obbPatchFileSha256 = in.readString();
long tmpAdded = in.readLong();
this.added = tmpAdded == -1 ? null : new Date(tmpAdded);
- this.permissions = in.createStringArray();
+ this.requestedPermissions = in.createStringArray();
this.features = in.createStringArray();
this.nativecode = in.createStringArray();
this.sig = in.readString();
@@ -388,4 +355,17 @@ public class Apk extends ValueObject implements Comparable, Parcelable {
return new Apk[size];
}
};
+
+ private String[] convertToRequestedPermissions(String permissionsFromDb) {
+ String[] array = Utils.parseCommaSeparatedString(permissionsFromDb);
+ if (array != null) {
+ HashSet requestedPermissionsSet = new HashSet<>();
+ for (String permission : array) {
+ requestedPermissionsSet.add(RepoXMLHandler.fdroidToAndroidPermission(permission));
+ }
+ return requestedPermissionsSet.toArray(new String[requestedPermissionsSet.size()]);
+ }
+ return null;
+ }
+
}
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 e7d96ba1d..9bce69640 100644
--- a/app/src/main/java/org/fdroid/fdroid/data/App.java
+++ b/app/src/main/java/org/fdroid/fdroid/data/App.java
@@ -372,7 +372,7 @@ public class App extends ValueObject implements Comparable, Parcelable {
apk.targetSdkVersion = minTargetMax[1];
apk.maxSdkVersion = minTargetMax[2];
apk.packageName = this.packageName;
- apk.permissions = packageInfo.requestedPermissions;
+ apk.requestedPermissions = packageInfo.requestedPermissions;
apk.apkName = apk.packageName + "_" + apk.versionCode + ".apk";
apk.installedFile = apkFile;
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 aaee174b4..0a2125425 100644
--- a/app/src/main/java/org/fdroid/fdroid/data/DBHelper.java
+++ b/app/src/main/java/org/fdroid/fdroid/data/DBHelper.java
@@ -96,7 +96,7 @@ class DBHelper extends SQLiteOpenHelper {
+ 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.REQUESTED_PERMISSIONS + " string, "
+ ApkTable.Cols.FEATURES + " string, "
+ ApkTable.Cols.NATIVE_CODE + " string, "
+ ApkTable.Cols.HASH_TYPE + " string, "
@@ -504,7 +504,7 @@ class DBHelper extends SQLiteOpenHelper {
+ ApkTable.Cols.MIN_SDK_VERSION + " integer, "
+ ApkTable.Cols.TARGET_SDK_VERSION + " integer, "
+ ApkTable.Cols.MAX_SDK_VERSION + " integer, "
- + ApkTable.Cols.PERMISSIONS + " string, "
+ + ApkTable.Cols.REQUESTED_PERMISSIONS + " string, "
+ ApkTable.Cols.FEATURES + " string, "
+ ApkTable.Cols.NATIVE_CODE + " string, "
+ ApkTable.Cols.HASH_TYPE + " string, "
@@ -529,7 +529,7 @@ class DBHelper extends SQLiteOpenHelper {
ApkTable.Cols.MIN_SDK_VERSION,
ApkTable.Cols.TARGET_SDK_VERSION,
ApkTable.Cols.MAX_SDK_VERSION,
- ApkTable.Cols.PERMISSIONS,
+ ApkTable.Cols.REQUESTED_PERMISSIONS,
ApkTable.Cols.FEATURES,
ApkTable.Cols.NATIVE_CODE,
ApkTable.Cols.HASH_TYPE,
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 b910c107f..8f39d62ed 100644
--- a/app/src/main/java/org/fdroid/fdroid/data/Schema.java
+++ b/app/src/main/java/org/fdroid/fdroid/data/Schema.java
@@ -169,7 +169,7 @@ public interface Schema {
String OBB_MAIN_FILE_SHA256 = "obbMainFileSha256";
String OBB_PATCH_FILE = "obbPatchFile";
String OBB_PATCH_FILE_SHA256 = "obbPatchFileSha256";
- String PERMISSIONS = "permissions";
+ String REQUESTED_PERMISSIONS = "permissions";
String FEATURES = "features";
String NATIVE_CODE = "nativecode";
String HASH_TYPE = "hashType";
@@ -193,7 +193,7 @@ public interface Schema {
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,
+ REQUESTED_PERMISSIONS, FEATURES, NATIVE_CODE, HASH_TYPE, ADDED_DATE,
IS_COMPATIBLE, INCOMPATIBLE_REASONS,
};
@@ -204,7 +204,7 @@ public interface Schema {
_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,
+ REQUESTED_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/ApkVerifier.java b/app/src/main/java/org/fdroid/fdroid/installer/ApkVerifier.java
index 76795df29..ea06fe437 100644
--- a/app/src/main/java/org/fdroid/fdroid/installer/ApkVerifier.java
+++ b/app/src/main/java/org/fdroid/fdroid/installer/ApkVerifier.java
@@ -80,18 +80,10 @@ class ApkVerifier {
}
// verify permissions, important for unattended installer
- HashSet localPermissions = getLocalPermissionsSet(localApkInfo);
- HashSet expectedPermissions = expectedApk.getFullPermissionsSet();
- Utils.debugLog(TAG, "localPermissions: " + localPermissions);
- Utils.debugLog(TAG, "expectedPermissions: " + expectedPermissions);
- // NOTE: Some permissions could have a maxSdkVersion < current sdk version
- // and are thus not parsed by pm.getPackageArchiveInfo().
- // Thus, containsAll() instead of equals() is used!
- // See also https://gitlab.com/fdroid/fdroidclient/issues/703
- if (!expectedPermissions.containsAll(localPermissions)) {
- throw new ApkPermissionUnequalException(
- "Permissions of the apk file are not a true subset of the permissions listed by the repo," +
- " i.e., some permissions have not been shown to the user!");
+ Utils.debugLog(TAG, "localPermissions: " + TextUtils.join("\n", localApkInfo.requestedPermissions));
+ Utils.debugLog(TAG, "expectedPermissions: " + TextUtils.join("\n", expectedApk.requestedPermissions));
+ if (!requestedPermissionsEqual(expectedApk.requestedPermissions, localApkInfo.requestedPermissions)) {
+ throw new ApkPermissionUnequalException("Permissions in APK and index.xml do not match!");
}
int localTargetSdkVersion = localApkInfo.applicationInfo.targetSdkVersion;
@@ -106,13 +98,24 @@ class ApkVerifier {
}
}
- private HashSet getLocalPermissionsSet(PackageInfo localApkInfo) {
- String[] localPermissions = localApkInfo.requestedPermissions;
- if (localPermissions == null) {
- return new HashSet<>();
+ /**
+ * Compares to sets of APK permissions to see if they are an exact match. The
+ * data format is {@link String} arrays but they are in effect sets. This is the
+ * same data format as {@link android.content.pm.PackageInfo#requestedPermissions}
+ */
+ public static boolean requestedPermissionsEqual(String[] expected, String[] actual) {
+ if (expected == null && actual == null) {
+ return true;
}
-
- return new HashSet<>(Arrays.asList(localApkInfo.requestedPermissions));
+ if (expected == null || actual == null) {
+ return false;
+ }
+ if (expected.length != actual.length) {
+ return false;
+ }
+ HashSet expectedSet = new HashSet<>(Arrays.asList(expected));
+ HashSet actualSet = new HashSet<>(Arrays.asList(actual));
+ return expectedSet.equals(actualSet);
}
public static class ApkVerificationException extends Exception {
diff --git a/app/src/main/java/org/fdroid/fdroid/localrepo/LocalRepoManager.java b/app/src/main/java/org/fdroid/fdroid/localrepo/LocalRepoManager.java
index 6e8db5e3d..7d66f08a9 100644
--- a/app/src/main/java/org/fdroid/fdroid/localrepo/LocalRepoManager.java
+++ b/app/src/main/java/org/fdroid/fdroid/localrepo/LocalRepoManager.java
@@ -436,10 +436,10 @@ public final class LocalRepoManager {
private void tagPermissions(App app) throws IOException {
serializer.startTag("", "permissions");
- if (app.installedApk.permissions != null) {
+ if (app.installedApk.requestedPermissions != null) {
StringBuilder buff = new StringBuilder();
- for (String permission : app.installedApk.permissions) {
+ for (String permission : app.installedApk.requestedPermissions) {
buff.append(permission.replace("android.permission.", ""));
buff.append(',');
}
diff --git a/app/src/main/java/org/fdroid/fdroid/privileged/views/AppDiff.java b/app/src/main/java/org/fdroid/fdroid/privileged/views/AppDiff.java
index f006a90a5..036a78533 100644
--- a/app/src/main/java/org/fdroid/fdroid/privileged/views/AppDiff.java
+++ b/app/src/main/java/org/fdroid/fdroid/privileged/views/AppDiff.java
@@ -39,7 +39,7 @@ public class AppDiff {
pkgInfo = new PackageInfo();
pkgInfo.packageName = apk.packageName;
pkgInfo.applicationInfo = new ApplicationInfo();
- pkgInfo.requestedPermissions = apk.getFullPermissionsArray();
+ pkgInfo.requestedPermissions = apk.requestedPermissions;
init();
}
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 193c8dc23..b14ec574c 100644
--- a/app/src/test/java/org/fdroid/fdroid/updater/RepoXMLHandlerTest.java
+++ b/app/src/test/java/org/fdroid/fdroid/updater/RepoXMLHandlerTest.java
@@ -28,23 +28,18 @@ 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;
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.fdroid.fdroid.mock.RepoDetails;
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;
@@ -55,16 +50,11 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
-import javax.xml.parsers.ParserConfigurationException;
-import javax.xml.parsers.SAXParser;
-import javax.xml.parsers.SAXParserFactory;
-
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
// TODO: Use sdk=24 when Robolectric supports this
@Config(constants = BuildConfig.class, sdk = 23)
@@ -79,6 +69,17 @@ public class RepoXMLHandlerTest {
ShadowLog.stream = System.out;
}
+ @Test
+ public void testExtendedPerms() throws IOException {
+ Repo expectedRepo = new Repo();
+ expectedRepo.name = "F-Droid";
+ expectedRepo.signingCertificate = "3082035e30820246a00302010202044c49cd00300d06092a864886f70d01010505003071310b300906035504061302554b3110300e06035504081307556e6b6e6f776e3111300f0603550407130857657468657262793110300e060355040a1307556e6b6e6f776e3110300e060355040b1307556e6b6e6f776e311930170603550403131043696172616e2047756c746e69656b73301e170d3130303732333137313032345a170d3337313230383137313032345a3071310b300906035504061302554b3110300e06035504081307556e6b6e6f776e3111300f0603550407130857657468657262793110300e060355040a1307556e6b6e6f776e3110300e060355040b1307556e6b6e6f776e311930170603550403131043696172616e2047756c746e69656b7330820122300d06092a864886f70d01010105000382010f003082010a028201010096d075e47c014e7822c89fd67f795d23203e2a8843f53ba4e6b1bf5f2fd0e225938267cfcae7fbf4fe596346afbaf4070fdb91f66fbcdf2348a3d92430502824f80517b156fab00809bdc8e631bfa9afd42d9045ab5fd6d28d9e140afc1300917b19b7c6c4df4a494cf1f7cb4a63c80d734265d735af9e4f09455f427aa65a53563f87b336ca2c19d244fcbba617ba0b19e56ed34afe0b253ab91e2fdb1271f1b9e3c3232027ed8862a112f0706e234cf236914b939bcf959821ecb2a6c18057e070de3428046d94b175e1d89bd795e535499a091f5bc65a79d539a8d43891ec504058acb28c08393b5718b57600a211e803f4a634e5c57f25b9b8c4422c6fd90203010001300d06092a864886f70d0101050500038201010008e4ef699e9807677ff56753da73efb2390d5ae2c17e4db691d5df7a7b60fc071ae509c5414be7d5da74df2811e83d3668c4a0b1abc84b9fa7d96b4cdf30bba68517ad2a93e233b042972ac0553a4801c9ebe07bf57ebe9a3b3d6d663965260e50f3b8f46db0531761e60340a2bddc3426098397fda54044a17e5244549f9869b460ca5e6e216b6f6a2db0580b480ca2afe6ec6b46eedacfa4aa45038809ece0c5978653d6c85f678e7f5a2156d1bedd8117751e64a4b0dcd140f3040b021821a8d93aed8d01ba36db6c82372211fed714d9a32607038cdfd565bd529ffc637212aaa2c224ef22b603eccefb5bf1e085c191d4b24fe742b17ab3f55d4e6f05ef";
+ expectedRepo.description = "This is just a test of the extended permissions attributes.";
+ expectedRepo.timestamp = 1467169032;
+ RepoDetails actualDetails = getFromFile("extendedPerms.xml");
+ handlerTestSuite(expectedRepo, actualDetails, 2, 6, 14, 16);
+ }
+
@Test
public void testObbIndex() throws IOException {
writeResourceToObbDir("main.1101613.obb.main.twoversions.obb");
@@ -835,41 +836,6 @@ public class RepoXMLHandlerTest {
assertEquals(apks.size(), apkCount);
}
- private static class RepoDetails implements RepoXMLHandler.IndexReceiver {
-
- public String name;
- public String description;
- public String signingCert;
- public int maxAge;
- public int version;
- public long timestamp;
-
- public List apks = new ArrayList<>();
- public List apps = new ArrayList<>();
- public List repoPushRequestList = new ArrayList<>();
-
- @Override
- public void receiveRepo(String name, String description, String signingCert, int maxage, int version, long timestamp) {
- this.name = name;
- this.description = description;
- this.signingCert = signingCert;
- this.maxAge = maxage;
- this.version = version;
- this.timestamp = timestamp;
- }
-
- @Override
- public void receiveApp(App app, List packages) {
- apks.addAll(packages);
- apps.add(app);
- }
-
- @Override
- public void receiveRepoPushRequest(RepoPushRequest repoPushRequest) {
- repoPushRequestList.add(repoPushRequest);
- }
- }
-
@NonNull
private RepoDetails getFromFile(String indexFilename) {
return getFromFile(indexFilename, Repo.PUSH_REQUEST_IGNORE);
@@ -877,28 +843,9 @@ public class RepoXMLHandlerTest {
@NonNull
private RepoDetails getFromFile(String indexFilename, int pushRequests) {
- try {
- SAXParserFactory factory = SAXParserFactory.newInstance();
- factory.setNamespaceAware(true);
- SAXParser parser = factory.newSAXParser();
- XMLReader reader = parser.getXMLReader();
- RepoDetails repoDetails = new RepoDetails();
- MockRepo mockRepo = new MockRepo(100, pushRequests);
- RepoXMLHandler handler = new RepoXMLHandler(mockRepo, repoDetails);
- reader.setContentHandler(handler);
- Log.i(TAG, "test file: " + getClass().getClassLoader().getResource(indexFilename));
- InputStream input = getClass().getClassLoader().getResourceAsStream(indexFilename);
- InputSource is = new InputSource(new BufferedInputStream(input));
- reader.parse(is);
- return repoDetails;
- } catch (ParserConfigurationException | SAXException | IOException e) {
- e.printStackTrace();
- fail();
-
- // Satisfies the compiler, but fail() will always throw a runtime exception so we never
- // reach this return statement.
- return null;
- }
+ Log.i(TAG, "test file: " + getClass().getClassLoader().getResource(indexFilename));
+ InputStream inputStream = getClass().getClassLoader().getResourceAsStream(indexFilename);
+ return RepoDetails.getFromFile(inputStream, pushRequests);
}
private void writeResourceToObbDir(String assetName) throws IOException {
diff --git a/app/src/test/resources/extendedPerms.xml b/app/src/test/resources/extendedPerms.xml
new file mode 100644
index 000000000..4329e53c9
--- /dev/null
+++ b/app/src/test/resources/extendedPerms.xml
@@ -0,0 +1,195 @@
+
+
+
+
+ This is just a test of the extended permissions attributes.
+
+
+
+
+ at.bitfire.davdroid
+ 2013-10-13
+ 2016-09-21
+ DAVdroid
+ Contacts and Calendar sync
+ at.bitfire.davdroid.116.png
+
+
DAVdroid is a CalDAV/CardDAV synchronisation adapter for Android 4+ devices.
+
Use it with your own server (likeOwnCloud,Baïkal,
+ DAViCal
+ orradiCALe) or with a trusted hoster to keep your
+ contacts and events under your control.
+
+
Integrates natively in Android calendar/contact apps. See homepage for configuration
+ details, including info about self-signed certificates.
+
+
For a comparison of server software, see the
+ Debian wiki.
+
Asks you questions about a picture of a galaxy, with each question depending on the
+ previous question. This "Citizen Science" helps astronomers to analyze the huge
+ amount of images of galaxies provided, for instance, by the Hubble Space Telescope.
+
+
+ GPLv3
+ Science & Education
+ Science & Education
+
+ https://github.com/murraycu/android-galaxyzoo
+ https://github.com/murraycu/android-galaxyzoo/issues
+ 1.64
+ 64
+
+ 1.64
+ 64
+ com.murrayc.galaxyzoo.app_64.apk
+ com.murrayc.galaxyzoo.app_64_src.tar.gz
+ 6f10487c8ef84078232aafe115228c6252ef982a228666454005b8d62c27fe42
+
+ f1a84be1ce965e270f64318cfdb41861
+ 4095584
+ 11
+ 24
+ 2016-09-02
+
+ READ_EXTERNAL_STORAGE,WRITE_SYNC_SETTINGS,ACCESS_NETWORK_STATE,WRITE_EXTERNAL_STORAGE,USE_CREDENTIALS,READ_SYNC_SETTINGS,MANAGE_ACCOUNTS,INTERNET,AUTHENTICATE_ACCOUNTS,GET_ACCOUNTS
+
+
+
+
+
+
+
+ 1.61
+ 61
+ com.murrayc.galaxyzoo.app_61.apk
+ com.murrayc.galaxyzoo.app_61_src.tar.gz
+ c35bc15885d57a2ddfcb2c702c9147b63b63a9765a6eeacf98d6a1534f6f8ff2
+
+ f1a84be1ce965e270f64318cfdb41861
+ 4588315
+ 11
+ 24
+ 2016-08-26
+
+ READ_EXTERNAL_STORAGE,WRITE_SYNC_SETTINGS,ACCESS_NETWORK_STATE,WRITE_EXTERNAL_STORAGE,USE_CREDENTIALS,READ_SYNC_SETTINGS,MANAGE_ACCOUNTS,INTERNET,AUTHENTICATE_ACCOUNTS,GET_ACCOUNTS
+
+
+
+
+
+
+
+ 1.59
+ 59
+ com.murrayc.galaxyzoo.app_59.apk
+ com.murrayc.galaxyzoo.app_59_src.tar.gz
+ 719d1440dd2f16ad29b45edba5b43262f22ef68cee0c6b478e40fe8eb0e53227
+
+ f1a84be1ce965e270f64318cfdb41861
+ 4116175
+ 11
+ 24
+ 2016-08-25
+
+ READ_EXTERNAL_STORAGE,WRITE_SYNC_SETTINGS,ACCESS_NETWORK_STATE,WRITE_EXTERNAL_STORAGE,USE_CREDENTIALS,READ_SYNC_SETTINGS,MANAGE_ACCOUNTS,INTERNET,AUTHENTICATE_ACCOUNTS,GET_ACCOUNTS
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/test/java/org/fdroid/fdroid/mock/MockRepo.java b/app/src/testShared/java/org/fdroid/fdroid/mock/MockRepo.java
similarity index 100%
rename from app/src/test/java/org/fdroid/fdroid/mock/MockRepo.java
rename to app/src/testShared/java/org/fdroid/fdroid/mock/MockRepo.java
diff --git a/app/src/testShared/java/org/fdroid/fdroid/mock/RepoDetails.java b/app/src/testShared/java/org/fdroid/fdroid/mock/RepoDetails.java
new file mode 100644
index 000000000..4371bbaa4
--- /dev/null
+++ b/app/src/testShared/java/org/fdroid/fdroid/mock/RepoDetails.java
@@ -0,0 +1,85 @@
+package org.fdroid.fdroid.mock;
+
+import android.support.annotation.NonNull;
+
+import org.fdroid.fdroid.RepoXMLHandler;
+import org.fdroid.fdroid.data.Apk;
+import org.fdroid.fdroid.data.App;
+import org.fdroid.fdroid.data.RepoPushRequest;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+import org.xml.sax.XMLReader;
+
+import java.io.BufferedInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.parsers.SAXParser;
+import javax.xml.parsers.SAXParserFactory;
+
+import static org.junit.Assert.fail;
+
+public class RepoDetails implements RepoXMLHandler.IndexReceiver {
+ public static final String TAG = "RepoDetails";
+
+ public String name;
+ public String description;
+ public String signingCert;
+ public int maxAge;
+ public int version;
+ public long timestamp;
+
+ public List apks = new ArrayList<>();
+ public List apps = new ArrayList<>();
+ public List repoPushRequestList = new ArrayList<>();
+
+ @Override
+ public void receiveRepo(String name, String description, String signingCert, int maxage, int version, long timestamp) {
+ this.name = name;
+ this.description = description;
+ this.signingCert = signingCert;
+ this.maxAge = maxage;
+ this.version = version;
+ this.timestamp = timestamp;
+ }
+
+ @Override
+ public void receiveApp(App app, List packages) {
+ apks.addAll(packages);
+ apps.add(app);
+ }
+
+ @Override
+ public void receiveRepoPushRequest(RepoPushRequest repoPushRequest) {
+ repoPushRequestList.add(repoPushRequest);
+ }
+
+ @NonNull
+ public static RepoDetails getFromFile(InputStream inputStream, int pushRequests) {
+ try {
+ SAXParserFactory factory = SAXParserFactory.newInstance();
+ factory.setNamespaceAware(true);
+ SAXParser parser = factory.newSAXParser();
+ XMLReader reader = parser.getXMLReader();
+ RepoDetails repoDetails = new RepoDetails();
+ MockRepo mockRepo = new MockRepo(100, pushRequests);
+ RepoXMLHandler handler = new RepoXMLHandler(mockRepo, repoDetails);
+ reader.setContentHandler(handler);
+ InputSource is = new InputSource(new BufferedInputStream(inputStream));
+ reader.parse(is);
+ return repoDetails;
+ } catch (ParserConfigurationException | SAXException | IOException e) {
+ e.printStackTrace();
+ fail();
+
+ // Satisfies the compiler, but fail() will always throw a runtime exception so we never
+ // reach this return statement.
+ return null;
+ }
+ }
+
+}
+