clean up content:// vs file:// logic in installation process

This hopefully makes apparent which pieces are only related to APKs, and
which pieces are used for all installable file types (media, OTA ZIPs, etc)

ExtensionInstaller only works on < android-20 anyway, so that's self-
enforcing in terms of URI scheme: it'll only ever see file:// URIs.
This commit is contained in:
Hans-Christoph Steiner 2018-03-21 12:00:51 +01:00
parent 32e5471d6c
commit 72fcc3d2c5
6 changed files with 36 additions and 54 deletions

View File

@ -25,7 +25,6 @@ import android.content.pm.PackageInfo;
import android.net.Uri; import android.net.Uri;
import android.os.Build; import android.os.Build;
import android.support.v4.content.FileProvider; import android.support.v4.content.FileProvider;
import org.fdroid.fdroid.BuildConfig; import org.fdroid.fdroid.BuildConfig;
import org.fdroid.fdroid.data.Apk; import org.fdroid.fdroid.data.Apk;
import org.fdroid.fdroid.data.SanitizedFile; import org.fdroid.fdroid.data.SanitizedFile;
@ -34,7 +33,8 @@ import java.io.File;
import java.io.IOException; import java.io.IOException;
/** /**
* This class has helper methods for preparing apks for installation. * Helper methods for preparing APKs and arbitrary files for installation,
* either locally or for sending via bluetooth.
* <p/> * <p/>
* APK handling for installations: * APK handling for installations:
* 1. APKs are downloaded into a cache directory that is either created on SD card * 1. APKs are downloaded into a cache directory that is either created on SD card
@ -56,34 +56,44 @@ public class ApkFileProvider extends FileProvider {
} }
/** /**
* Copies the APK into private data directory of F-Droid and returns a "file" or "content" Uri * Copies the APK into private data directory of F-Droid and returns a
* to be used for installation. * {@code file://} or {@code content://} URI to be used for the
* actual installation process. Only APKs will ever use a {@code content://}
* URI, any other file will always use a {@code file://} URI since F-Droid
* itself handles their whole installation process.
*/ */
public static Uri getSafeUri(Context context, Uri localApkUri, Apk expectedApk, boolean useContentUri) public static Uri getSafeUri(Context context, Uri localApkUri, Apk expectedApk)
throws IOException { throws IOException {
File apkFile = new File(localApkUri.getPath()); File apkFile = new File(localApkUri.getPath());
SanitizedFile tempApkFile = ApkCache.copyApkFromCacheToFiles(context, apkFile, expectedApk); SanitizedFile tempApkFile = ApkCache.copyApkFromCacheToFiles(context, apkFile, expectedApk);
return getSafeUri(context, tempApkFile, useContentUri); return getSafeUri(context, tempApkFile,
Build.VERSION.SDK_INT >= 24 && expectedApk.isApk());
} }
private static Uri getSafeUri(Context context, SanitizedFile tempApkFile, boolean useContentUri) { /**
* Return a {@link Uri} for all install processes to install this package
* from. This supports APKs and all other supported files. It also
* supports all installation methods, e.g. default, privileged, etc.
* It can return either a {@code content://} or {@code file://} URI.
* <p>
* APKs need to be world readable, so that the Android system installer
* is able to read it. Saving it into external storage to send it to 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.
*/
private static Uri getSafeUri(Context context, SanitizedFile tempFile, boolean useContentUri) {
if (useContentUri) { if (useContentUri) {
// return a content Uri using support libs FileProvider Uri apkUri = getUriForFile(context, AUTHORITY, tempFile);
Uri apkUri = getUriForFile(context, AUTHORITY, tempApkFile);
context.grantUriPermission("org.fdroid.fdroid.privileged", apkUri, Intent.FLAG_GRANT_READ_URI_PERMISSION); context.grantUriPermission("org.fdroid.fdroid.privileged", apkUri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
context.grantUriPermission("com.android.bluetooth", apkUri, Intent.FLAG_GRANT_READ_URI_PERMISSION); context.grantUriPermission("com.android.bluetooth", apkUri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
return apkUri; return apkUri;
} }
// Need the apk to be world readable, so that the installer is able to read it. tempFile.setReadable(true, false);
// 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.
tempApkFile.setReadable(true, false);
return Uri.fromFile(tempApkFile); return Uri.fromFile(tempFile);
} }
} }

View File

@ -24,8 +24,6 @@ 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.data.Apk; import org.fdroid.fdroid.data.Apk;
/** /**
@ -82,10 +80,4 @@ 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;
}
} }

View File

@ -24,7 +24,6 @@ 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 org.fdroid.fdroid.BuildConfig; import org.fdroid.fdroid.BuildConfig;
import org.fdroid.fdroid.data.Apk; import org.fdroid.fdroid.data.Apk;
import org.fdroid.fdroid.privileged.install.InstallExtensionDialogActivity; import org.fdroid.fdroid.privileged.install.InstallExtensionDialogActivity;
@ -33,11 +32,16 @@ import java.io.File;
/** /**
* Special Installer that is only useful to install the Privileged Extension apk * Special Installer that is only useful to install the Privileged Extension apk
* as a privileged app into the system partition of Android. * as a privileged app into the system partition of Android. It is deprecated
* because it cannot work on Android versions newer than {@code android-20} or so,
* due to increased SELinux enforcement that restricts what even root can do.
* <p/> * <p/>
* This is installer requires user interaction and thus install/uninstall directly * This is installer requires user interaction and thus install/uninstall directly
* return PendingIntents. * return PendingIntents.
*
* @see <a href="https://www.androidauthority.com/chainfire-rooting-android-lollipop-541458/">Chainfire talks Android Lollipop and the future of rooting</a>
*/ */
@Deprecated
public class ExtensionInstaller extends Installer { public class ExtensionInstaller extends Installer {
ExtensionInstaller(Context context, Apk apk) { ExtensionInstaller(Context context, Apk apk) {
@ -94,9 +98,4 @@ public class ExtensionInstaller extends Installer {
protected boolean isUnattended() { protected boolean isUnattended() {
return false; return false;
} }
@Override
protected boolean supportsContentUri() {
return false;
}
} }

View File

@ -85,10 +85,4 @@ public class FileInstaller extends Installer {
protected boolean isUnattended() { protected boolean isUnattended() {
return false; return false;
} }
@Override
protected boolean supportsContentUri() {
return false;
}
} }

View File

@ -31,7 +31,6 @@ 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 android.util.Log;
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.ApkProvider; import org.fdroid.fdroid.data.ApkProvider;
@ -144,7 +143,7 @@ public abstract class Installer {
PackageManager pm = context.getPackageManager(); PackageManager pm = context.getPackageManager();
if (Build.VERSION.SDK_INT >= 24 && ( if (Build.VERSION.SDK_INT >= 24 && (
pm.getInstallerPackageName(apk.packageName).equals("com.android.packageinstaller") pm.getInstallerPackageName(apk.packageName).equals("com.android.packageinstaller")
|| pm.getInstallerPackageName(apk.packageName).equals("com.google.android.packageinstaller"))) { || pm.getInstallerPackageName(apk.packageName).equals("com.google.android.packageinstaller"))) {
Utils.debugLog(TAG, "Falling back to default installer for uninstall"); Utils.debugLog(TAG, "Falling back to default installer for uninstall");
Intent intent = new Intent(context, DefaultInstallerActivity.class); Intent intent = new Intent(context, DefaultInstallerActivity.class);
intent.setAction(DefaultInstallerActivity.ACTION_UNINSTALL_PACKAGE); intent.setAction(DefaultInstallerActivity.ACTION_UNINSTALL_PACKAGE);
@ -195,7 +194,7 @@ public abstract class Installer {
sendBroadcastUninstall(action, pendingIntent, null); sendBroadcastUninstall(action, pendingIntent, null);
} }
void sendBroadcastUninstall(String action, PendingIntent pendingIntent, String errorMessage) { private void sendBroadcastUninstall(String action, PendingIntent pendingIntent, String errorMessage) {
Uri uri = Uri.fromParts("package", apk.packageName, null); Uri uri = Uri.fromParts("package", apk.packageName, null);
Intent intent = new Intent(action); Intent intent = new Intent(action);
@ -247,7 +246,7 @@ public abstract class Installer {
try { try {
// move apk file to private directory for installation and check hash // move apk file to private directory for installation and check hash
sanitizedUri = ApkFileProvider.getSafeUri(context, localApkUri, apk, supportsContentUri()); sanitizedUri = ApkFileProvider.getSafeUri(context, localApkUri, apk);
} catch (IOException e) { } catch (IOException e) {
Log.e(TAG, e.getMessage(), e); Log.e(TAG, e.getMessage(), e);
sendBroadcastInstall(downloadUri, Installer.ACTION_INSTALL_INTERRUPTED, e.getMessage()); sendBroadcastInstall(downloadUri, Installer.ACTION_INSTALL_INTERRUPTED, e.getMessage());
@ -270,7 +269,7 @@ public abstract class Installer {
Log.e(TAG, e.getMessage(), e); Log.e(TAG, e.getMessage(), e);
Log.e(TAG, "Falling back to AOSP DefaultInstaller!"); Log.e(TAG, "Falling back to AOSP DefaultInstaller!");
DefaultInstaller defaultInstaller = new DefaultInstaller(context, apk); DefaultInstaller defaultInstaller = new DefaultInstaller(context, apk);
// https://code.google.com/p/android/issues/detail?id=205827 // https://issuetracker.google.com/issues/37091886
if (Build.VERSION.SDK_INT >= 24) { if (Build.VERSION.SDK_INT >= 24) {
// content scheme for N and above // content scheme for N and above
defaultInstaller.installPackageInternal(sanitizedUri, downloadUri); defaultInstaller.installPackageInternal(sanitizedUri, downloadUri);
@ -298,10 +297,4 @@ public abstract class Installer {
* uninstall activities, without the system enforcing a user prompt. * uninstall activities, without the system enforcing a user prompt.
*/ */
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();
} }

View File

@ -27,7 +27,6 @@ import android.content.Intent;
import android.content.ServiceConnection; import android.content.ServiceConnection;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.net.Uri; import android.net.Uri;
import android.os.Build;
import android.os.IBinder; import android.os.IBinder;
import android.os.RemoteException; import android.os.RemoteException;
import android.util.Log; import android.util.Log;
@ -404,9 +403,4 @@ public class PrivilegedInstaller extends Installer {
protected boolean isUnattended() { protected boolean isUnattended() {
return true; return true;
} }
@Override
protected boolean supportsContentUri() {
return Build.VERSION.SDK_INT >= 24;
}
} }