/*
* Copyright (C) 2016 Blue Jay Wireless
* 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.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.Uri;
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;
import org.fdroid.fdroid.privileged.views.AppDiff;
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.
*/
public abstract class Installer {
private static final String TAG = "Installer";
final Context context;
final Apk apk;
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";
public static final String ACTION_INSTALL_USER_INTERACTION = "org.fdroid.fdroid.installer.Installer.action.INSTALL_USER_INTERACTION";
public static final String ACTION_UNINSTALL_STARTED = "org.fdroid.fdroid.installer.Installer.action.UNINSTALL_STARTED";
public static final String ACTION_UNINSTALL_COMPLETE = "org.fdroid.fdroid.installer.Installer.action.UNINSTALL_COMPLETE";
public static final String ACTION_UNINSTALL_INTERRUPTED = "org.fdroid.fdroid.installer.Installer.action.UNINSTALL_INTERRUPTED";
public static final String ACTION_UNINSTALL_USER_INTERACTION = "org.fdroid.fdroid.installer.Installer.action.UNINSTALL_USER_INTERACTION";
/**
* The URI where the APK was originally downloaded from. This is also used
* as the unique ID representing this in the whole install process in
* {@link InstallManagerService}, there is is generally known as the
* "download URL" since it is the URL used to download the APK.
*
* @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_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";
/**
* @param apk must be included so that all the phases of the install process
* can get all the data about the app, even after F-Droid was killed
*/
Installer(Context context, Apk apk) {
this.context = context;
this.apk = apk;
}
/**
* Returns permission screen for given apk.
*
* @return Intent with Activity to show required permissions.
* Returns null if Installer handles that on itself, e.g., with DefaultInstaller,
* or if no new permissions have been introduced during an update
*/
public Intent getPermissionScreen() {
if (!isUnattended()) {
return null;
}
int count = newPermissionCount();
if (count == 0) {
// no permission screen needed!
return null;
}
Uri uri = ApkProvider.getApkFromAnyRepoUri(apk);
Intent intent = new Intent(context, InstallConfirmActivity.class);
intent.setData(uri);
return intent;
}
private int newPermissionCount() {
boolean supportsRuntimePermissions = apk.targetSdkVersion >= 23;
if (supportsRuntimePermissions) {
return 0;
}
AppDiff appDiff = new AppDiff(context.getPackageManager(), apk);
if (appDiff.pkgInfo == null) {
// could not get diff because we couldn't parse the package
throw new RuntimeException("cannot parse!");
}
AppSecurityPermissions perms = new AppSecurityPermissions(context, appDiff.pkgInfo);
if (appDiff.installedAppInfo != null) {
// update to an existing app
return perms.getPermissionCount(AppSecurityPermissions.WHICH_NEW);
}
// new app install
return perms.getPermissionCount(AppSecurityPermissions.WHICH_ALL);
}
/**
* Returns an Intent to start a dialog wrapped in an activity
* for uninstall confirmation.
*
* @return Intent with activity for uninstall confirmation
* Returns null if Installer handles that on itself, e.g.,
* with DefaultInstaller.
*/
public Intent getUninstallScreen() {
if (!isUnattended()) {
return null;
}
Intent intent = new Intent(context, UninstallDialogActivity.class);
intent.putExtra(Installer.EXTRA_APK, apk);
return intent;
}
void sendBroadcastInstall(Uri downloadUri, String action, PendingIntent pendingIntent) {
sendBroadcastInstall(context, downloadUri, action, apk, pendingIntent, null);
}
void sendBroadcastInstall(Uri downloadUri, String action) {
sendBroadcastInstall(context, downloadUri, action, apk, null, null);
}
void sendBroadcastInstall(Uri downloadUri, String action, String errorMessage) {
sendBroadcastInstall(context, downloadUri, action, apk, null, errorMessage);
}
static void sendBroadcastInstall(Context context,
Uri downloadUri, String action, Apk apk,
PendingIntent pendingIntent, String errorMessage) {
Intent intent = new Intent(action);
intent.setData(downloadUri);
intent.putExtra(Installer.EXTRA_USER_INTERACTION_PI, pendingIntent);
intent.putExtra(Installer.EXTRA_APK, apk);
if (!TextUtils.isEmpty(errorMessage)) {
intent.putExtra(Installer.EXTRA_ERROR_MESSAGE, errorMessage);
}
LocalBroadcastManager.getInstance(context).sendBroadcast(intent);
}
void sendBroadcastUninstall(String action, String errorMessage) {
sendBroadcastUninstall(action, null, errorMessage);
}
void sendBroadcastUninstall(String action) {
sendBroadcastUninstall(action, null, null);
}
void sendBroadcastUninstall(String action, PendingIntent pendingIntent) {
sendBroadcastUninstall(action, pendingIntent, null);
}
void sendBroadcastUninstall(String action, PendingIntent pendingIntent, String errorMessage) {
Uri uri = Uri.fromParts("package", apk.packageName, null);
Intent intent = new Intent(action);
intent.setData(uri); // for broadcast filtering
intent.putExtra(Installer.EXTRA_APK, apk);
intent.putExtra(Installer.EXTRA_USER_INTERACTION_PI, pendingIntent);
if (!TextUtils.isEmpty(errorMessage)) {
intent.putExtra(Installer.EXTRA_ERROR_MESSAGE, errorMessage);
}
LocalBroadcastManager.getInstance(context).sendBroadcast(intent);
}
/**
* Gets an {@link IntentFilter} for matching events from the install
* process based on the original download URL as a {@link Uri}.
*/
public static IntentFilter getInstallIntentFilter(Uri uri) {
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(Installer.ACTION_INSTALL_STARTED);
intentFilter.addAction(Installer.ACTION_INSTALL_COMPLETE);
intentFilter.addAction(Installer.ACTION_INSTALL_INTERRUPTED);
intentFilter.addAction(Installer.ACTION_INSTALL_USER_INTERACTION);
intentFilter.addDataScheme(uri.getScheme());
intentFilter.addDataAuthority(uri.getHost(), String.valueOf(uri.getPort()));
intentFilter.addDataPath(uri.getPath(), PatternMatcher.PATTERN_LITERAL);
return intentFilter;
}
public static IntentFilter getUninstallIntentFilter(String packageName) {
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(Installer.ACTION_UNINSTALL_STARTED);
intentFilter.addAction(Installer.ACTION_UNINSTALL_COMPLETE);
intentFilter.addAction(Installer.ACTION_UNINSTALL_INTERRUPTED);
intentFilter.addAction(Installer.ACTION_UNINSTALL_USER_INTERACTION);
intentFilter.addDataScheme("package");
intentFilter.addDataPath(packageName, PatternMatcher.PATTERN_LITERAL);
return intentFilter;
}
/**
* 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
*/
public void installPackage(Uri localApkUri, Uri downloadUri) {
try {
// verify that permissions of the apk file match the ones from the apk object
ApkVerifier apkVerifier = new ApkVerifier(context, localApkUri, apk);
apkVerifier.verifyApk();
} catch (ApkVerifier.ApkVerificationException e) {
Log.e(TAG, e.getMessage(), e);
sendBroadcastInstall(downloadUri, Installer.ACTION_INSTALL_INTERRUPTED,
e.getMessage());
return;
} catch (ApkVerifier.ApkPermissionUnequalException e) {
// if permissions of apk are not the ones listed in the repo
// and an unattended installer is used, a wrong permission screen
// has been shown, thus fallback to AOSP DefaultInstaller!
if (isUnattended()) {
Log.e(TAG, e.getMessage(), e);
Log.e(TAG, "Falling back to AOSP DefaultInstaller!");
DefaultInstaller defaultInstaller = new DefaultInstaller(context, apk);
defaultInstaller.installPackageInternal(localApkUri, downloadUri);
return;
}
}
Uri sanitizedUri;
try {
// move apk file to private directory for installation and check hash
sanitizedUri = ApkFileProvider.getSafeUri(
context, localApkUri, apk, supportsContentUri());
} catch (IOException e) {
Log.e(TAG, e.getMessage(), e);
sendBroadcastInstall(downloadUri, Installer.ACTION_INSTALL_INTERRUPTED,
e.getMessage());
return;
}
installPackageInternal(sanitizedUri, downloadUri);
}
protected abstract void installPackageInternal(Uri localApkUri, Uri downloadUri);
/**
* Uninstall app as defined by {@link Installer#apk} in
* {@link Installer#Installer(Context, Apk)}
*/
protected abstract void uninstallPackage();
/**
* This {@link Installer} instance is capable of "unattended" install and
* uninstall activities, without the system enforcing a user prompt.
*/
protected abstract boolean isUnattended();
/**
* @return true if the Installer supports content Uris and not just file Uris
*/
protected abstract boolean supportsContentUri();
}