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: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
 | 
			
		||||
            android:name="android.app.default_searchable"
 | 
			
		||||
            android:value=".FDroid" />
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
@ -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();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -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;
 | 
			
		||||
@ -271,58 +269,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].*")) {
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										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.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) {
 | 
			
		||||
 | 
			
		||||
@ -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,10 @@ public class DefaultInstaller extends Installer {
 | 
			
		||||
    protected boolean isUnattended() {
 | 
			
		||||
        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!");
 | 
			
		||||
        }
 | 
			
		||||
        // 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"))) {
 | 
			
		||||
            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"))) {
 | 
			
		||||
            throw new RuntimeException("PackageInstaller >= Android N only supports content scheme!");
 | 
			
		||||
        }
 | 
			
		||||
@ -91,26 +91,28 @@ 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);
 | 
			
		||||
 | 
			
		||||
            // 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);
 | 
			
		||||
 | 
			
		||||
            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
 | 
			
		||||
                // deprecated in Android 4.1
 | 
			
		||||
                intent.putExtra(Intent.EXTRA_ALLOW_REPLACE, true);
 | 
			
		||||
            }
 | 
			
		||||
            intent.putExtra(Intent.EXTRA_ALLOW_REPLACE, true);
 | 
			
		||||
        } else if (Build.VERSION.SDK_INT < 24) { // TODO: Use Build.VERSION_CODES.N
 | 
			
		||||
            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);
 | 
			
		||||
            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 {
 | 
			
		||||
@ -171,12 +173,6 @@ public class DefaultInstallerActivity extends FragmentActivity {
 | 
			
		||||
                    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) {
 | 
			
		||||
                    case Activity.RESULT_OK:
 | 
			
		||||
                        installer.sendBroadcastInstall(downloadUri,
 | 
			
		||||
 | 
			
		||||
@ -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;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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() {
 | 
			
		||||
 | 
			
		||||
@ -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();
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
@ -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);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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 {
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										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 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.assertNotNull;
 | 
			
		||||
@ -169,40 +165,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());
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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