Merge branch 'apkfileprovider' into 'master'
Provide content Uris to downloaded apks via FileProvider * moves apk verification back inside the Installer class * uses support libs FileProvider for content Uris * move apk file caching and storage methods into ApkFileProvider class Some of the ugly version checks for Android N can be removed after Android N has been released. Unfortunately Google decided to keep SDK version at 23 for Android N dev preview and only change the CODENAME, thus ``Build.VERSION.SDK_INT <= Build.VERSION_CODES.M`` returns true on Android N preview :/ , see https://commonsware.com/blog/2016/03/17/backwards-compatibility-n-developer-preview.html Tested on Android N dev preview 3 emulator, Android 6 stock and Android 5.1 rooted with priv extension. See merge request !331
This commit is contained in:
commit
34aa8ab062
@ -95,6 +95,16 @@
|
|||||||
android:name="org.fdroid.fdroid.data.InstalledAppProvider"
|
android:name="org.fdroid.fdroid.data.InstalledAppProvider"
|
||||||
android:exported="false"/>
|
android:exported="false"/>
|
||||||
|
|
||||||
|
<provider
|
||||||
|
android:name="org.fdroid.fdroid.installer.ApkFileProvider"
|
||||||
|
android:authorities="org.fdroid.fdroid.installer.ApkFileProvider"
|
||||||
|
android:exported="false"
|
||||||
|
android:grantUriPermissions="true">
|
||||||
|
<meta-data
|
||||||
|
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||||
|
android:resource="@xml/apk_file_provider" />
|
||||||
|
</provider>
|
||||||
|
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.app.default_searchable"
|
android:name="android.app.default_searchable"
|
||||||
android:value=".FDroid" />
|
android:value=".FDroid" />
|
||||||
|
@ -971,7 +971,7 @@ public class AppDetails extends AppCompatActivity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void initiateInstall(Apk apk) {
|
private void initiateInstall(Apk apk) {
|
||||||
Installer installer = InstallerFactory.create(this, apk.packageName);
|
Installer installer = InstallerFactory.create(this, apk);
|
||||||
Intent intent = installer.getPermissionScreen(apk);
|
Intent intent = installer.getPermissionScreen(apk);
|
||||||
if (intent != null) {
|
if (intent != null) {
|
||||||
// permission screen required
|
// permission screen required
|
||||||
@ -990,7 +990,7 @@ public class AppDetails extends AppCompatActivity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void uninstallApk(String packageName) {
|
private void uninstallApk(String packageName) {
|
||||||
Installer installer = InstallerFactory.create(this, packageName);
|
Installer installer = InstallerFactory.create(this, null);
|
||||||
Intent intent = installer.getUninstallScreen(packageName);
|
Intent intent = installer.getUninstallScreen(packageName);
|
||||||
if (intent != null) {
|
if (intent != null) {
|
||||||
// uninstall screen required
|
// uninstall screen required
|
||||||
|
@ -9,6 +9,7 @@ import android.os.Process;
|
|||||||
import android.os.SystemClock;
|
import android.os.SystemClock;
|
||||||
|
|
||||||
import org.apache.commons.io.FileUtils;
|
import org.apache.commons.io.FileUtils;
|
||||||
|
import org.fdroid.fdroid.installer.ApkCache;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
|
||||||
@ -51,7 +52,7 @@ public class CleanCacheService extends IntentService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Process.setThreadPriority(Process.THREAD_PRIORITY_LOWEST);
|
Process.setThreadPriority(Process.THREAD_PRIORITY_LOWEST);
|
||||||
Utils.clearOldFiles(Utils.getApkCacheDir(this), Preferences.get().getKeepCacheTime());
|
ApkCache.clearApkCache(this);
|
||||||
deleteStrayIndexFiles();
|
deleteStrayIndexFiles();
|
||||||
deleteOldInstallerFiles();
|
deleteOldInstallerFiles();
|
||||||
}
|
}
|
||||||
|
@ -33,9 +33,7 @@ import android.util.Log;
|
|||||||
import com.nostra13.universalimageloader.core.DisplayImageOptions;
|
import com.nostra13.universalimageloader.core.DisplayImageOptions;
|
||||||
import com.nostra13.universalimageloader.core.assist.ImageScaleType;
|
import com.nostra13.universalimageloader.core.assist.ImageScaleType;
|
||||||
import com.nostra13.universalimageloader.core.display.FadeInBitmapDisplayer;
|
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.compat.FileCompat;
|
||||||
import org.fdroid.fdroid.data.Repo;
|
import org.fdroid.fdroid.data.Repo;
|
||||||
import org.fdroid.fdroid.data.SanitizedFile;
|
import org.fdroid.fdroid.data.SanitizedFile;
|
||||||
@ -271,58 +269,6 @@ public final class Utils {
|
|||||||
return Uri.parse("package:" + packageName);
|
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) {
|
public static String calcFingerprint(String keyHexString) {
|
||||||
if (TextUtils.isEmpty(keyHexString)
|
if (TextUtils.isEmpty(keyHexString)
|
||||||
|| keyHexString.matches(".*[^a-fA-F0-9].*")) {
|
|| keyHexString.matches(".*[^a-fA-F0-9].*")) {
|
||||||
|
165
app/src/main/java/org/fdroid/fdroid/installer/ApkCache.java
Normal file
165
app/src/main/java/org/fdroid/fdroid/installer/ApkCache.java
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016 Dominik Schürmann <dominik@dominikschuermann.de>
|
||||||
|
*
|
||||||
|
* 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,76 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016 Dominik Schürmann <dominik@dominikschuermann.de>
|
||||||
|
*
|
||||||
|
* 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.
|
||||||
|
* <p/>
|
||||||
|
* APK handling for installations:
|
||||||
|
* 1. APKs are downloaded into a cache directory that is either created on SD card
|
||||||
|
* <i>"/Android/data/[app_package_name]/cache/apks"</i> (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,
|
||||||
|
* <i>"/data/data/[app_package_name]/files/install-$random.apk"</i>.
|
||||||
|
* 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -26,15 +26,9 @@ import android.net.Uri;
|
|||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import org.apache.commons.io.FileUtils;
|
|
||||||
import org.fdroid.fdroid.Hasher;
|
|
||||||
import org.fdroid.fdroid.Utils;
|
import org.fdroid.fdroid.Utils;
|
||||||
import org.fdroid.fdroid.data.Apk;
|
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.Arrays;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
|
|
||||||
@ -47,13 +41,11 @@ public class ApkVerifier {
|
|||||||
|
|
||||||
private static final String TAG = "ApkVerifier";
|
private static final String TAG = "ApkVerifier";
|
||||||
|
|
||||||
private final Context context;
|
|
||||||
private final Uri localApkUri;
|
private final Uri localApkUri;
|
||||||
private final Apk expectedApk;
|
private final Apk expectedApk;
|
||||||
private final PackageManager pm;
|
private final PackageManager pm;
|
||||||
|
|
||||||
ApkVerifier(Context context, Uri localApkUri, Apk expectedApk) {
|
ApkVerifier(Context context, Uri localApkUri, Apk expectedApk) {
|
||||||
this.context = context;
|
|
||||||
this.localApkUri = localApkUri;
|
this.localApkUri = localApkUri;
|
||||||
this.expectedApk = expectedApk;
|
this.expectedApk = expectedApk;
|
||||||
this.pm = context.getPackageManager();
|
this.pm = context.getPackageManager();
|
||||||
@ -107,67 +99,6 @@ public class ApkVerifier {
|
|||||||
return new HashSet<>(Arrays.asList(localApkInfo.requestedPermissions));
|
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 static class ApkVerificationException extends Exception {
|
||||||
|
|
||||||
public ApkVerificationException(String message) {
|
public ApkVerificationException(String message) {
|
||||||
|
@ -23,8 +23,10 @@ import android.app.PendingIntent;
|
|||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
|
import android.os.Build;
|
||||||
|
|
||||||
import org.fdroid.fdroid.Utils;
|
import org.fdroid.fdroid.Utils;
|
||||||
|
import org.fdroid.fdroid.data.Apk;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
|
||||||
@ -44,7 +46,7 @@ public class DefaultInstaller extends Installer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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);
|
sendBroadcastInstall(downloadUri, Installer.ACTION_INSTALL_STARTED);
|
||||||
|
|
||||||
Utils.debugLog(TAG, "DefaultInstaller uri: " + localApkUri + " file: " + new File(localApkUri.getPath()));
|
Utils.debugLog(TAG, "DefaultInstaller uri: " + localApkUri + " file: " + new File(localApkUri.getPath()));
|
||||||
@ -86,4 +88,10 @@ public class DefaultInstaller extends Installer {
|
|||||||
protected boolean isUnattended() {
|
protected boolean isUnattended() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean supportsContentUri() {
|
||||||
|
// Android N only supports content Uris
|
||||||
|
return Build.VERSION.SDK_INT >= 24; // TODO: Use Build.VERSION_CODES.N
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -79,11 +79,11 @@ public class DefaultInstallerActivity extends FragmentActivity {
|
|||||||
throw new RuntimeException("Set the data uri to point to an apk location!");
|
throw new RuntimeException("Set the data uri to point to an apk location!");
|
||||||
}
|
}
|
||||||
// https://code.google.com/p/android/issues/detail?id=205827
|
// https://code.google.com/p/android/issues/detail?id=205827
|
||||||
if ((Build.VERSION.SDK_INT <= Build.VERSION_CODES.M)
|
if ((Build.VERSION.SDK_INT < 24) // TODO: Use Build.VERSION_CODES.N
|
||||||
&& (!uri.getScheme().equals("file"))) {
|
&& (!uri.getScheme().equals("file"))) {
|
||||||
throw new RuntimeException("PackageInstaller <= Android 6 only supports file scheme!");
|
throw new RuntimeException("PackageInstaller < Android N only supports file scheme!");
|
||||||
}
|
}
|
||||||
if (("N".equals(Build.VERSION.CODENAME))
|
if ((Build.VERSION.SDK_INT >= 24) // TODO: Use Build.VERSION_CODES.N
|
||||||
&& (!uri.getScheme().equals("content"))) {
|
&& (!uri.getScheme().equals("content"))) {
|
||||||
throw new RuntimeException("PackageInstaller >= Android N only supports content scheme!");
|
throw new RuntimeException("PackageInstaller >= Android N only supports content scheme!");
|
||||||
}
|
}
|
||||||
@ -91,26 +91,28 @@ public class DefaultInstallerActivity extends FragmentActivity {
|
|||||||
Intent intent = new Intent();
|
Intent intent = new Intent();
|
||||||
intent.setData(uri);
|
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) {
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
|
||||||
intent.setAction(Intent.ACTION_VIEW);
|
intent.setAction(Intent.ACTION_VIEW);
|
||||||
intent.setType("application/vnd.android.package-archive");
|
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.setAction(Intent.ACTION_INSTALL_PACKAGE);
|
||||||
|
intent.putExtra(Intent.EXTRA_RETURN_RESULT, true);
|
||||||
// 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_NOT_UNKNOWN_SOURCE, true);
|
intent.putExtra(Intent.EXTRA_NOT_UNKNOWN_SOURCE, true);
|
||||||
|
intent.putExtra(Intent.EXTRA_ALLOW_REPLACE, true);
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
|
} else if (Build.VERSION.SDK_INT < 24) { // TODO: Use Build.VERSION_CODES.N
|
||||||
// deprecated in Android 4.1
|
intent.setAction(Intent.ACTION_INSTALL_PACKAGE);
|
||||||
intent.putExtra(Intent.EXTRA_ALLOW_REPLACE, true);
|
intent.putExtra(Intent.EXTRA_RETURN_RESULT, true);
|
||||||
}
|
intent.putExtra(Intent.EXTRA_NOT_UNKNOWN_SOURCE, true);
|
||||||
|
} else { // Android N
|
||||||
|
intent.setAction(Intent.ACTION_INSTALL_PACKAGE);
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -171,12 +173,6 @@ public class DefaultInstallerActivity extends FragmentActivity {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback on N for https://gitlab.com/fdroid/fdroidclient/issues/631
|
|
||||||
if ("N".equals(Build.VERSION.CODENAME)) {
|
|
||||||
installer.sendBroadcastInstall(downloadUri, Installer.ACTION_INSTALL_COMPLETE);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (resultCode) {
|
switch (resultCode) {
|
||||||
case Activity.RESULT_OK:
|
case Activity.RESULT_OK:
|
||||||
installer.sendBroadcastInstall(downloadUri,
|
installer.sendBroadcastInstall(downloadUri,
|
||||||
|
@ -25,6 +25,7 @@ import android.content.Intent;
|
|||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
|
|
||||||
import org.fdroid.fdroid.BuildConfig;
|
import org.fdroid.fdroid.BuildConfig;
|
||||||
|
import org.fdroid.fdroid.data.Apk;
|
||||||
import org.fdroid.fdroid.privileged.install.InstallExtensionDialogActivity;
|
import org.fdroid.fdroid.privileged.install.InstallExtensionDialogActivity;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
@ -43,7 +44,7 @@ public class ExtensionInstaller extends Installer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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
|
// 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
|
// NOTE: Disabled for debug builds to be able to test official extension from repo
|
||||||
ApkSignatureVerifier signatureVerifier = new ApkSignatureVerifier(context);
|
ApkSignatureVerifier signatureVerifier = new ApkSignatureVerifier(context);
|
||||||
@ -93,4 +94,9 @@ public class ExtensionInstaller extends Installer {
|
|||||||
protected boolean isUnattended() {
|
protected boolean isUnattended() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean supportsContentUri() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,6 @@ import android.support.v4.app.NotificationCompat;
|
|||||||
import android.support.v4.app.TaskStackBuilder;
|
import android.support.v4.app.TaskStackBuilder;
|
||||||
import android.support.v4.content.LocalBroadcastManager;
|
import android.support.v4.content.LocalBroadcastManager;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import org.fdroid.fdroid.AppDetails;
|
import org.fdroid.fdroid.AppDetails;
|
||||||
import org.fdroid.fdroid.R;
|
import org.fdroid.fdroid.R;
|
||||||
@ -27,7 +26,6 @@ import org.fdroid.fdroid.net.Downloader;
|
|||||||
import org.fdroid.fdroid.net.DownloaderService;
|
import org.fdroid.fdroid.net.DownloaderService;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.security.NoSuchAlgorithmException;
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
@ -162,12 +160,12 @@ public class InstallManagerService extends Service {
|
|||||||
|
|
||||||
registerDownloaderReceivers(urlString, builder);
|
registerDownloaderReceivers(urlString, builder);
|
||||||
|
|
||||||
File apkFilePath = Utils.getApkDownloadPath(this, intent.getData());
|
File apkFilePath = ApkCache.getApkDownloadPath(this, intent.getData());
|
||||||
long apkFileSize = apkFilePath.length();
|
long apkFileSize = apkFilePath.length();
|
||||||
if (!apkFilePath.exists() || apkFileSize < apk.size) {
|
if (!apkFilePath.exists() || apkFileSize < apk.size) {
|
||||||
Utils.debugLog(TAG, "download " + urlString + " " + apkFilePath);
|
Utils.debugLog(TAG, "download " + urlString + " " + apkFilePath);
|
||||||
DownloaderService.queue(this, urlString);
|
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);
|
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_STARTED, apkFilePath);
|
||||||
sendBroadcast(intent.getData(), Downloader.ACTION_COMPLETE, 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
|
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) {
|
private void sendBroadcast(Uri uri, String action, File file) {
|
||||||
Intent intent = new Intent(action);
|
Intent intent = new Intent(action);
|
||||||
intent.setData(uri);
|
intent.setData(uri);
|
||||||
@ -238,21 +221,7 @@ public class InstallManagerService extends Service {
|
|||||||
|
|
||||||
Apk apk = ACTIVE_APKS.get(urlString);
|
Apk apk = ACTIVE_APKS.get(urlString);
|
||||||
|
|
||||||
Uri sanitizedUri;
|
InstallerService.install(context, localApkUri, downloadUri, apk);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
BroadcastReceiver interruptedReceiver = new BroadcastReceiver() {
|
BroadcastReceiver interruptedReceiver = new BroadcastReceiver() {
|
||||||
|
@ -28,6 +28,7 @@ import android.os.Build;
|
|||||||
import android.os.PatternMatcher;
|
import android.os.PatternMatcher;
|
||||||
import android.support.v4.content.LocalBroadcastManager;
|
import android.support.v4.content.LocalBroadcastManager;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
import org.fdroid.fdroid.data.Apk;
|
import org.fdroid.fdroid.data.Apk;
|
||||||
import org.fdroid.fdroid.data.ApkProvider;
|
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.InstallConfirmActivity;
|
||||||
import org.fdroid.fdroid.privileged.views.UninstallDialogActivity;
|
import org.fdroid.fdroid.privileged.views.UninstallDialogActivity;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles the actual install process. Subclasses implement the details.
|
* Handles the actual install process. Subclasses implement the details.
|
||||||
*/
|
*/
|
||||||
@ -43,6 +46,8 @@ public abstract class Installer {
|
|||||||
final Context context;
|
final Context context;
|
||||||
private final LocalBroadcastManager localBroadcastManager;
|
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_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_COMPLETE = "org.fdroid.fdroid.installer.Installer.action.INSTALL_COMPLETE";
|
||||||
public static final String ACTION_INSTALL_INTERRUPTED = "org.fdroid.fdroid.installer.Installer.action.INSTALL_INTERRUPTED";
|
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
|
* @see Intent#EXTRA_ORIGINATING_URI
|
||||||
*/
|
*/
|
||||||
static final String EXTRA_DOWNLOAD_URI = "org.fdroid.fdroid.installer.Installer.extra.DOWNLOAD_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_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_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 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) {
|
Installer(Context context) {
|
||||||
this.context = context;
|
this.context = context;
|
||||||
localBroadcastManager = LocalBroadcastManager.getInstance(context);
|
localBroadcastManager = LocalBroadcastManager.getInstance(context);
|
||||||
@ -150,8 +143,7 @@ public abstract class Installer {
|
|||||||
return intent;
|
return intent;
|
||||||
}
|
}
|
||||||
|
|
||||||
void sendBroadcastInstall(Uri downloadUri, String action,
|
void sendBroadcastInstall(Uri downloadUri, String action, PendingIntent pendingIntent) {
|
||||||
PendingIntent pendingIntent) {
|
|
||||||
sendBroadcastInstall(downloadUri, action, pendingIntent, null);
|
sendBroadcastInstall(downloadUri, action, pendingIntent, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -164,7 +156,7 @@ public abstract class Installer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void sendBroadcastInstall(Uri downloadUri, String action,
|
void sendBroadcastInstall(Uri downloadUri, String action,
|
||||||
PendingIntent pendingIntent, String errorMessage) {
|
PendingIntent pendingIntent, String errorMessage) {
|
||||||
Intent intent = new Intent(action);
|
Intent intent = new Intent(action);
|
||||||
intent.setData(downloadUri);
|
intent.setData(downloadUri);
|
||||||
intent.putExtra(Installer.EXTRA_USER_INTERACTION_PI, pendingIntent);
|
intent.putExtra(Installer.EXTRA_USER_INTERACTION_PI, pendingIntent);
|
||||||
@ -182,13 +174,12 @@ public abstract class Installer {
|
|||||||
sendBroadcastUninstall(packageName, action, null, null);
|
sendBroadcastUninstall(packageName, action, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
void sendBroadcastUninstall(String packageName, String action,
|
void sendBroadcastUninstall(String packageName, String action, PendingIntent pendingIntent) {
|
||||||
PendingIntent pendingIntent) {
|
|
||||||
sendBroadcastUninstall(packageName, action, pendingIntent, null);
|
sendBroadcastUninstall(packageName, action, pendingIntent, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
void sendBroadcastUninstall(String packageName, String action,
|
void sendBroadcastUninstall(String packageName, String action,
|
||||||
PendingIntent pendingIntent, String errorMessage) {
|
PendingIntent pendingIntent, String errorMessage) {
|
||||||
Uri uri = Uri.fromParts("package", packageName, null);
|
Uri uri = Uri.fromParts("package", packageName, null);
|
||||||
|
|
||||||
Intent intent = new Intent(action);
|
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 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
|
* @param downloadUri serves as the unique ID for all actions related to the
|
||||||
* installation of that specific APK
|
* 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);
|
protected abstract void uninstallPackage(String packageName);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -244,4 +262,9 @@ public abstract class Installer {
|
|||||||
*/
|
*/
|
||||||
protected abstract boolean isUnattended();
|
protected abstract boolean isUnattended();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return true if the Installer supports content Uris and not just file Uris
|
||||||
|
*/
|
||||||
|
protected abstract boolean supportsContentUri();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -24,6 +24,7 @@ import android.util.Log;
|
|||||||
|
|
||||||
import org.fdroid.fdroid.Preferences;
|
import org.fdroid.fdroid.Preferences;
|
||||||
import org.fdroid.fdroid.Utils;
|
import org.fdroid.fdroid.Utils;
|
||||||
|
import org.fdroid.fdroid.data.Apk;
|
||||||
|
|
||||||
public class InstallerFactory {
|
public class InstallerFactory {
|
||||||
|
|
||||||
@ -34,17 +35,16 @@ public class InstallerFactory {
|
|||||||
* Either DefaultInstaller, PrivilegedInstaller, or in the special
|
* Either DefaultInstaller, PrivilegedInstaller, or in the special
|
||||||
* case to install the "F-Droid Privileged Extension" ExtensionInstaller.
|
* case to install the "F-Droid Privileged Extension" ExtensionInstaller.
|
||||||
*
|
*
|
||||||
* @param context current {@link Context}
|
* @param context current {@link Context}
|
||||||
* @param packageName package name of apk to be installed. Required to select
|
* @param apk apk to be installed. Required to select the ExtensionInstaller.
|
||||||
* the ExtensionInstaller.
|
* If this is null, the ExtensionInstaller will never be returned.
|
||||||
* If this is null, the ExtensionInstaller will never be returned.
|
|
||||||
* @return instance of an Installer
|
* @return instance of an Installer
|
||||||
*/
|
*/
|
||||||
public static Installer create(Context context, String packageName) {
|
public static Installer create(Context context, Apk apk) {
|
||||||
Installer installer;
|
Installer installer;
|
||||||
|
|
||||||
if (packageName != null
|
if (apk != null
|
||||||
&& packageName.equals(PrivilegedInstaller.PRIVILEGED_EXTENSION_PACKAGE_NAME)) {
|
&& apk.packageName.equals(PrivilegedInstaller.PRIVILEGED_EXTENSION_PACKAGE_NAME)) {
|
||||||
// special case for "F-Droid Privileged Extension"
|
// special case for "F-Droid Privileged Extension"
|
||||||
installer = new ExtensionInstaller(context);
|
installer = new ExtensionInstaller(context);
|
||||||
} else if (isPrivilegedInstallerEnabled()) {
|
} else if (isPrivilegedInstallerEnabled()) {
|
||||||
@ -54,9 +54,7 @@ public class InstallerFactory {
|
|||||||
|
|
||||||
installer = new PrivilegedInstaller(context);
|
installer = new PrivilegedInstaller(context);
|
||||||
} else {
|
} else {
|
||||||
Log.e(TAG, "PrivilegedInstaller is enabled in prefs, but permissions are not granted!");
|
Log.e(TAG, "PrivilegedInstaller is enabled in prefs, but not working correctly!");
|
||||||
// TODO: better error handling?
|
|
||||||
|
|
||||||
// fallback to default installer
|
// fallback to default installer
|
||||||
installer = new DefaultInstaller(context);
|
installer = new DefaultInstaller(context);
|
||||||
}
|
}
|
||||||
|
@ -23,6 +23,9 @@ import android.app.IntentService;
|
|||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
|
import android.os.Parcelable;
|
||||||
|
|
||||||
|
import org.fdroid.fdroid.data.Apk;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This service handles the install process of apk files and
|
* This service handles the install process of apk files and
|
||||||
@ -49,14 +52,17 @@ public class InstallerService extends IntentService {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onHandleIntent(Intent intent) {
|
protected void onHandleIntent(Intent intent) {
|
||||||
String packageName = intent.getStringExtra(Installer.EXTRA_PACKAGE_NAME);
|
Parcelable apkParcel = intent.getParcelableExtra(Installer.EXTRA_APK);
|
||||||
Installer installer = InstallerFactory.create(this, packageName);
|
Apk apk = apkParcel == null ? null : new Apk(apkParcel);
|
||||||
|
|
||||||
|
Installer installer = InstallerFactory.create(this, apk);
|
||||||
|
|
||||||
if (ACTION_INSTALL.equals(intent.getAction())) {
|
if (ACTION_INSTALL.equals(intent.getAction())) {
|
||||||
Uri uri = intent.getData();
|
Uri uri = intent.getData();
|
||||||
Uri downloadUri = intent.getParcelableExtra(Installer.EXTRA_DOWNLOAD_URI);
|
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())) {
|
} else if (ACTION_UNINSTALL.equals(intent.getAction())) {
|
||||||
|
String packageName = intent.getStringExtra(Installer.EXTRA_PACKAGE_NAME);
|
||||||
installer.uninstallPackage(packageName);
|
installer.uninstallPackage(packageName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -67,14 +73,14 @@ public class InstallerService extends IntentService {
|
|||||||
* @param context this app's {@link Context}
|
* @param context this app's {@link Context}
|
||||||
* @param localApkUri {@link Uri} pointing to (downloaded) local apk file
|
* @param localApkUri {@link Uri} pointing to (downloaded) local apk file
|
||||||
* @param downloadUri {@link Uri} where the apk has been downloaded from
|
* @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 intent = new Intent(context, InstallerService.class);
|
||||||
intent.setAction(ACTION_INSTALL);
|
intent.setAction(ACTION_INSTALL);
|
||||||
intent.setData(localApkUri);
|
intent.setData(localApkUri);
|
||||||
intent.putExtra(Installer.EXTRA_DOWNLOAD_URI, downloadUri);
|
intent.putExtra(Installer.EXTRA_DOWNLOAD_URI, downloadUri);
|
||||||
intent.putExtra(Installer.EXTRA_PACKAGE_NAME, packageName);
|
intent.putExtra(Installer.EXTRA_APK, apk.toContentValues());
|
||||||
context.startService(intent);
|
context.startService(intent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -31,6 +31,7 @@ import android.os.RemoteException;
|
|||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import org.fdroid.fdroid.R;
|
import org.fdroid.fdroid.R;
|
||||||
|
import org.fdroid.fdroid.data.Apk;
|
||||||
import org.fdroid.fdroid.privileged.IPrivilegedCallback;
|
import org.fdroid.fdroid.privileged.IPrivilegedCallback;
|
||||||
import org.fdroid.fdroid.privileged.IPrivilegedService;
|
import org.fdroid.fdroid.privileged.IPrivilegedService;
|
||||||
|
|
||||||
@ -297,7 +298,7 @@ public class PrivilegedInstaller extends Installer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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);
|
sendBroadcastInstall(downloadUri, Installer.ACTION_INSTALL_STARTED);
|
||||||
|
|
||||||
ServiceConnection mServiceConnection = new ServiceConnection() {
|
ServiceConnection mServiceConnection = new ServiceConnection() {
|
||||||
@ -396,4 +397,9 @@ public class PrivilegedInstaller extends Installer {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean supportsContentUri() {
|
||||||
|
// TODO: correct?
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -37,6 +37,7 @@ import android.text.TextUtils;
|
|||||||
import org.fdroid.fdroid.ProgressListener;
|
import org.fdroid.fdroid.ProgressListener;
|
||||||
import org.fdroid.fdroid.Utils;
|
import org.fdroid.fdroid.Utils;
|
||||||
import org.fdroid.fdroid.data.SanitizedFile;
|
import org.fdroid.fdroid.data.SanitizedFile;
|
||||||
|
import org.fdroid.fdroid.installer.ApkCache;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@ -196,7 +197,7 @@ public class DownloaderService extends Service {
|
|||||||
*/
|
*/
|
||||||
protected void handleIntent(Intent intent) {
|
protected void handleIntent(Intent intent) {
|
||||||
final Uri uri = intent.getData();
|
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);
|
sendBroadcast(uri, Downloader.ACTION_STARTED, localFile);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
6
app/src/main/res/xml/apk_file_provider.xml
Normal file
6
app/src/main/res/xml/apk_file_provider.xml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<paths>
|
||||||
|
<files-path
|
||||||
|
name="files"
|
||||||
|
path="." />
|
||||||
|
</paths>
|
@ -3,16 +3,12 @@ package org.fdroid.fdroid;
|
|||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
|
||||||
import org.apache.commons.io.FileUtils;
|
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.runner.RunWith;
|
import org.junit.runner.RunWith;
|
||||||
import org.robolectric.RobolectricGradleTestRunner;
|
import org.robolectric.RobolectricGradleTestRunner;
|
||||||
import org.robolectric.RuntimeEnvironment;
|
import org.robolectric.RuntimeEnvironment;
|
||||||
import org.robolectric.annotation.Config;
|
import org.robolectric.annotation.Config;
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
import static org.junit.Assert.assertFalse;
|
import static org.junit.Assert.assertFalse;
|
||||||
import static org.junit.Assert.assertNotNull;
|
import static org.junit.Assert.assertNotNull;
|
||||||
@ -169,40 +165,4 @@ public class UtilsTest {
|
|||||||
|
|
||||||
// TODO write tests that work with a Certificate
|
// 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());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user