diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index b5685b014..41acae8d8 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -95,6 +95,16 @@
android:name="org.fdroid.fdroid.data.InstalledAppProvider"
android:exported="false"/>
+
+
+
+
diff --git a/app/src/main/java/org/fdroid/fdroid/AppDetails.java b/app/src/main/java/org/fdroid/fdroid/AppDetails.java
index 69b03523e..c96738ef2 100644
--- a/app/src/main/java/org/fdroid/fdroid/AppDetails.java
+++ b/app/src/main/java/org/fdroid/fdroid/AppDetails.java
@@ -971,7 +971,7 @@ public class AppDetails extends AppCompatActivity {
}
private void initiateInstall(Apk apk) {
- Installer installer = InstallerFactory.create(this, apk.packageName);
+ Installer installer = InstallerFactory.create(this, apk);
Intent intent = installer.getPermissionScreen(apk);
if (intent != null) {
// permission screen required
@@ -990,7 +990,7 @@ public class AppDetails extends AppCompatActivity {
}
private void uninstallApk(String packageName) {
- Installer installer = InstallerFactory.create(this, packageName);
+ Installer installer = InstallerFactory.create(this, null);
Intent intent = installer.getUninstallScreen(packageName);
if (intent != null) {
// uninstall screen required
diff --git a/app/src/main/java/org/fdroid/fdroid/CleanCacheService.java b/app/src/main/java/org/fdroid/fdroid/CleanCacheService.java
index cea679d9f..d51719917 100644
--- a/app/src/main/java/org/fdroid/fdroid/CleanCacheService.java
+++ b/app/src/main/java/org/fdroid/fdroid/CleanCacheService.java
@@ -9,6 +9,7 @@ import android.os.Process;
import android.os.SystemClock;
import org.apache.commons.io.FileUtils;
+import org.fdroid.fdroid.installer.ApkCache;
import java.io.File;
@@ -51,7 +52,7 @@ public class CleanCacheService extends IntentService {
return;
}
Process.setThreadPriority(Process.THREAD_PRIORITY_LOWEST);
- Utils.clearOldFiles(Utils.getApkCacheDir(this), Preferences.get().getKeepCacheTime());
+ ApkCache.clearApkCache(this);
deleteStrayIndexFiles();
deleteOldInstallerFiles();
}
diff --git a/app/src/main/java/org/fdroid/fdroid/Utils.java b/app/src/main/java/org/fdroid/fdroid/Utils.java
index 5e6d2f7ea..fb1c64415 100644
--- a/app/src/main/java/org/fdroid/fdroid/Utils.java
+++ b/app/src/main/java/org/fdroid/fdroid/Utils.java
@@ -33,9 +33,7 @@ import android.util.Log;
import com.nostra13.universalimageloader.core.DisplayImageOptions;
import com.nostra13.universalimageloader.core.assist.ImageScaleType;
import com.nostra13.universalimageloader.core.display.FadeInBitmapDisplayer;
-import com.nostra13.universalimageloader.utils.StorageUtils;
-import org.apache.commons.io.FileUtils;
import org.fdroid.fdroid.compat.FileCompat;
import org.fdroid.fdroid.data.Repo;
import org.fdroid.fdroid.data.SanitizedFile;
@@ -274,58 +272,6 @@ public final class Utils {
return Uri.parse("package:" + packageName);
}
- /**
- * This location is only for caching, do not install directly from this location
- * because if the file is on the External Storage, any other app could swap out
- * the APK while the install was in process, allowing malware to install things.
- * Using {@link Installer#installPackage(File, String, String)}
- * is fine since that does the right thing.
- */
- public static File getApkCacheDir(Context context) {
- File apkCacheDir = new File(StorageUtils.getCacheDirectory(context, true), "apks");
- if (apkCacheDir.isFile()) {
- apkCacheDir.delete();
- }
- if (!apkCacheDir.exists()) {
- apkCacheDir.mkdir();
- }
- return apkCacheDir;
- }
-
- /**
- * Get the full path for where an APK URL will be downloaded into.
- */
- public static SanitizedFile getApkDownloadPath(Context context, Uri uri) {
- File dir = new File(Utils.getApkCacheDir(context), uri.getHost() + "-" + uri.getPort());
- if (!dir.exists()) {
- dir.mkdirs();
- }
- return new SanitizedFile(dir, uri.getLastPathSegment());
- }
-
- /**
- * Recursively delete files in {@code dir} that were last modified
- * {@code secondsAgo} seconds ago, e.g. when it was downloaded.
- *
- * @param dir The directory to recurse in
- * @param secondsAgo The number of seconds old that marks a file for deletion.
- */
- public static void clearOldFiles(File dir, long secondsAgo) {
- if (dir == null) {
- return;
- }
- long olderThan = System.currentTimeMillis() - (secondsAgo * 1000L);
- for (File f : dir.listFiles()) {
- if (f.isDirectory()) {
- clearOldFiles(f, olderThan);
- f.delete();
- }
- if (FileUtils.isFileOlder(f, olderThan)) {
- f.delete();
- }
- }
- }
-
public static String calcFingerprint(String keyHexString) {
if (TextUtils.isEmpty(keyHexString)
|| keyHexString.matches(".*[^a-fA-F0-9].*")) {
diff --git a/app/src/main/java/org/fdroid/fdroid/installer/ApkCache.java b/app/src/main/java/org/fdroid/fdroid/installer/ApkCache.java
new file mode 100644
index 000000000..b0dc8b3b8
--- /dev/null
+++ b/app/src/main/java/org/fdroid/fdroid/installer/ApkCache.java
@@ -0,0 +1,165 @@
+/*
+ * Copyright (C) 2016 Dominik Schürmann
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 3
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+ * MA 02110-1301, USA.
+ */
+
+package org.fdroid.fdroid.installer;
+
+import android.content.Context;
+import android.net.Uri;
+
+import com.nostra13.universalimageloader.utils.StorageUtils;
+
+import org.apache.commons.io.FileUtils;
+import org.fdroid.fdroid.Hasher;
+import org.fdroid.fdroid.Preferences;
+import org.fdroid.fdroid.data.Apk;
+import org.fdroid.fdroid.data.SanitizedFile;
+
+import java.io.File;
+import java.io.IOException;
+import java.security.NoSuchAlgorithmException;
+
+public class ApkCache {
+
+ private static final String CACHE_DIR = "apks";
+
+ /**
+ * Copy the APK to the safe location inside of the protected area
+ * of the app to prevent attacks based on other apps swapping the file
+ * out during the install process. Most likely, apkFile was just downloaded,
+ * so it should still be in the RAM disk cache.
+ */
+ public static SanitizedFile copyApkFromCacheToFiles(Context context, File apkFile, Apk expectedApk)
+ throws IOException {
+ SanitizedFile sanitizedApkFile = null;
+
+ try {
+ sanitizedApkFile = SanitizedFile.knownSanitized(
+ File.createTempFile("install-", ".apk", context.getFilesDir()));
+ FileUtils.copyFile(apkFile, sanitizedApkFile);
+
+ // verify copied file's hash with expected hash from Apk class
+ if (!verifyApkFile(sanitizedApkFile, expectedApk.hash, expectedApk.hashType)) {
+ FileUtils.deleteQuietly(apkFile);
+ throw new IOException(apkFile + " failed to verify!");
+ }
+
+ return sanitizedApkFile;
+ } catch (NoSuchAlgorithmException e) {
+ throw new RuntimeException(e);
+ } finally {
+ // 20 minutes the start of the install process, delete the file
+ final File apkToDelete = sanitizedApkFile;
+ new Thread() {
+ @Override
+ public void run() {
+ android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_LOWEST);
+ try {
+ Thread.sleep(1200000);
+ } catch (InterruptedException ignored) {
+ } finally {
+ FileUtils.deleteQuietly(apkToDelete);
+ }
+ }
+ }.start();
+ }
+ }
+
+ /**
+ * Checks the APK file against the provided hash, returning whether it is a match.
+ */
+ private static boolean verifyApkFile(File apkFile, String hash, String hashType)
+ throws NoSuchAlgorithmException {
+ if (!apkFile.exists()) {
+ return false;
+ }
+ Hasher hasher = new Hasher(hashType, apkFile);
+ return hasher.match(hash);
+ }
+
+ /**
+ * Get the full path for where an APK URL will be downloaded into.
+ */
+ public static SanitizedFile getApkDownloadPath(Context context, Uri uri) {
+ File dir = new File(getApkCacheDir(context), uri.getHost() + "-" + uri.getPort());
+ if (!dir.exists()) {
+ dir.mkdirs();
+ }
+ return new SanitizedFile(dir, uri.getLastPathSegment());
+ }
+
+ /**
+ * Verifies the size of the file on disk matches, and then hashes the file to compare with what
+ * we received from the signed repo (i.e. {@link Apk#hash} and {@link Apk#hashType}).
+ * Bails out if the file sizes don't match to prevent having to do the work of hashing the file.
+ */
+ public static boolean apkIsCached(File apkFile, Apk apkToCheck) {
+ try {
+ return apkFile.length() == apkToCheck.size &&
+ verifyApkFile(apkFile, apkToCheck.hash, apkToCheck.hashType);
+ } catch (NoSuchAlgorithmException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public static void clearApkCache(Context context) {
+ clearOldFiles(getApkCacheDir(context), Preferences.get().getKeepCacheTime());
+ }
+
+
+ /**
+ * This location is only for caching, do not install directly from this location
+ * because if the file is on the External Storage, any other app could swap out
+ * the APK while the install was in process, allowing malware to install things.
+ * Using {@link Installer#installPackage(Uri localApkUri, Uri downloadUri, String packageName)}
+ * is fine since that does the right thing.
+ */
+ private static File getApkCacheDir(Context context) {
+ File apkCacheDir = new File(StorageUtils.getCacheDirectory(context, true), CACHE_DIR);
+ if (apkCacheDir.isFile()) {
+ apkCacheDir.delete();
+ }
+ if (!apkCacheDir.exists()) {
+ apkCacheDir.mkdir();
+ }
+ return apkCacheDir;
+ }
+
+ /**
+ * Recursively delete files in {@code dir} that were last modified
+ * {@code secondsAgo} seconds ago, e.g. when it was downloaded.
+ *
+ * @param dir The directory to recurse in
+ * @param secondsAgo The number of seconds old that marks a file for deletion.
+ */
+ public static void clearOldFiles(File dir, long secondsAgo) {
+ if (dir == null) {
+ return;
+ }
+ long olderThan = System.currentTimeMillis() - (secondsAgo * 1000L);
+ for (File f : dir.listFiles()) {
+ if (f.isDirectory()) {
+ clearOldFiles(f, olderThan);
+ f.delete();
+ }
+ if (FileUtils.isFileOlder(f, olderThan)) {
+ f.delete();
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/org/fdroid/fdroid/installer/ApkFileProvider.java b/app/src/main/java/org/fdroid/fdroid/installer/ApkFileProvider.java
new file mode 100644
index 000000000..0c81a5381
--- /dev/null
+++ b/app/src/main/java/org/fdroid/fdroid/installer/ApkFileProvider.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2016 Dominik Schürmann
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 3
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+ * MA 02110-1301, USA.
+ */
+
+package org.fdroid.fdroid.installer;
+
+import android.content.Context;
+import android.net.Uri;
+import android.support.v4.content.FileProvider;
+
+import org.fdroid.fdroid.data.Apk;
+import org.fdroid.fdroid.data.SanitizedFile;
+
+import java.io.File;
+import java.io.IOException;
+
+/**
+ * This class has helper methods for preparing apks for installation.
+ *
+ * APK handling for installations:
+ * 1. APKs are downloaded into a cache directory that is either created on SD card
+ * "/Android/data/[app_package_name]/cache/apks" (if card is mounted and app has
+ * appropriate permission) or on device's file system depending incoming parameters.
+ * 2. Before installation, the APK is copied into the private data directory of the F-Droid,
+ * "/data/data/[app_package_name]/files/install-$random.apk".
+ * 3. The hash of the file is checked against the expected hash from the repository
+ * 4. For Android < 7, a file Uri pointing to the File is returned, for Android >= 7,
+ * a content Uri is returned using support lib's FileProvider.
+ */
+public class ApkFileProvider extends FileProvider {
+
+ public static final String AUTHORITY = "org.fdroid.fdroid.installer.ApkFileProvider";
+
+ /**
+ * Copies the APK into private data directory of F-Droid and returns a "file" or "content" Uri
+ * to be used for installation.
+ */
+ public static Uri getSafeUri(Context context, Uri localApkUri, Apk expectedApk, boolean useContentUri)
+ throws IOException {
+ File apkFile = new File(localApkUri.getPath());
+
+ SanitizedFile sanitizedApkFile =
+ ApkCache.copyApkFromCacheToFiles(context, apkFile, expectedApk);
+
+ if (useContentUri) {
+ // return a content Uri using support libs FileProvider
+
+ return getUriForFile(context, AUTHORITY, sanitizedApkFile);
+ }
+
+ // Need the apk to be world readable, so that the installer is able to read it.
+ // Note that saving it into external storage for the purpose of letting the installer
+ // have access is insecure, because apps with permission to write to the external
+ // storage can overwrite the app between F-Droid asking for it to be installed and
+ // the installer actually installing it.
+ sanitizedApkFile.setReadable(true, false);
+
+ return Uri.fromFile(sanitizedApkFile);
+ }
+
+}
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 649bedf51..bfb902009 100644
--- a/app/src/main/java/org/fdroid/fdroid/installer/ApkVerifier.java
+++ b/app/src/main/java/org/fdroid/fdroid/installer/ApkVerifier.java
@@ -26,15 +26,9 @@ import android.net.Uri;
import android.text.TextUtils;
import android.util.Log;
-import org.apache.commons.io.FileUtils;
-import org.fdroid.fdroid.Hasher;
import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.data.Apk;
-import org.fdroid.fdroid.data.SanitizedFile;
-import java.io.File;
-import java.io.IOException;
-import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.HashSet;
@@ -47,13 +41,11 @@ public class ApkVerifier {
private static final String TAG = "ApkVerifier";
- private final Context context;
private final Uri localApkUri;
private final Apk expectedApk;
private final PackageManager pm;
ApkVerifier(Context context, Uri localApkUri, Apk expectedApk) {
- this.context = context;
this.localApkUri = localApkUri;
this.expectedApk = expectedApk;
this.pm = context.getPackageManager();
@@ -107,67 +99,6 @@ public class ApkVerifier {
return new HashSet<>(Arrays.asList(localApkInfo.requestedPermissions));
}
- public Uri getSafeUri() throws ApkVerificationException {
- File apkFile = new File(localApkUri.getPath());
-
- SanitizedFile sanitizedApkFile = null;
- try {
-
- /* Always copy the APK to the safe location inside of the protected area
- * of the app to prevent attacks based on other apps swapping the file
- * out during the install process. Most likely, apkFile was just downloaded,
- * so it should still be in the RAM disk cache */
- sanitizedApkFile = SanitizedFile.knownSanitized(File.createTempFile("install-", ".apk",
- context.getFilesDir()));
- FileUtils.copyFile(apkFile, sanitizedApkFile);
- if (!verifyApkFile(sanitizedApkFile, expectedApk.hash, expectedApk.hashType)) {
- FileUtils.deleteQuietly(apkFile);
- throw new ApkVerificationException(apkFile + " failed to verify!");
- }
- apkFile = null; // ensure this is not used now that its copied to apkToInstall
-
- // Need the apk to be world readable, so that the installer is able to read it.
- // Note that saving it into external storage for the purpose of letting the installer
- // have access is insecure, because apps with permission to write to the external
- // storage can overwrite the app between F-Droid asking for it to be installed and
- // the installer actually installing it.
- sanitizedApkFile.setReadable(true, false);
-
- } catch (IOException | NoSuchAlgorithmException e) {
- throw new ApkVerificationException(e);
- } finally {
- // 20 minutes the start of the install process, delete the file
- final File apkToDelete = sanitizedApkFile;
- new Thread() {
- @Override
- public void run() {
- android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_LOWEST);
- try {
- Thread.sleep(1200000);
- } catch (InterruptedException e) {
- e.printStackTrace();
- } finally {
- FileUtils.deleteQuietly(apkToDelete);
- }
- }
- }.start();
- }
-
- return Uri.fromFile(sanitizedApkFile);
- }
-
- /**
- * Checks the APK file against the provided hash, returning whether it is a match.
- */
- static boolean verifyApkFile(File apkFile, String hash, String hashType)
- throws NoSuchAlgorithmException {
- if (!apkFile.exists()) {
- return false;
- }
- Hasher hasher = new Hasher(hashType, apkFile);
- return hasher.match(hash);
- }
-
public static class ApkVerificationException extends Exception {
public ApkVerificationException(String message) {
diff --git a/app/src/main/java/org/fdroid/fdroid/installer/DefaultInstaller.java b/app/src/main/java/org/fdroid/fdroid/installer/DefaultInstaller.java
index 7f48e7ca3..9afc00d2c 100644
--- a/app/src/main/java/org/fdroid/fdroid/installer/DefaultInstaller.java
+++ b/app/src/main/java/org/fdroid/fdroid/installer/DefaultInstaller.java
@@ -23,8 +23,10 @@ import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
+import android.os.Build;
import org.fdroid.fdroid.Utils;
+import org.fdroid.fdroid.data.Apk;
import java.io.File;
@@ -44,7 +46,7 @@ public class DefaultInstaller extends Installer {
}
@Override
- protected void installPackage(Uri localApkUri, Uri downloadUri, String packageName) {
+ protected void installPackageInternal(Uri localApkUri, Uri downloadUri, Apk apk) {
sendBroadcastInstall(downloadUri, Installer.ACTION_INSTALL_STARTED);
Utils.debugLog(TAG, "DefaultInstaller uri: " + localApkUri + " file: " + new File(localApkUri.getPath()));
@@ -86,4 +88,12 @@ public class DefaultInstaller extends Installer {
protected boolean isUnattended() {
return false;
}
+
+ @Override
+ protected boolean supportsContentUri() {
+ // TODO: replace Android N check with proper version code
+ //if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ // Android N only supports content Uris
+ return "N".equals(Build.VERSION.CODENAME);
+ }
}
diff --git a/app/src/main/java/org/fdroid/fdroid/installer/DefaultInstallerActivity.java b/app/src/main/java/org/fdroid/fdroid/installer/DefaultInstallerActivity.java
index 420f37a05..9b65f62c3 100644
--- a/app/src/main/java/org/fdroid/fdroid/installer/DefaultInstallerActivity.java
+++ b/app/src/main/java/org/fdroid/fdroid/installer/DefaultInstallerActivity.java
@@ -79,10 +79,12 @@ public class DefaultInstallerActivity extends FragmentActivity {
throw new RuntimeException("Set the data uri to point to an apk location!");
}
// https://code.google.com/p/android/issues/detail?id=205827
- if ((Build.VERSION.SDK_INT <= Build.VERSION_CODES.M)
- && (!uri.getScheme().equals("file"))) {
- throw new RuntimeException("PackageInstaller <= Android 6 only supports file scheme!");
- }
+ // TODO: re-enable after Android N release
+ //if ((Build.VERSION.SDK_INT <= Build.VERSION_CODES.M)
+ // && (!uri.getScheme().equals("file"))) {
+ // throw new RuntimeException("PackageInstaller <= Android 6 only supports file scheme!");
+ //}
+ // TODO: replace with proper version check after Android N release
if (("N".equals(Build.VERSION.CODENAME))
&& (!uri.getScheme().equals("content"))) {
throw new RuntimeException("PackageInstaller >= Android N only supports content scheme!");
@@ -91,26 +93,41 @@ public class DefaultInstallerActivity extends FragmentActivity {
Intent intent = new Intent();
intent.setData(uri);
+ // Note regarding EXTRA_NOT_UNKNOWN_SOURCE:
+ // works only when being installed as system-app
+ // https://code.google.com/p/android/issues/detail?id=42253
+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
intent.setAction(Intent.ACTION_VIEW);
intent.setType("application/vnd.android.package-archive");
- } else {
+ } else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
+ intent.setAction(Intent.ACTION_INSTALL_PACKAGE);
+ intent.putExtra(Intent.EXTRA_RETURN_RESULT, true);
+ intent.putExtra(Intent.EXTRA_NOT_UNKNOWN_SOURCE, true);
+ intent.putExtra(Intent.EXTRA_ALLOW_REPLACE, true);
+ } else if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) {
+ intent.setAction(Intent.ACTION_INSTALL_PACKAGE);
+ intent.putExtra(Intent.EXTRA_RETURN_RESULT, true);
+ intent.putExtra(Intent.EXTRA_NOT_UNKNOWN_SOURCE, true);
+ } else { // Android N
intent.setAction(Intent.ACTION_INSTALL_PACKAGE);
-
// EXTRA_RETURN_RESULT throws a RuntimeException on N
// https://gitlab.com/fdroid/fdroidclient/issues/631
- if (!"N".equals(Build.VERSION.CODENAME)) {
- intent.putExtra(Intent.EXTRA_RETURN_RESULT, true);
- }
-
- // following extras only work when being installed as system-app
- // https://code.google.com/p/android/issues/detail?id=42253
+ //intent.putExtra(Intent.EXTRA_RETURN_RESULT, true);
intent.putExtra(Intent.EXTRA_NOT_UNKNOWN_SOURCE, true);
+ // grant READ permission for this content Uri
+ intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ }
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
- // deprecated in Android 4.1
- intent.putExtra(Intent.EXTRA_ALLOW_REPLACE, true);
- }
+ // TODO: remove whole block after Android N release
+ if ("N".equals(Build.VERSION.CODENAME)) {
+ intent.setAction(Intent.ACTION_INSTALL_PACKAGE);
+ // EXTRA_RETURN_RESULT throws a RuntimeException on N
+ // https://gitlab.com/fdroid/fdroidclient/issues/631
+ intent.putExtra(Intent.EXTRA_RETURN_RESULT, false);
+ intent.putExtra(Intent.EXTRA_NOT_UNKNOWN_SOURCE, true);
+ // grant READ permission for this content Uri
+ intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
}
try {
@@ -171,6 +188,7 @@ public class DefaultInstallerActivity extends FragmentActivity {
break;
}
+ // TODO: remove Android N hack after release
// Fallback on N for https://gitlab.com/fdroid/fdroidclient/issues/631
if ("N".equals(Build.VERSION.CODENAME)) {
installer.sendBroadcastInstall(downloadUri, Installer.ACTION_INSTALL_COMPLETE);
diff --git a/app/src/main/java/org/fdroid/fdroid/installer/ExtensionInstaller.java b/app/src/main/java/org/fdroid/fdroid/installer/ExtensionInstaller.java
index a17565dd3..d40137c2d 100644
--- a/app/src/main/java/org/fdroid/fdroid/installer/ExtensionInstaller.java
+++ b/app/src/main/java/org/fdroid/fdroid/installer/ExtensionInstaller.java
@@ -25,6 +25,7 @@ import android.content.Intent;
import android.net.Uri;
import org.fdroid.fdroid.BuildConfig;
+import org.fdroid.fdroid.data.Apk;
import org.fdroid.fdroid.privileged.install.InstallExtensionDialogActivity;
import java.io.File;
@@ -43,7 +44,7 @@ public class ExtensionInstaller extends Installer {
}
@Override
- protected void installPackage(Uri localApkUri, Uri downloadUri, String packageName) {
+ protected void installPackageInternal(Uri localApkUri, Uri downloadUri, Apk apk) {
// extension must be signed with the same public key as main F-Droid
// NOTE: Disabled for debug builds to be able to test official extension from repo
ApkSignatureVerifier signatureVerifier = new ApkSignatureVerifier(context);
@@ -93,4 +94,9 @@ public class ExtensionInstaller extends Installer {
protected boolean isUnattended() {
return false;
}
+
+ @Override
+ protected boolean supportsContentUri() {
+ return false;
+ }
}
diff --git a/app/src/main/java/org/fdroid/fdroid/installer/InstallManagerService.java b/app/src/main/java/org/fdroid/fdroid/installer/InstallManagerService.java
index 971ed732d..a838fc3e1 100644
--- a/app/src/main/java/org/fdroid/fdroid/installer/InstallManagerService.java
+++ b/app/src/main/java/org/fdroid/fdroid/installer/InstallManagerService.java
@@ -15,7 +15,6 @@ import android.support.v4.app.NotificationCompat;
import android.support.v4.app.TaskStackBuilder;
import android.support.v4.content.LocalBroadcastManager;
import android.text.TextUtils;
-import android.util.Log;
import org.fdroid.fdroid.AppDetails;
import org.fdroid.fdroid.R;
@@ -27,7 +26,6 @@ import org.fdroid.fdroid.net.Downloader;
import org.fdroid.fdroid.net.DownloaderService;
import java.io.File;
-import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
@@ -162,12 +160,12 @@ public class InstallManagerService extends Service {
registerDownloaderReceivers(urlString, builder);
- File apkFilePath = Utils.getApkDownloadPath(this, intent.getData());
+ File apkFilePath = ApkCache.getApkDownloadPath(this, intent.getData());
long apkFileSize = apkFilePath.length();
if (!apkFilePath.exists() || apkFileSize < apk.size) {
Utils.debugLog(TAG, "download " + urlString + " " + apkFilePath);
DownloaderService.queue(this, urlString);
- } else if (apkIsCached(apkFilePath, apk)) {
+ } else if (ApkCache.apkIsCached(apkFilePath, apk)) {
Utils.debugLog(TAG, "skip download, we have it, straight to install " + urlString + " " + apkFilePath);
sendBroadcast(intent.getData(), Downloader.ACTION_STARTED, apkFilePath);
sendBroadcast(intent.getData(), Downloader.ACTION_COMPLETE, apkFilePath);
@@ -179,21 +177,6 @@ public class InstallManagerService extends Service {
return START_REDELIVER_INTENT; // if killed before completion, retry Intent
}
- /**
- * Verifies the size of the file on disk matches, and then hashes the file to compare with what
- * we received from the signed repo (i.e. {@link Apk#hash} and {@link Apk#hashType}).
- * Bails out if the file sizes don't match to prevent having to do the work of hashing the file.
- */
- private static boolean apkIsCached(File apkFile, Apk apkToCheck) {
- try {
- return apkFile.length() == apkToCheck.size &&
- ApkVerifier.verifyApkFile(apkFile, apkToCheck.hash, apkToCheck.hashType);
- } catch (NoSuchAlgorithmException e) {
- e.printStackTrace();
- return false;
- }
- }
-
private void sendBroadcast(Uri uri, String action, File file) {
Intent intent = new Intent(action);
intent.setData(uri);
@@ -238,21 +221,7 @@ public class InstallManagerService extends Service {
Apk apk = ACTIVE_APKS.get(urlString);
- Uri sanitizedUri;
- try {
- ApkVerifier apkVerifier = new ApkVerifier(context, localApkUri, apk);
- apkVerifier.verifyApk();
- sanitizedUri = apkVerifier.getSafeUri();
- } catch (ApkVerifier.ApkVerificationException e) {
- Log.e(TAG, "ApkVerifier failed", e);
- String title = String.format(
- getString(R.string.install_error_notify_title),
- apk.packageName);
- notifyError(urlString, title, e.getMessage());
- return;
- }
-
- InstallerService.install(context, sanitizedUri, downloadUri, apk.packageName);
+ InstallerService.install(context, localApkUri, downloadUri, apk);
}
};
BroadcastReceiver interruptedReceiver = new BroadcastReceiver() {
diff --git a/app/src/main/java/org/fdroid/fdroid/installer/Installer.java b/app/src/main/java/org/fdroid/fdroid/installer/Installer.java
index c143b1033..ff94f2563 100644
--- a/app/src/main/java/org/fdroid/fdroid/installer/Installer.java
+++ b/app/src/main/java/org/fdroid/fdroid/installer/Installer.java
@@ -28,6 +28,7 @@ import android.os.Build;
import android.os.PatternMatcher;
import android.support.v4.content.LocalBroadcastManager;
import android.text.TextUtils;
+import android.util.Log;
import org.fdroid.fdroid.data.Apk;
import org.fdroid.fdroid.data.ApkProvider;
@@ -36,6 +37,8 @@ import org.fdroid.fdroid.privileged.views.AppSecurityPermissions;
import org.fdroid.fdroid.privileged.views.InstallConfirmActivity;
import org.fdroid.fdroid.privileged.views.UninstallDialogActivity;
+import java.io.IOException;
+
/**
* Handles the actual install process. Subclasses implement the details.
*/
@@ -43,6 +46,8 @@ public abstract class Installer {
final Context context;
private final LocalBroadcastManager localBroadcastManager;
+ private static final String TAG = "Installer";
+
public static final String ACTION_INSTALL_STARTED = "org.fdroid.fdroid.installer.Installer.action.INSTALL_STARTED";
public static final String ACTION_INSTALL_COMPLETE = "org.fdroid.fdroid.installer.Installer.action.INSTALL_COMPLETE";
public static final String ACTION_INSTALL_INTERRUPTED = "org.fdroid.fdroid.installer.Installer.action.INSTALL_INTERRUPTED";
@@ -62,23 +67,11 @@ public abstract class Installer {
* @see Intent#EXTRA_ORIGINATING_URI
*/
static final String EXTRA_DOWNLOAD_URI = "org.fdroid.fdroid.installer.Installer.extra.DOWNLOAD_URI";
+ public static final String EXTRA_APK = "org.fdroid.fdroid.installer.Installer.extra.APK";
public static final String EXTRA_PACKAGE_NAME = "org.fdroid.fdroid.installer.Installer.extra.PACKAGE_NAME";
public static final String EXTRA_USER_INTERACTION_PI = "org.fdroid.fdroid.installer.Installer.extra.USER_INTERACTION_PI";
public static final String EXTRA_ERROR_MESSAGE = "org.fdroid.fdroid.net.installer.Installer.extra.ERROR_MESSAGE";
- public static class InstallFailedException extends Exception {
-
- private static final long serialVersionUID = -8343133906463328027L;
-
- public InstallFailedException(String message) {
- super(message);
- }
-
- public InstallFailedException(Throwable cause) {
- super(cause);
- }
- }
-
Installer(Context context) {
this.context = context;
localBroadcastManager = LocalBroadcastManager.getInstance(context);
@@ -150,8 +143,7 @@ public abstract class Installer {
return intent;
}
- void sendBroadcastInstall(Uri downloadUri, String action,
- PendingIntent pendingIntent) {
+ void sendBroadcastInstall(Uri downloadUri, String action, PendingIntent pendingIntent) {
sendBroadcastInstall(downloadUri, action, pendingIntent, null);
}
@@ -164,7 +156,7 @@ public abstract class Installer {
}
void sendBroadcastInstall(Uri downloadUri, String action,
- PendingIntent pendingIntent, String errorMessage) {
+ PendingIntent pendingIntent, String errorMessage) {
Intent intent = new Intent(action);
intent.setData(downloadUri);
intent.putExtra(Installer.EXTRA_USER_INTERACTION_PI, pendingIntent);
@@ -182,13 +174,12 @@ public abstract class Installer {
sendBroadcastUninstall(packageName, action, null, null);
}
- void sendBroadcastUninstall(String packageName, String action,
- PendingIntent pendingIntent) {
+ void sendBroadcastUninstall(String packageName, String action, PendingIntent pendingIntent) {
sendBroadcastUninstall(packageName, action, pendingIntent, null);
}
void sendBroadcastUninstall(String packageName, String action,
- PendingIntent pendingIntent, String errorMessage) {
+ PendingIntent pendingIntent, String errorMessage) {
Uri uri = Uri.fromParts("package", packageName, null);
Intent intent = new Intent(action);
@@ -229,13 +220,40 @@ public abstract class Installer {
}
/**
+ * Install apk
+ *
* @param localApkUri points to the local copy of the APK to be installed
* @param downloadUri serves as the unique ID for all actions related to the
* installation of that specific APK
- * @param packageName package name of the app that should be installed
+ * @param apk apk object of the app that should be installed
*/
- protected abstract void installPackage(Uri localApkUri, Uri downloadUri, String packageName);
+ public void installPackage(Uri localApkUri, Uri downloadUri, Apk apk) {
+ Uri sanitizedUri;
+ try {
+ // verify that permissions of the apk file match the ones from the apk object
+ ApkVerifier apkVerifier = new ApkVerifier(context, localApkUri, apk);
+ apkVerifier.verifyApk();
+ // move apk file to private directory for installation and check hash
+ sanitizedUri = ApkFileProvider.getSafeUri(
+ context, localApkUri, apk, supportsContentUri());
+ } catch (ApkVerifier.ApkVerificationException | IOException e) {
+ Log.e(TAG, "ApkVerifier / ApkFileProvider failed", e);
+ sendBroadcastInstall(downloadUri, Installer.ACTION_INSTALL_INTERRUPTED,
+ e.getMessage());
+ return;
+ }
+
+ installPackageInternal(sanitizedUri, downloadUri, apk);
+ }
+
+ protected abstract void installPackageInternal(Uri localApkUri, Uri downloadUri, Apk apk);
+
+ /**
+ * Uninstall app
+ *
+ * @param packageName package name of the app that should be uninstalled
+ */
protected abstract void uninstallPackage(String packageName);
/**
@@ -244,4 +262,9 @@ public abstract class Installer {
*/
protected abstract boolean isUnattended();
+ /**
+ * @return true if the Installer supports content Uris and not just file Uris
+ */
+ protected abstract boolean supportsContentUri();
+
}
diff --git a/app/src/main/java/org/fdroid/fdroid/installer/InstallerFactory.java b/app/src/main/java/org/fdroid/fdroid/installer/InstallerFactory.java
index f2ecd7b6a..b35686e4e 100644
--- a/app/src/main/java/org/fdroid/fdroid/installer/InstallerFactory.java
+++ b/app/src/main/java/org/fdroid/fdroid/installer/InstallerFactory.java
@@ -24,6 +24,7 @@ import android.util.Log;
import org.fdroid.fdroid.Preferences;
import org.fdroid.fdroid.Utils;
+import org.fdroid.fdroid.data.Apk;
public class InstallerFactory {
@@ -34,17 +35,16 @@ public class InstallerFactory {
* Either DefaultInstaller, PrivilegedInstaller, or in the special
* case to install the "F-Droid Privileged Extension" ExtensionInstaller.
*
- * @param context current {@link Context}
- * @param packageName package name of apk to be installed. Required to select
- * the ExtensionInstaller.
- * If this is null, the ExtensionInstaller will never be returned.
+ * @param context current {@link Context}
+ * @param apk apk to be installed. Required to select the ExtensionInstaller.
+ * If this is null, the ExtensionInstaller will never be returned.
* @return instance of an Installer
*/
- public static Installer create(Context context, String packageName) {
+ public static Installer create(Context context, Apk apk) {
Installer installer;
- if (packageName != null
- && packageName.equals(PrivilegedInstaller.PRIVILEGED_EXTENSION_PACKAGE_NAME)) {
+ if (apk != null
+ && apk.packageName.equals(PrivilegedInstaller.PRIVILEGED_EXTENSION_PACKAGE_NAME)) {
// special case for "F-Droid Privileged Extension"
installer = new ExtensionInstaller(context);
} else if (isPrivilegedInstallerEnabled()) {
@@ -54,9 +54,7 @@ public class InstallerFactory {
installer = new PrivilegedInstaller(context);
} else {
- Log.e(TAG, "PrivilegedInstaller is enabled in prefs, but permissions are not granted!");
- // TODO: better error handling?
-
+ Log.e(TAG, "PrivilegedInstaller is enabled in prefs, but not working correctly!");
// fallback to default installer
installer = new DefaultInstaller(context);
}
diff --git a/app/src/main/java/org/fdroid/fdroid/installer/InstallerService.java b/app/src/main/java/org/fdroid/fdroid/installer/InstallerService.java
index 7e08806c2..e12282770 100644
--- a/app/src/main/java/org/fdroid/fdroid/installer/InstallerService.java
+++ b/app/src/main/java/org/fdroid/fdroid/installer/InstallerService.java
@@ -23,6 +23,9 @@ import android.app.IntentService;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
+import android.os.Parcelable;
+
+import org.fdroid.fdroid.data.Apk;
/**
* This service handles the install process of apk files and
@@ -49,14 +52,17 @@ public class InstallerService extends IntentService {
@Override
protected void onHandleIntent(Intent intent) {
- String packageName = intent.getStringExtra(Installer.EXTRA_PACKAGE_NAME);
- Installer installer = InstallerFactory.create(this, packageName);
+ Parcelable apkParcel = intent.getParcelableExtra(Installer.EXTRA_APK);
+ Apk apk = apkParcel == null ? null : new Apk(apkParcel);
+
+ Installer installer = InstallerFactory.create(this, apk);
if (ACTION_INSTALL.equals(intent.getAction())) {
Uri uri = intent.getData();
Uri downloadUri = intent.getParcelableExtra(Installer.EXTRA_DOWNLOAD_URI);
- installer.installPackage(uri, downloadUri, packageName);
+ installer.installPackage(uri, downloadUri, apk);
} else if (ACTION_UNINSTALL.equals(intent.getAction())) {
+ String packageName = intent.getStringExtra(Installer.EXTRA_PACKAGE_NAME);
installer.uninstallPackage(packageName);
}
}
@@ -67,14 +73,14 @@ public class InstallerService extends IntentService {
* @param context this app's {@link Context}
* @param localApkUri {@link Uri} pointing to (downloaded) local apk file
* @param downloadUri {@link Uri} where the apk has been downloaded from
- * @param packageName package name of the app that should be installed
+ * @param apk apk object of app that should be installed
*/
- public static void install(Context context, Uri localApkUri, Uri downloadUri, String packageName) {
+ public static void install(Context context, Uri localApkUri, Uri downloadUri, Apk apk) {
Intent intent = new Intent(context, InstallerService.class);
intent.setAction(ACTION_INSTALL);
intent.setData(localApkUri);
intent.putExtra(Installer.EXTRA_DOWNLOAD_URI, downloadUri);
- intent.putExtra(Installer.EXTRA_PACKAGE_NAME, packageName);
+ intent.putExtra(Installer.EXTRA_APK, apk.toContentValues());
context.startService(intent);
}
diff --git a/app/src/main/java/org/fdroid/fdroid/installer/PrivilegedInstaller.java b/app/src/main/java/org/fdroid/fdroid/installer/PrivilegedInstaller.java
index ec8039880..ee409c1c9 100644
--- a/app/src/main/java/org/fdroid/fdroid/installer/PrivilegedInstaller.java
+++ b/app/src/main/java/org/fdroid/fdroid/installer/PrivilegedInstaller.java
@@ -31,6 +31,7 @@ import android.os.RemoteException;
import android.util.Log;
import org.fdroid.fdroid.R;
+import org.fdroid.fdroid.data.Apk;
import org.fdroid.fdroid.privileged.IPrivilegedCallback;
import org.fdroid.fdroid.privileged.IPrivilegedService;
@@ -297,7 +298,7 @@ public class PrivilegedInstaller extends Installer {
}
@Override
- protected void installPackage(final Uri localApkUri, final Uri downloadUri, String packageName) {
+ protected void installPackageInternal(final Uri localApkUri, final Uri downloadUri, Apk apk) {
sendBroadcastInstall(downloadUri, Installer.ACTION_INSTALL_STARTED);
ServiceConnection mServiceConnection = new ServiceConnection() {
@@ -396,4 +397,9 @@ public class PrivilegedInstaller extends Installer {
return true;
}
+ @Override
+ protected boolean supportsContentUri() {
+ // TODO: correct?
+ return false;
+ }
}
diff --git a/app/src/main/java/org/fdroid/fdroid/net/DownloaderService.java b/app/src/main/java/org/fdroid/fdroid/net/DownloaderService.java
index d3e7e6e90..98fcb4c21 100644
--- a/app/src/main/java/org/fdroid/fdroid/net/DownloaderService.java
+++ b/app/src/main/java/org/fdroid/fdroid/net/DownloaderService.java
@@ -37,6 +37,7 @@ import android.text.TextUtils;
import org.fdroid.fdroid.ProgressListener;
import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.data.SanitizedFile;
+import org.fdroid.fdroid.installer.ApkCache;
import java.io.File;
import java.io.IOException;
@@ -196,7 +197,7 @@ public class DownloaderService extends Service {
*/
protected void handleIntent(Intent intent) {
final Uri uri = intent.getData();
- final SanitizedFile localFile = Utils.getApkDownloadPath(this, uri);
+ final SanitizedFile localFile = ApkCache.getApkDownloadPath(this, uri);
sendBroadcast(uri, Downloader.ACTION_STARTED, localFile);
try {
diff --git a/app/src/main/res/xml/apk_file_provider.xml b/app/src/main/res/xml/apk_file_provider.xml
new file mode 100644
index 000000000..f5475388c
--- /dev/null
+++ b/app/src/main/res/xml/apk_file_provider.xml
@@ -0,0 +1,6 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/test/java/org/fdroid/fdroid/UtilsTest.java b/app/src/test/java/org/fdroid/fdroid/UtilsTest.java
index 7ec649ca3..286e97664 100644
--- a/app/src/test/java/org/fdroid/fdroid/UtilsTest.java
+++ b/app/src/test/java/org/fdroid/fdroid/UtilsTest.java
@@ -3,16 +3,12 @@ package org.fdroid.fdroid;
import android.content.Context;
-import org.apache.commons.io.FileUtils;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricGradleTestRunner;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config;
-import java.io.File;
-import java.io.IOException;
-
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
@@ -144,40 +140,4 @@ public class UtilsTest {
// TODO write tests that work with a Certificate
- @Test
- public void testClearOldFiles() throws IOException, InterruptedException {
- File tempDir = new File(System.getProperty("java.io.tmpdir"));
- assertTrue(tempDir.isDirectory());
- assertTrue(tempDir.canWrite());
-
- File dir = new File(tempDir, "F-Droid-test.clearOldFiles");
- FileUtils.deleteQuietly(dir);
- assertTrue(dir.mkdirs());
- assertTrue(dir.isDirectory());
-
- File first = new File(dir, "first");
- first.deleteOnExit();
-
- File second = new File(dir, "second");
- second.deleteOnExit();
-
- assertFalse(first.exists());
- assertFalse(second.exists());
-
- assertTrue(first.createNewFile());
- assertTrue(first.exists());
-
- Thread.sleep(7000);
- assertTrue(second.createNewFile());
- assertTrue(second.exists());
-
- Utils.clearOldFiles(dir, 3);
- assertFalse(first.exists());
- assertTrue(second.exists());
-
- Thread.sleep(7000);
- Utils.clearOldFiles(dir, 3);
- assertFalse(first.exists());
- assertFalse(second.exists());
- }
}
diff --git a/app/src/test/java/org/fdroid/fdroid/installer/ApkCacheTest.java b/app/src/test/java/org/fdroid/fdroid/installer/ApkCacheTest.java
new file mode 100644
index 000000000..13ca70fea
--- /dev/null
+++ b/app/src/test/java/org/fdroid/fdroid/installer/ApkCacheTest.java
@@ -0,0 +1,56 @@
+package org.fdroid.fdroid.installer;
+
+import org.apache.commons.io.FileUtils;
+import org.fdroid.fdroid.BuildConfig;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricGradleTestRunner;
+import org.robolectric.annotation.Config;
+
+import java.io.File;
+import java.io.IOException;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+@Config(constants = BuildConfig.class)
+@RunWith(RobolectricGradleTestRunner.class)
+public class ApkCacheTest {
+
+ @Test
+ public void testClearOldFiles() throws IOException, InterruptedException {
+ File tempDir = new File(System.getProperty("java.io.tmpdir"));
+ assertTrue(tempDir.isDirectory());
+ assertTrue(tempDir.canWrite());
+
+ File dir = new File(tempDir, "F-Droid-test.clearOldFiles");
+ FileUtils.deleteQuietly(dir);
+ assertTrue(dir.mkdirs());
+ assertTrue(dir.isDirectory());
+
+ File first = new File(dir, "first");
+ first.deleteOnExit();
+
+ File second = new File(dir, "second");
+ second.deleteOnExit();
+
+ assertFalse(first.exists());
+ assertFalse(second.exists());
+
+ assertTrue(first.createNewFile());
+ assertTrue(first.exists());
+
+ Thread.sleep(7000);
+ assertTrue(second.createNewFile());
+ assertTrue(second.exists());
+
+ ApkCache.clearOldFiles(dir, 3);
+ assertFalse(first.exists());
+ assertTrue(second.exists());
+
+ Thread.sleep(7000);
+ ApkCache.clearOldFiles(dir, 3);
+ assertFalse(first.exists());
+ assertFalse(second.exists());
+ }
+}