Merge branch 'support-extended-permissions' into 'master'

Support extended <uses-permissions/> tags

`<uses-permission-sdk-23>` is a new tag for requesting permissions on _android-23_ and above.  `<uses-permission/>` and `<uses-permission-sdk-23>` both can have optional _maxSdkVersion_ values, and these need to be fully supported in order for the Privileged Extension to be able to check the APK permissions against the index.xml permissions.  This is needed so that it can prompt the user once, then download and install the APK after that.  Its also needed for transparent background updates to check if the permissions have changed in the update.

This gets closer to the complete support, really the full permissions should be stored in the database.  Then the only
conversion would happen ewhen parsing the XML.  Right now, it still stores the old F-Droid permissions names, i.e. without
`android.permission.`.  Then those are converted when they are loaded from the DB into an Apk instance.

See merge request !402
This commit is contained in:
Hans-Christoph Steiner 2016-10-11 07:00:00 +00:00
commit 53e8061ef3
16 changed files with 692 additions and 165 deletions

View File

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

View File

@ -0,0 +1,125 @@
<?xml version="1.0" encoding="utf-8"?>
<fdroid>
<repo name="F-Droid" icon="fdroid-icon.png" maxage="14"
pubkey="3082035e30820246a00302010202044c49cd00300d06092a864886f70d01010505003071310b300906035504061302554b3110300e06035504081307556e6b6e6f776e3111300f0603550407130857657468657262793110300e060355040a1307556e6b6e6f776e3110300e060355040b1307556e6b6e6f776e311930170603550403131043696172616e2047756c746e69656b73301e170d3130303732333137313032345a170d3337313230383137313032345a3071310b300906035504061302554b3110300e06035504081307556e6b6e6f776e3111300f0603550407130857657468657262793110300e060355040a1307556e6b6e6f776e3110300e060355040b1307556e6b6e6f776e311930170603550403131043696172616e2047756c746e69656b7330820122300d06092a864886f70d01010105000382010f003082010a028201010096d075e47c014e7822c89fd67f795d23203e2a8843f53ba4e6b1bf5f2fd0e225938267cfcae7fbf4fe596346afbaf4070fdb91f66fbcdf2348a3d92430502824f80517b156fab00809bdc8e631bfa9afd42d9045ab5fd6d28d9e140afc1300917b19b7c6c4df4a494cf1f7cb4a63c80d734265d735af9e4f09455f427aa65a53563f87b336ca2c19d244fcbba617ba0b19e56ed34afe0b253ab91e2fdb1271f1b9e3c3232027ed8862a112f0706e234cf236914b939bcf959821ecb2a6c18057e070de3428046d94b175e1d89bd795e535499a091f5bc65a79d539a8d43891ec504058acb28c08393b5718b57600a211e803f4a634e5c57f25b9b8c4422c6fd90203010001300d06092a864886f70d0101050500038201010008e4ef699e9807677ff56753da73efb2390d5ae2c17e4db691d5df7a7b60fc071ae509c5414be7d5da74df2811e83d3668c4a0b1abc84b9fa7d96b4cdf30bba68517ad2a93e233b042972ac0553a4801c9ebe07bf57ebe9a3b3d6d663965260e50f3b8f46db0531761e60340a2bddc3426098397fda54044a17e5244549f9869b460ca5e6e216b6f6a2db0580b480ca2afe6ec6b46eedacfa4aa45038809ece0c5978653d6c85f678e7f5a2156d1bedd8117751e64a4b0dcd140f3040b021821a8d93aed8d01ba36db6c82372211fed714d9a32607038cdfd565bd529ffc637212aaa2c224ef22b603eccefb5bf1e085c191d4b24fe742b17ab3f55d4e6f05ef"
timestamp="1467169032" url="http://f-droid.org/repo" version="16">
<description>
This is just a test of the extended permissions attributes.
</description>
</repo>
<application id="urzip.at.or.at.urzip">
<id>at.bitfire.davdroid</id>
<added>2013-10-13</added>
<lastupdated>2016-06-26</lastupdated>
<name>DAVdroid</name>
<summary>Contacts and Calendar sync</summary>
<icon>at.bitfire.davdroid.107.png</icon>
<desc>apk generated from urzip to test extended permissions</desc>
<license>GPLv3</license>
<categories>Internet</categories>
<category>Internet</category>
<web>https://davdroid.bitfire.at/</web>
<source>https://davdroid.bitfire.at/source/</source>
<tracker>https://davdroid.bitfire.at/forums/</tracker>
<changelog>https://gitlab.com/bitfireAT/davdroid/tags</changelog>
<donate>https://davdroid.bitfire.at/donate/</donate>
<bitcoin>1KSCy7RHztKuhW9fLLaUYqdwdC2iwbejZU</bitcoin>
<flattr>2100160</flattr>
<marketversion>1.1.1.2</marketversion>
<marketvercode>107</marketvercode>
<package>
<version>1.3.2-FAKE</version>
<versioncode>117</versioncode>
<apkname>org.fdroid.extendedpermissionstest.apk</apkname>
<srcname>at.bitfire.davdroid_116_src.tar.gz</srcname>
<hash type="sha256">f1aa02257e99c167d2ea9b0e9525c3ce7c181fe2e7f4dd00b65dd81ed2e27a62
</hash>
<sig>03542175324d067b4c36582242f8aecc</sig>
<size>3298864</size>
<sdkver>14</sdkver>
<targetSdkVersion>23</targetSdkVersion>
<added>2016-09-22</added>
<permissions>
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
</permissions>
<uses-permission name="android.permission.GET_ACCOUNTS" maxSdkVersion="22" />
<uses-permission name="android.permission.READ_EXTERNAL_STORAGE" maxSdkVersion="18" />
<uses-permission name="android.permission.WRITE_EXTERNAL_STORAGE" maxSdkVersion="18" />
<uses-permission name="android.permission.AUTHENTICATE_ACCOUNTS" maxSdkVersion="22" />
<uses-permission name="android.permission.MANAGE_ACCOUNTS" maxSdkVersion="22" />
<uses-permission-sdk-23 name="android.permission.CAMERA" />
<uses-permission-sdk-23 name="android.permission.CALL_PHONE" maxSdkVersion="23" />
</package>
<package>
<version>1.3.1-ose</version>
<versioncode>116</versioncode>
<apkname>at.bitfire.davdroid_116.apk</apkname>
<srcname>at.bitfire.davdroid_116_src.tar.gz</srcname>
<hash type="sha256">f1aa02257e99c167d2ea9b0e9525c3ce7c181fe2e7f4dd00b65dd81ed2e27a62
</hash>
<sig>03542175324d067b4c36582242f8aecc</sig>
<size>3298864</size>
<sdkver>14</sdkver>
<targetSdkVersion>24</targetSdkVersion>
<added>2016-09-21</added>
<permissions>
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
</permissions>
<uses-permission name="android.permission.GET_ACCOUNTS" maxSdkVersion="22" />
<uses-permission name="android.permission.READ_EXTERNAL_STORAGE" maxSdkVersion="18" />
<uses-permission name="android.permission.WRITE_EXTERNAL_STORAGE" maxSdkVersion="18" />
<uses-permission name="android.permission.AUTHENTICATE_ACCOUNTS" maxSdkVersion="22" />
<uses-permission name="android.permission.MANAGE_ACCOUNTS" maxSdkVersion="22" />
</package>
<package>
<version>1.1.1.2</version>
<versioncode>107</versioncode>
<apkname>at.bitfire.davdroid_107.apk</apkname>
<srcname>at.bitfire.davdroid_107_src.tar.gz</srcname>
<hash type="sha256">9a616f2e97bf8cf012baf896f95667dea4e3ce3252b31c5715073638a9fcc3d4
</hash>
<sig>03542175324d067b4c36582242f8aecc</sig>
<size>3134363</size>
<sdkver>14</sdkver>
<targetSdkVersion>23</targetSdkVersion>
<added>2016-06-26</added>
<permissions>
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
</permissions>
</package>
<package>
<version>1.1.1.1</version>
<versioncode>105</versioncode>
<apkname>at.bitfire.davdroid_105.apk</apkname>
<srcname>at.bitfire.davdroid_105_src.tar.gz</srcname>
<hash type="sha256">4a0408c61536a1cc1028cea4273adbde2e57dfa2b12d93c3b52f4c3d095e2849
</hash>
<sig>03542175324d067b4c36582242f8aecc</sig>
<size>3131567</size>
<sdkver>14</sdkver>
<targetSdkVersion>23</targetSdkVersion>
<added>2016-06-24</added>
<permissions>
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
</permissions>
</package>
<package>
<version>1.1.1</version>
<versioncode>104</versioncode>
<apkname>at.bitfire.davdroid_104.apk</apkname>
<srcname>at.bitfire.davdroid_104_src.tar.gz</srcname>
<hash type="sha256">09ba34996177efe8b1498a93fe6521ab84efab3bccb0f42449116e80b59e5b56
</hash>
<sig>03542175324d067b4c36582242f8aecc</sig>
<size>3131367</size>
<sdkver>14</sdkver>
<targetSdkVersion>23</targetSdkVersion>
<added>2016-06-22</added>
<permissions>
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
</permissions>
</package>
</application>
</fdroid>

View File

@ -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.
* <p/>
* <p>
* 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<String> 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<String> 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<String> 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);
}
}
}

View File

@ -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<String> 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 <uses-permissions* makes ApkTable.Cols.REQUESTED_PERMISSIONS
addCommaSeparatedPermissions(str);
break;
case ApkTable.Cols.FEATURES:
curapk.features = Utils.parseCommaSeparatedString(str);
@ -248,6 +261,42 @@ public class RepoXMLHandler extends DefaultHandler {
}
}
private static final Pattern OLD_FDROID_PERMISSION = Pattern.compile("[A-Z_]+");
/**
* 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 <a href="https://gitlab.com/fdroid/fdroidserver/blob/1afa8cfc/update.py#L91">
* More info into index - size, permissions, features, sdk version</a>
*/
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);
}

View File

@ -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<Apk>, 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<Apk>, 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<Apk>, Parcelable {
return new File(App.getObbDir(packageName), obbPatchFile);
}
public ArrayList<String> getFullPermissionList() {
if (this.permissions == null) {
return new ArrayList<>();
}
ArrayList<String> permissionsFull = new ArrayList<>();
for (String perm : this.permissions) {
permissionsFull.add(fdroidToAndroidPermission(perm));
}
return permissionsFull;
}
public String[] getFullPermissionsArray() {
ArrayList<String> fullPermissions = getFullPermissionList();
return fullPermissions.toArray(new String[fullPermissions.size()]);
}
public HashSet<String> 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 <a href="https://gitlab.com/fdroid/fdroidserver/blob/1afa8cfc/update.py#L91">
* More info into index - size, permissions, features, sdk version</a>
*/
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<Apk>, 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<Apk>, 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<Apk>, 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<Apk>, Parcelable {
return new Apk[size];
}
};
private String[] convertToRequestedPermissions(String permissionsFromDb) {
String[] array = Utils.parseCommaSeparatedString(permissionsFromDb);
if (array != null) {
HashSet<String> requestedPermissionsSet = new HashSet<>();
for (String permission : array) {
requestedPermissionsSet.add(RepoXMLHandler.fdroidToAndroidPermission(permission));
}
return requestedPermissionsSet.toArray(new String[requestedPermissionsSet.size()]);
}
return null;
}
}

View File

@ -372,7 +372,7 @@ public class App extends ValueObject implements Comparable<App>, 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;

View File

@ -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,

View File

@ -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,
};
}

View File

@ -80,18 +80,10 @@ class ApkVerifier {
}
// verify permissions, important for unattended installer
HashSet<String> localPermissions = getLocalPermissionsSet(localApkInfo);
HashSet<String> 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<String> 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<String> expectedSet = new HashSet<>(Arrays.asList(expected));
HashSet<String> actualSet = new HashSet<>(Arrays.asList(actual));
return expectedSet.equals(actualSet);
}
public static class ApkVerificationException extends Exception {

View File

@ -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(',');
}

View File

@ -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();
}

View File

@ -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<Apk> apks = new ArrayList<>();
public List<App> apps = new ArrayList<>();
public List<RepoPushRequest> 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<Apk> 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 {

View File

@ -0,0 +1,195 @@
<?xml version="1.0" encoding="utf-8"?>
<fdroid>
<repo name="F-Droid" icon="fdroid-icon.png" maxage="14"
pubkey="3082035e30820246a00302010202044c49cd00300d06092a864886f70d01010505003071310b300906035504061302554b3110300e06035504081307556e6b6e6f776e3111300f0603550407130857657468657262793110300e060355040a1307556e6b6e6f776e3110300e060355040b1307556e6b6e6f776e311930170603550403131043696172616e2047756c746e69656b73301e170d3130303732333137313032345a170d3337313230383137313032345a3071310b300906035504061302554b3110300e06035504081307556e6b6e6f776e3111300f0603550407130857657468657262793110300e060355040a1307556e6b6e6f776e3110300e060355040b1307556e6b6e6f776e311930170603550403131043696172616e2047756c746e69656b7330820122300d06092a864886f70d01010105000382010f003082010a028201010096d075e47c014e7822c89fd67f795d23203e2a8843f53ba4e6b1bf5f2fd0e225938267cfcae7fbf4fe596346afbaf4070fdb91f66fbcdf2348a3d92430502824f80517b156fab00809bdc8e631bfa9afd42d9045ab5fd6d28d9e140afc1300917b19b7c6c4df4a494cf1f7cb4a63c80d734265d735af9e4f09455f427aa65a53563f87b336ca2c19d244fcbba617ba0b19e56ed34afe0b253ab91e2fdb1271f1b9e3c3232027ed8862a112f0706e234cf236914b939bcf959821ecb2a6c18057e070de3428046d94b175e1d89bd795e535499a091f5bc65a79d539a8d43891ec504058acb28c08393b5718b57600a211e803f4a634e5c57f25b9b8c4422c6fd90203010001300d06092a864886f70d0101050500038201010008e4ef699e9807677ff56753da73efb2390d5ae2c17e4db691d5df7a7b60fc071ae509c5414be7d5da74df2811e83d3668c4a0b1abc84b9fa7d96b4cdf30bba68517ad2a93e233b042972ac0553a4801c9ebe07bf57ebe9a3b3d6d663965260e50f3b8f46db0531761e60340a2bddc3426098397fda54044a17e5244549f9869b460ca5e6e216b6f6a2db0580b480ca2afe6ec6b46eedacfa4aa45038809ece0c5978653d6c85f678e7f5a2156d1bedd8117751e64a4b0dcd140f3040b021821a8d93aed8d01ba36db6c82372211fed714d9a32607038cdfd565bd529ffc637212aaa2c224ef22b603eccefb5bf1e085c191d4b24fe742b17ab3f55d4e6f05ef"
timestamp="1467169032" url="http://f-droid.org/repo" version="16">
<description>
This is just a test of the extended permissions attributes.
</description>
</repo>
<application id="at.bitfire.davdroid">
<id>at.bitfire.davdroid</id>
<added>2013-10-13</added>
<lastupdated>2016-09-21</lastupdated>
<name>DAVdroid</name>
<summary>Contacts and Calendar sync</summary>
<icon>at.bitfire.davdroid.116.png</icon>
<desc>
<p>DAVdroid is a CalDAV/CardDAV synchronisation adapter for Android 4+ devices.</p>
<p>Use it with your own server (like<a href="https://owncloud.org/">OwnCloud</a>,<a
href="http://baikal-server.com/">Baïkal</a>,
<a href="http://www.davical.org/">DAViCal</a>
or<a href="http://radicale.org/">radiCALe</a>) or with a trusted hoster to keep your
contacts and events under your control.
</p>
<p>Integrates natively in Android calendar/contact apps. See homepage for configuration
details, including info about self-signed certificates.
</p>
<p>For a comparison of server software, see the<a
href="https://wiki.debian.org/Groupware">
Debian wiki</a>.
</p>
</desc>
<license>GPLv3</license>
<categories>Internet</categories>
<category>Internet</category>
<web>https://davdroid.bitfire.at/</web>
<source>https://davdroid.bitfire.at/source/</source>
<tracker>https://davdroid.bitfire.at/forums/</tracker>
<changelog>https://gitlab.com/bitfireAT/davdroid/tags</changelog>
<donate>https://davdroid.bitfire.at/donate/</donate>
<bitcoin>1KSCy7RHztKuhW9fLLaUYqdwdC2iwbejZU</bitcoin>
<flattr>2100160</flattr>
<marketversion>1.3.1-ose</marketversion>
<marketvercode>116</marketvercode>
<package>
<version>1.3.1-ose</version>
<versioncode>116</versioncode>
<apkname>at.bitfire.davdroid_116.apk</apkname>
<srcname>at.bitfire.davdroid_116_src.tar.gz</srcname>
<hash type="sha256">f1aa02257e99c167d2ea9b0e9525c3ce7c181fe2e7f4dd00b65dd81ed2e27a62
</hash>
<sig>03542175324d067b4c36582242f8aecc</sig>
<size>3298864</size>
<sdkver>14</sdkver>
<targetSdkVersion>24</targetSdkVersion>
<added>2016-09-21</added>
<permissions>
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
</permissions>
<uses-permission name="android.permission.GET_ACCOUNTS" maxSdkVersion="22" />
<uses-permission name="android.permission.READ_EXTERNAL_STORAGE" maxSdkVersion="18" />
<uses-permission name="android.permission.WRITE_EXTERNAL_STORAGE" maxSdkVersion="18" />
<uses-permission name="android.permission.AUTHENTICATE_ACCOUNTS" maxSdkVersion="22" />
<uses-permission name="android.permission.MANAGE_ACCOUNTS" maxSdkVersion="22" />
</package>
<package>
<version>1.3-ose</version>
<versioncode>114</versioncode>
<apkname>at.bitfire.davdroid_114.apk</apkname>
<srcname>at.bitfire.davdroid_114_src.tar.gz</srcname>
<hash type="sha256">aaf956539aad7400269997bf1a6689f191b592e70146ffe5484312f9375df9d9
</hash>
<sig>03542175324d067b4c36582242f8aecc</sig>
<size>3295326</size>
<sdkver>14</sdkver>
<targetSdkVersion>24</targetSdkVersion>
<added>2016-09-05</added>
<permissions>
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
</permissions>
<uses-permission name="android.permission.GET_ACCOUNTS" maxSdkVersion="22" />
<uses-permission name="android.permission.READ_EXTERNAL_STORAGE" maxSdkVersion="18" />
<uses-permission name="android.permission.WRITE_EXTERNAL_STORAGE" maxSdkVersion="18" />
<uses-permission name="android.permission.AUTHENTICATE_ACCOUNTS" maxSdkVersion="22" />
<uses-permission name="android.permission.MANAGE_ACCOUNTS" maxSdkVersion="22" />
</package>
<package>
<version>1.2.3-ose</version>
<versioncode>112</versioncode>
<apkname>at.bitfire.davdroid_112.apk</apkname>
<srcname>at.bitfire.davdroid_112_src.tar.gz</srcname>
<hash type="sha256">045480c571f4dfabb23a5efb803b594c432c20ab33b6eb575b4476a8df22bb05
</hash>
<sig>03542175324d067b4c36582242f8aecc</sig>
<size>3172611</size>
<sdkver>14</sdkver>
<targetSdkVersion>24</targetSdkVersion>
<added>2016-08-12</added>
<permissions>
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
</permissions>
<uses-permission name="android.permission.READ_EXTERNAL_STORAGE" maxSdkVersion="18" />
<uses-permission name="android.permission.WRITE_EXTERNAL_STORAGE" maxSdkVersion="18" />
<uses-permission name="android.permission.AUTHENTICATE_ACCOUNTS" maxSdkVersion="22" />
</package>
</application>
<application id="com.murrayc.galaxyzoo.app">
<id>com.murrayc.galaxyzoo.app</id>
<added>2014-11-23</added>
<lastupdated>2016-09-02</lastupdated>
<name>Galaxy Zoo</name>
<summary>Help to classify galaxies</summary>
<icon>com.murrayc.galaxyzoo.app.64.png</icon>
<desc>
<p>Classify
<a href="http://www.galaxyzoo.org/">Galaxy Zoo</a>
subjects. Official approved by the<a href="https://www.zooniverse.org/">Zooniverse
project</a>.
</p>
<p>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.
</p>
</desc>
<license>GPLv3</license>
<categories>Science &amp; Education</categories>
<category>Science &amp; Education</category>
<web />
<source>https://github.com/murraycu/android-galaxyzoo</source>
<tracker>https://github.com/murraycu/android-galaxyzoo/issues</tracker>
<marketversion>1.64</marketversion>
<marketvercode>64</marketvercode>
<package>
<version>1.64</version>
<versioncode>64</versioncode>
<apkname>com.murrayc.galaxyzoo.app_64.apk</apkname>
<srcname>com.murrayc.galaxyzoo.app_64_src.tar.gz</srcname>
<hash type="sha256">6f10487c8ef84078232aafe115228c6252ef982a228666454005b8d62c27fe42
</hash>
<sig>f1a84be1ce965e270f64318cfdb41861</sig>
<size>4095584</size>
<sdkver>11</sdkver>
<targetSdkVersion>24</targetSdkVersion>
<added>2016-09-02</added>
<permissions>
READ_EXTERNAL_STORAGE,WRITE_SYNC_SETTINGS,ACCESS_NETWORK_STATE,WRITE_EXTERNAL_STORAGE,USE_CREDENTIALS,READ_SYNC_SETTINGS,MANAGE_ACCOUNTS,INTERNET,AUTHENTICATE_ACCOUNTS,GET_ACCOUNTS
</permissions>
<uses-permission name="android.permission.USE_CREDENTIALS" maxSdkVersion="22" />
<uses-permission name="android.permission.GET_ACCOUNTS" maxSdkVersion="22" />
<uses-permission name="android.permission.AUTHENTICATE_ACCOUNTS" maxSdkVersion="22" />
<uses-permission name="android.permission.MANAGE_ACCOUNTS" maxSdkVersion="22" />
</package>
<package>
<version>1.61</version>
<versioncode>61</versioncode>
<apkname>com.murrayc.galaxyzoo.app_61.apk</apkname>
<srcname>com.murrayc.galaxyzoo.app_61_src.tar.gz</srcname>
<hash type="sha256">c35bc15885d57a2ddfcb2c702c9147b63b63a9765a6eeacf98d6a1534f6f8ff2
</hash>
<sig>f1a84be1ce965e270f64318cfdb41861</sig>
<size>4588315</size>
<sdkver>11</sdkver>
<targetSdkVersion>24</targetSdkVersion>
<added>2016-08-26</added>
<permissions>
READ_EXTERNAL_STORAGE,WRITE_SYNC_SETTINGS,ACCESS_NETWORK_STATE,WRITE_EXTERNAL_STORAGE,USE_CREDENTIALS,READ_SYNC_SETTINGS,MANAGE_ACCOUNTS,INTERNET,AUTHENTICATE_ACCOUNTS,GET_ACCOUNTS
</permissions>
<uses-permission name="android.permission.USE_CREDENTIALS" maxSdkVersion="22" />
<uses-permission name="android.permission.GET_ACCOUNTS" maxSdkVersion="22" />
<uses-permission name="android.permission.AUTHENTICATE_ACCOUNTS" maxSdkVersion="22" />
<uses-permission name="android.permission.MANAGE_ACCOUNTS" maxSdkVersion="22" />
</package>
<package>
<version>1.59</version>
<versioncode>59</versioncode>
<apkname>com.murrayc.galaxyzoo.app_59.apk</apkname>
<srcname>com.murrayc.galaxyzoo.app_59_src.tar.gz</srcname>
<hash type="sha256">719d1440dd2f16ad29b45edba5b43262f22ef68cee0c6b478e40fe8eb0e53227
</hash>
<sig>f1a84be1ce965e270f64318cfdb41861</sig>
<size>4116175</size>
<sdkver>11</sdkver>
<targetSdkVersion>24</targetSdkVersion>
<added>2016-08-25</added>
<permissions>
READ_EXTERNAL_STORAGE,WRITE_SYNC_SETTINGS,ACCESS_NETWORK_STATE,WRITE_EXTERNAL_STORAGE,USE_CREDENTIALS,READ_SYNC_SETTINGS,MANAGE_ACCOUNTS,INTERNET,AUTHENTICATE_ACCOUNTS,GET_ACCOUNTS
</permissions>
<uses-permission name="android.permission.USE_CREDENTIALS" maxSdkVersion="22" />
<uses-permission name="android.permission.GET_ACCOUNTS" maxSdkVersion="22" />
<uses-permission name="android.permission.AUTHENTICATE_ACCOUNTS" maxSdkVersion="22" />
<uses-permission name="android.permission.MANAGE_ACCOUNTS" maxSdkVersion="22" />
</package>
</application>
</fdroid>

View File

@ -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<Apk> apks = new ArrayList<>();
public List<App> apps = new ArrayList<>();
public List<RepoPushRequest> 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<Apk> 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;
}
}
}