InstallerService

This commit is contained in:
Dominik Schürmann 2016-05-19 00:32:55 +03:00
parent 4ef0642134
commit 6d2f2d20a8
7 changed files with 947 additions and 117 deletions

View File

@ -401,6 +401,9 @@
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
<activity
android:name=".installer.AndroidInstallerActivity"
android:theme="@style/AppThemeTransparent" />
<receiver android:name=".receiver.StartupReceiver" >
<intent-filter>
@ -440,6 +443,9 @@
<service
android:name=".net.DownloaderService"
android:exported="false" />
<service
android:name=".installer.InstallerService"
android:exported="false" />
<service
android:name=".CleanCacheService"
android:exported="false" />

View File

@ -22,6 +22,7 @@
package org.fdroid.fdroid;
import android.app.Activity;
import android.app.PendingIntent;
import android.bluetooth.BluetoothAdapter;
import android.content.ActivityNotFoundException;
import android.content.BroadcastReceiver;
@ -85,10 +86,9 @@ import org.fdroid.fdroid.data.App;
import org.fdroid.fdroid.data.AppProvider;
import org.fdroid.fdroid.data.InstalledAppProvider;
import org.fdroid.fdroid.data.RepoProvider;
import org.fdroid.fdroid.installer.InstallHelper;
import org.fdroid.fdroid.installer.InstallManagerService;
import org.fdroid.fdroid.installer.Installer;
import org.fdroid.fdroid.installer.Installer.InstallFailedException;
import org.fdroid.fdroid.installer.Installer.InstallerCallback;
import org.fdroid.fdroid.installer.InstallerService;
import org.fdroid.fdroid.net.Downloader;
import org.fdroid.fdroid.net.DownloaderService;
@ -319,7 +319,6 @@ public class AppDetails extends AppCompatActivity {
private int startingIgnoreThis;
private final Context context = this;
private Installer installer;
private AppDetailsHeaderFragment headerFragment;
@ -375,8 +374,6 @@ public class AppDetails extends AppCompatActivity {
packageManager = getPackageManager();
installer = Installer.getActivityInstaller(this, packageManager, myInstallerCallback);
// Get the preferences we're going to use in this Activity...
ConfigurationChangeHelper previousData = (ConfigurationChangeHelper) getLastCustomNonConfigurationInstance();
if (previousData != null) {
@ -530,13 +527,12 @@ public class AppDetails extends AppCompatActivity {
private final BroadcastReceiver completeReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
File localFile = new File(intent.getStringExtra(Downloader.EXTRA_DOWNLOAD_PATH));
try {
installer.installPackage(localFile, app.packageName, intent.getDataString());
} catch (InstallFailedException e) {
Log.e(TAG, "Android not compatible with this Installer!", e);
}
cleanUpFinishedDownload();
Uri localUri =
Uri.fromFile(new File(intent.getStringExtra(Downloader.EXTRA_DOWNLOAD_PATH)));
localBroadcastManager.registerReceiver(installReceiver,
InstallerService.getInstallIntentFilter(localUri));
}
};
@ -555,6 +551,165 @@ public class AppDetails extends AppCompatActivity {
}
};
private final BroadcastReceiver installReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
switch (intent.getAction()) {
case InstallHelper.ACTION_INSTALL_STARTED: {
headerFragment.startProgress();
headerFragment.showIndeterminateProgress(getString(R.string.installing));
break;
}
case InstallHelper.ACTION_INSTALL_COMPLETE: {
headerFragment.removeProgress();
localBroadcastManager.unregisterReceiver(this);
PackageManagerCompat.setInstaller(packageManager, app.packageName);
onAppChanged();
break;
}
case InstallHelper.ACTION_INSTALL_INTERRUPTED: {
headerFragment.removeProgress();
localBroadcastManager.unregisterReceiver(this);
// TODO: old error handling code:
// if (errorCode == InstallerCallback.ERROR_CODE_CANCELED) {
// return;
// }
// final int title, body;
// if (operation == InstallerCallback.OPERATION_INSTALL) {
// title = R.string.install_error_title;
// switch (errorCode) {
// case ERROR_CODE_CANNOT_PARSE:
// body = R.string.install_error_cannot_parse;
// break;
// default: // ERROR_CODE_OTHER
// body = R.string.install_error_unknown;
// break;
// }
// } else { // InstallerCallback.OPERATION_DELETE
// title = R.string.uninstall_error_title;
// switch (errorCode) {
// default: // ERROR_CODE_OTHER
// body = R.string.uninstall_error_unknown;
// break;
// }
// }
// runOnUiThread(new Runnable() {
// @Override
// public void run() {
// onAppChanged();
//
// Log.e(TAG, "Installer aborted with errorCode: " + errorCode);
//
// AlertDialog.Builder alertBuilder = new AlertDialog.Builder(AppDetails.this);
// alertBuilder.setTitle(title);
// alertBuilder.setMessage(body);
// alertBuilder.setNeutralButton(android.R.string.ok, null);
// alertBuilder.create().show();
// }
// });
break;
}
case InstallHelper.ACTION_INSTALL_USER_INTERACTION: {
PendingIntent installPendingIntent =
intent.getParcelableExtra(InstallHelper.EXTRA_USER_INTERACTION_PI);
try {
installPendingIntent.send();
} catch (PendingIntent.CanceledException e) {
Log.e(TAG, "PI canceled", e);
}
break;
}
default: {
throw new RuntimeException("intent action not handled!");
}
}
}
};
private final BroadcastReceiver uninstallReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
switch (intent.getAction()) {
case InstallHelper.ACTION_UNINSTALL_STARTED: {
headerFragment.startProgress();
headerFragment.showIndeterminateProgress(getString(R.string.uninstalling));
break;
}
case InstallHelper.ACTION_UNINSTALL_COMPLETE: {
headerFragment.removeProgress();
localBroadcastManager.unregisterReceiver(this);
onAppChanged();
break;
}
case InstallHelper.ACTION_UNINSTALL_INTERRUPTED: {
headerFragment.removeProgress();
localBroadcastManager.unregisterReceiver(this);
// TODO: old error handling code:
// if (errorCode == InstallerCallback.ERROR_CODE_CANCELED) {
// return;
// }
// final int title, body;
// if (operation == InstallerCallback.OPERATION_INSTALL) {
// title = R.string.install_error_title;
// switch (errorCode) {
// case ERROR_CODE_CANNOT_PARSE:
// body = R.string.install_error_cannot_parse;
// break;
// default: // ERROR_CODE_OTHER
// body = R.string.install_error_unknown;
// break;
// }
// } else { // InstallerCallback.OPERATION_DELETE
// title = R.string.uninstall_error_title;
// switch (errorCode) {
// default: // ERROR_CODE_OTHER
// body = R.string.uninstall_error_unknown;
// break;
// }
// }
// runOnUiThread(new Runnable() {
// @Override
// public void run() {
// onAppChanged();
//
// Log.e(TAG, "Installer aborted with errorCode: " + errorCode);
//
// AlertDialog.Builder alertBuilder = new AlertDialog.Builder(AppDetails.this);
// alertBuilder.setTitle(title);
// alertBuilder.setMessage(body);
// alertBuilder.setNeutralButton(android.R.string.ok, null);
// alertBuilder.create().show();
// }
// });
break;
}
case InstallHelper.ACTION_UNINSTALL_USER_INTERACTION: {
PendingIntent uninstallPendingIntent =
intent.getParcelableExtra(InstallHelper.EXTRA_USER_INTERACTION_PI);
try {
uninstallPendingIntent.send();
} catch (PendingIntent.CanceledException e) {
Log.e(TAG, "PI canceled", e);
}
break;
}
default: {
throw new RuntimeException("intent action not handled!");
}
}
}
};
private void onAppChanged() {
if (!reset(app.packageName)) {
this.finish();
@ -796,7 +951,7 @@ public class AppDetails extends AppCompatActivity {
return true;
case UNINSTALL:
removeApk(app.packageName);
uninstallApk(app.packageName);
return true;
case IGNOREALL:
@ -881,71 +1036,12 @@ public class AppDetails extends AppCompatActivity {
InstallManagerService.queue(this, app, apk);
}
private void removeApk(String packageName) {
try {
installer.deletePackage(packageName);
} catch (InstallFailedException e) {
Log.e(TAG, "Android not compatible with this Installer!", e);
}
private void uninstallApk(String packageName) {
localBroadcastManager.registerReceiver(uninstallReceiver,
InstallerService.getUninstallIntentFilter(packageName));
InstallerService.uninstall(context, packageName);
}
private final Installer.InstallerCallback myInstallerCallback = new Installer.InstallerCallback() {
@Override
public void onSuccess(final int operation) {
runOnUiThread(new Runnable() {
@Override
public void run() {
if (operation == Installer.InstallerCallback.OPERATION_INSTALL) {
PackageManagerCompat.setInstaller(packageManager, app.packageName);
}
onAppChanged();
}
});
}
@Override
public void onError(int operation, final int errorCode) {
if (errorCode == InstallerCallback.ERROR_CODE_CANCELED) {
return;
}
final int title, body;
if (operation == InstallerCallback.OPERATION_INSTALL) {
title = R.string.install_error_title;
switch (errorCode) {
case ERROR_CODE_CANNOT_PARSE:
body = R.string.install_error_cannot_parse;
break;
default: // ERROR_CODE_OTHER
body = R.string.install_error_unknown;
break;
}
} else { // InstallerCallback.OPERATION_DELETE
title = R.string.uninstall_error_title;
switch (errorCode) {
default: // ERROR_CODE_OTHER
body = R.string.uninstall_error_unknown;
break;
}
}
runOnUiThread(new Runnable() {
@Override
public void run() {
onAppChanged();
Log.e(TAG, "Installer aborted with errorCode: " + errorCode);
AlertDialog.Builder alertBuilder = new AlertDialog.Builder(AppDetails.this);
alertBuilder.setTitle(title);
alertBuilder.setMessage(body);
alertBuilder.setNeutralButton(android.R.string.ok, null);
alertBuilder.create().show();
}
});
}
};
private void launchApk(String packageName) {
Intent intent = packageManager.getLaunchIntentForPackage(packageName);
startActivity(intent);
@ -963,11 +1059,6 @@ public class AppDetails extends AppCompatActivity {
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
// handle cases for install manager first
if (installer.handleOnActivityResult(requestCode, resultCode, data)) {
return;
}
switch (requestCode) {
case REQUEST_ENABLE_BLUETOOTH:
fdroidApp.sendViaBluetooth(this, resultCode, app.packageName);
@ -1606,7 +1697,7 @@ public class AppDetails extends AppCompatActivity {
// If "launchable", launch
activity.launchApk(app.packageName);
} else {
activity.removeApk(app.packageName);
activity.uninstallApk(app.packageName);
}
} else if (app.suggestedVersionCode > 0) {
// If not installed, install
@ -1635,7 +1726,7 @@ public class AppDetails extends AppCompatActivity {
}
void remove() {
appDetails.removeApk(appDetails.getApp().packageName);
appDetails.uninstallApk(appDetails.getApp().packageName);
}
@Override

View File

@ -0,0 +1,291 @@
/*
* Copyright (C) 2014-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.annotation.SuppressLint;
import android.app.Activity;
import android.content.ActivityNotFoundException;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.support.v4.app.FragmentActivity;
import android.support.v4.content.LocalBroadcastManager;
import android.text.TextUtils;
import android.util.Log;
import org.fdroid.fdroid.Utils;
/**
* A transparent activity as a wrapper around AOSP's PackageInstaller Intents
*/
public class AndroidInstallerActivity extends FragmentActivity {
public static final String TAG = "AndroidInstallerAct";
public static final String ACTION_INSTALL_PACKAGE = "org.fdroid.fdroid.INSTALL_PACKAGE";
public static final String ACTION_UNINSTALL_PACKAGE = "org.fdroid.fdroid.UNINSTALL_PACKAGE";
public static final String EXTRA_UNINSTALL_PACKAGE_NAME = "uninstallPackageName";
public static final String EXTRA_ORIGINATING_URI = "originatingUri";
private static final int REQUEST_CODE_INSTALL = 0;
private static final int REQUEST_CODE_UNINSTALL = 1;
private LocalBroadcastManager localBroadcastManager;
private Uri mInstallOriginatingUri;
private Uri mInstallUri;
private String mUninstallPackageName;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
localBroadcastManager = LocalBroadcastManager.getInstance(this);
Intent intent = getIntent();
String action = intent.getAction();
switch (action) {
case ACTION_INSTALL_PACKAGE: {
mInstallUri = intent.getData();
mInstallOriginatingUri = intent.getParcelableExtra(EXTRA_ORIGINATING_URI);
installPackage(mInstallUri, mInstallOriginatingUri);
break;
}
case ACTION_UNINSTALL_PACKAGE: {
mUninstallPackageName = intent.getStringExtra(EXTRA_UNINSTALL_PACKAGE_NAME);
uninstallPackage(mUninstallPackageName);
break;
}
default: {
throw new IllegalStateException("Intent action not specified!");
}
}
}
@SuppressLint("InlinedApi")
private void installPackage(Uri uri, Uri originatingUri) {
Utils.debugLog(TAG, "Installing from " + uri);
if (uri == null) {
throw new RuntimeException("Set the data uri to point to an apk location!");
}
// https://code.google.com/p/android/issues/detail?id=205827
if ((Build.VERSION.SDK_INT <= Build.VERSION_CODES.M)
&& (!uri.getScheme().equals("file"))) {
throw new RuntimeException("PackageInstaller <= Android 6 only supports file scheme!");
}
if (("N".equals(Build.VERSION.CODENAME))
&& (!uri.getScheme().equals("content"))) {
throw new RuntimeException("PackageInstaller >= Android N only supports content scheme!");
}
Intent intent = new Intent();
intent.setData(uri);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
intent.setAction(Intent.ACTION_VIEW);
intent.setType("application/vnd.android.package-archive");
} else {
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_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);
}
}
try {
startActivityForResult(intent, REQUEST_CODE_INSTALL);
} catch (ActivityNotFoundException e) {
Log.e(TAG, "ActivityNotFoundException", e);
sendBroadcastInstall(uri, originatingUri, InstallHelper.ACTION_INSTALL_INTERRUPTED,
"This Android rom does not support ACTION_INSTALL_PACKAGE!");
finish();
}
sendBroadcastInstall(mInstallUri, mInstallOriginatingUri,
InstallHelper.ACTION_INSTALL_STARTED);
}
protected void uninstallPackage(String packageName) {
Intent intent = new Intent();
// check that the package is installed
try {
getPackageManager().getPackageInfo(packageName, 0);
} catch (PackageManager.NameNotFoundException e) {
Log.e(TAG, "NameNotFoundException", e);
sendBroadcastUninstall(packageName, InstallHelper.ACTION_UNINSTALL_INTERRUPTED,
"Package that is scheduled for uninstall is not installed!");
finish();
return;
}
Uri uri = Uri.fromParts("package", packageName, null);
intent.setData(uri);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
intent.setAction(Intent.ACTION_DELETE);
} else {
intent.setAction(Intent.ACTION_UNINSTALL_PACKAGE);
intent.putExtra(Intent.EXTRA_RETURN_RESULT, true);
}
try {
startActivityForResult(intent, REQUEST_CODE_UNINSTALL);
} catch (ActivityNotFoundException e) {
Log.e(TAG, "ActivityNotFoundException", e);
sendBroadcastUninstall(packageName, InstallHelper.ACTION_UNINSTALL_INTERRUPTED,
"This Android rom does not support ACTION_UNINSTALL_PACKAGE!");
finish();
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
switch (requestCode) {
case REQUEST_CODE_INSTALL: {
/**
* resultCode is always 0 on Android < 4.0. See
* com.android.packageinstaller.PackageInstallerActivity: setResult is
* never executed on Androids < 4.0
*/
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
sendBroadcastInstall(mInstallUri, mInstallOriginatingUri,
InstallHelper.ACTION_INSTALL_COMPLETE);
break;
}
// Fallback on N for https://gitlab.com/fdroid/fdroidclient/issues/631
if ("N".equals(Build.VERSION.CODENAME)) {
sendBroadcastInstall(mInstallUri, mInstallOriginatingUri,
InstallHelper.ACTION_INSTALL_COMPLETE);
break;
}
switch (resultCode) {
case Activity.RESULT_OK: {
sendBroadcastInstall(mInstallUri, mInstallOriginatingUri,
InstallHelper.ACTION_INSTALL_COMPLETE);
break;
}
case Activity.RESULT_CANCELED: {
sendBroadcastInstall(mInstallUri, mInstallOriginatingUri,
InstallHelper.ACTION_INSTALL_INTERRUPTED);
break;
}
default:
case Activity.RESULT_FIRST_USER: {
// AOSP actually returns Activity.RESULT_FIRST_USER if something breaks
sendBroadcastInstall(mInstallUri, mInstallOriginatingUri,
InstallHelper.ACTION_INSTALL_INTERRUPTED, "error");
break;
}
}
break;
}
case REQUEST_CODE_UNINSTALL: {
// resultCode is always 0 on Android < 4.0.
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
sendBroadcastUninstall(mUninstallPackageName,
InstallHelper.ACTION_UNINSTALL_COMPLETE);
break;
}
switch (resultCode) {
case Activity.RESULT_OK: {
sendBroadcastUninstall(mUninstallPackageName,
InstallHelper.ACTION_UNINSTALL_COMPLETE);
break;
}
case Activity.RESULT_CANCELED: {
sendBroadcastUninstall(mUninstallPackageName,
InstallHelper.ACTION_UNINSTALL_INTERRUPTED);
break;
}
default:
case Activity.RESULT_FIRST_USER: {
// AOSP UninstallAppProgress actually returns
// Activity.RESULT_FIRST_USER if something breaks
sendBroadcastUninstall(mUninstallPackageName,
InstallHelper.ACTION_UNINSTALL_INTERRUPTED,
"error");
break;
}
}
break;
}
default: {
throw new RuntimeException("Invalid request code!");
}
}
// after doing the broadcasts, finish this transparent wrapper activity
finish();
}
private void sendBroadcastInstall(Uri uri, Uri originatingUri, String action) {
sendBroadcastInstall(uri, originatingUri, action, null);
}
private void sendBroadcastInstall(Uri uri, Uri originatingUri, String action, String errorMessage) {
Intent intent = new Intent(action);
intent.setData(uri);
intent.putExtra(InstallHelper.EXTRA_ORIGINATING_URI, originatingUri);
if (!TextUtils.isEmpty(errorMessage)) {
intent.putExtra(InstallHelper.EXTRA_ERROR_MESSAGE, errorMessage);
}
localBroadcastManager.sendBroadcast(intent);
}
private void sendBroadcastUninstall(String packageName, String action) {
sendBroadcastUninstall(packageName, action, null);
}
private void sendBroadcastUninstall(String packageName, String action, String errorMessage) {
Uri uri = Uri.fromParts("package", packageName, null);
Intent intent = new Intent(action);
intent.setData(uri); // for broadcast filter
intent.putExtra(InstallHelper.EXTRA_UNINSTALL_PACKAGE_NAME, packageName);
if (!TextUtils.isEmpty(errorMessage)) {
intent.putExtra(InstallHelper.EXTRA_ERROR_MESSAGE, errorMessage);
}
localBroadcastManager.sendBroadcast(intent);
}
}

View File

@ -0,0 +1,128 @@
package org.fdroid.fdroid.installer;
import android.app.Activity;
import android.app.NotificationManager;
import android.content.Context;
import android.content.Intent;
import org.apache.commons.io.FileUtils;
import org.fdroid.fdroid.AndroidXMLDecompress;
import org.fdroid.fdroid.BuildConfig;
import org.fdroid.fdroid.Hasher;
import org.fdroid.fdroid.compat.FileCompat;
import org.fdroid.fdroid.data.SanitizedFile;
import org.fdroid.fdroid.privileged.install.InstallExtensionDialogActivity;
import java.io.File;
import java.io.IOException;
import java.security.NoSuchAlgorithmException;
import java.util.Map;
public class InstallHelper {
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";
/**
* Same as http://developer.android.com/reference/android/content/Intent.html#EXTRA_ORIGINATING_URI
* In InstallManagerService often called urlString
*/
public static final String EXTRA_ORIGINATING_URI = "org.fdroid.fdroid.installer.InstallerService.extra.ORIGINATING_URI";
public static final String EXTRA_UNINSTALL_PACKAGE_NAME = "org.fdroid.fdroid.installer.InstallerService.extra.UNINSTALL_PACKAGE_NAME";
public static final String EXTRA_USER_INTERACTION_PI = "org.fdroid.fdroid.installer.InstallerService.extra.USER_INTERACTION_PI";
public static final String EXTRA_ERROR_MESSAGE = "org.fdroid.fdroid.net.Downloader.extra.ERROR_MESSAGE";
public static SanitizedFile preparePackage(Context context, File apkFile, String packageName, String urlString)
throws Installer.InstallFailedException {
SanitizedFile apkToInstall;
try {
Map<String, Object> attributes = AndroidXMLDecompress.getManifestHeaderAttributes(apkFile.getAbsolutePath());
/* This isn't really needed, but might as well since we have the data already */
// if (attributes.containsKey("packageName") && !TextUtils.equals(packageName, (String) attributes.get("packageName"))) {
// throw new Installer.InstallFailedException(apkFile + " has packageName that clashes with " + packageName);
// }
if (!attributes.containsKey("versionCode")) {
throw new Installer.InstallFailedException(apkFile + " is missing versionCode!");
}
int versionCode = (Integer) attributes.get("versionCode");
// Apk apk = ApkProvider.Helper.find(context, packageName, versionCode, new String[]{
// ApkProvider.DataColumns.HASH,
// ApkProvider.DataColumns.HASH_TYPE,
// });
/* 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 */
apkToInstall = SanitizedFile.knownSanitized(File.createTempFile("install-", ".apk", context.getFilesDir()));
FileUtils.copyFile(apkFile, apkToInstall);
// if (!verifyApkFile(apkToInstall, apk.hash, apk.hashType)) {
// FileUtils.deleteQuietly(apkFile);
// throw new Installer.InstallFailedException(apkFile + " failed to verify!");
// }
apkFile = null; // ensure this is not used now that its copied to apkToInstall
// special case: F-Droid Privileged Extension
if (packageName != null && packageName.equals(PrivilegedInstaller.PRIVILEGED_EXTENSION_PACKAGE_NAME)) {
// extension must be signed with the same public key as main F-Droid
// NOTE: Disabled for debug builds to be able to use official extension from repo
ApkSignatureVerifier signatureVerifier = new ApkSignatureVerifier(context);
if (!BuildConfig.DEBUG && !signatureVerifier.hasFDroidSignature(apkToInstall)) {
throw new Installer.InstallFailedException("APK signature of extension not correct!");
}
Activity activity = (Activity) context;
Intent installIntent = new Intent(activity, InstallExtensionDialogActivity.class);
installIntent.setAction(InstallExtensionDialogActivity.ACTION_INSTALL);
installIntent.putExtra(InstallExtensionDialogActivity.EXTRA_INSTALL_APK, apkToInstall.getAbsolutePath());
activity.startActivity(installIntent);
return null;
}
// 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.
FileCompat.setReadable(apkToInstall, true);
NotificationManager nm = (NotificationManager)
context.getSystemService(Context.NOTIFICATION_SERVICE);
nm.cancel(urlString.hashCode());
} catch (NumberFormatException | IOException e) {
throw new Installer.InstallFailedException(e);
} catch (ClassCastException e) {
throw new Installer.InstallFailedException("F-Droid Privileged can only be updated using an activity!");
}
return apkToInstall;
}
/**
* Checks the APK file against the provided hash, returning whether it is a match.
*/
public 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);
}
}

View File

@ -85,18 +85,6 @@ public class InstallManagerService extends Service {
*/
private final HashMap<String, BroadcastReceiver[]> receivers = new HashMap<>(3);
/**
* Get the app name based on a {@code urlString} key. The app name needs
* to be kept around for the final notification update, but {@link App}
* and {@link Apk} instances have already removed by the time that final
* notification update comes around. Once there is a proper
* {@code InstallerService} and its integrated here, this must go away,
* since the {@link App} and {@link Apk} instances will be available.
* <p>
* TODO <b>delete me once InstallerService exists</b>
*/
private static final HashMap<String, String> TEMP_HACK_APP_NAMES = new HashMap<>(3);
private LocalBroadcastManager localBroadcastManager;
private NotificationManager notificationManager;
@ -234,15 +222,18 @@ public class InstallManagerService extends Service {
BroadcastReceiver completeReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String urlString = intent.getDataString();
// TODO these need to be removed based on whether they are fed to InstallerService or not
Apk apk = removeFromActive(urlString);
if (AppDetails.isAppVisible(apk.packageName)) {
cancelNotification(urlString);
} else {
notifyDownloadComplete(urlString, apk);
}
unregisterDownloaderReceivers(urlString);
// elsewhere called urlString
Uri originatingUri = intent.getData();
File localFile = new File(intent.getStringExtra(Downloader.EXTRA_DOWNLOAD_PATH));
Uri localUri = Uri.fromFile(localFile);
Utils.debugLog(TAG, "download completed of " + originatingUri
+ " to " + localUri);
unregisterDownloaderReceivers(intent.getDataString());
registerInstallerReceivers(localUri);
InstallerService.install(context, localUri, originatingUri);
}
};
BroadcastReceiver interruptedReceiver = new BroadcastReceiver() {
@ -265,6 +256,69 @@ public class InstallManagerService extends Service {
receivers.put(urlString, new BroadcastReceiver[]{
startedReceiver, progressReceiver, completeReceiver, interruptedReceiver,
});
}
private void registerInstallerReceivers(Uri uri) {
BroadcastReceiver installReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
switch (intent.getAction()) {
case InstallHelper.ACTION_INSTALL_STARTED: {
Utils.debugLog(TAG, "ACTION_INSTALL_STARTED");
break;
}
case InstallHelper.ACTION_INSTALL_COMPLETE: {
Utils.debugLog(TAG, "ACTION_INSTALL_COMPLETE");
Uri originatingUri =
intent.getParcelableExtra(InstallHelper.EXTRA_ORIGINATING_URI);
String urlString = originatingUri.toString();
removeFromActive(urlString);
localBroadcastManager.unregisterReceiver(this);
break;
}
case InstallHelper.ACTION_INSTALL_INTERRUPTED: {
Utils.debugLog(TAG, "ACTION_INSTALL_INTERRUPTED");
localBroadcastManager.unregisterReceiver(this);
break;
}
case InstallHelper.ACTION_INSTALL_USER_INTERACTION: {
Utils.debugLog(TAG, "ACTION_INSTALL_USER_INTERACTION");
Uri originatingUri =
intent.getParcelableExtra(InstallHelper.EXTRA_ORIGINATING_URI);
PendingIntent installPendingIntent =
intent.getParcelableExtra(InstallHelper.EXTRA_USER_INTERACTION_PI);
// TODO
String urlString = originatingUri.toString();
Apk apk = getFromActive(urlString);
Utils.debugLog(TAG, "urlString: " + urlString);
if (AppDetails.isAppVisible(apk.packageName)) {
cancelNotification(urlString);
} else {
notifyDownloadComplete(apk, urlString, installPendingIntent);
}
break;
}
default: {
throw new RuntimeException("intent action not handled!");
}
}
}
};
localBroadcastManager.registerReceiver(installReceiver,
InstallerService.getInstallIntentFilter(uri));
}
private NotificationCompat.Builder createNotificationBuilder(String urlString, Apk apk) {
@ -283,16 +337,7 @@ public class InstallManagerService extends Service {
private String getAppName(String urlString, Apk apk) {
App app = ACTIVE_APPS.get(apk.packageName);
if (app == null || TextUtils.isEmpty(app.name)) {
if (TEMP_HACK_APP_NAMES.containsKey(urlString)) {
return TEMP_HACK_APP_NAMES.get(urlString);
} else {
// this is ugly, but its better than nothing as a failsafe
return urlString;
}
} else {
return app.name;
}
return app.name;
}
/**
@ -319,7 +364,7 @@ public class InstallManagerService extends Service {
* Removing the progress bar from a notification should cause the notification's content
* text to return to normal size</a>
*/
private void notifyDownloadComplete(String urlString, Apk apk) {
private void notifyDownloadComplete(Apk apk, String urlString, PendingIntent installPendingIntent) {
String title;
try {
PackageManager pm = getPackageManager();
@ -335,7 +380,7 @@ public class InstallManagerService extends Service {
.setAutoCancel(true)
.setOngoing(false)
.setContentTitle(title)
.setContentIntent(getAppDetailsIntent(downloadUrlId, apk))
.setContentIntent(installPendingIntent)
.setSmallIcon(android.R.drawable.stat_sys_download_done)
.setContentText(getString(R.string.tap_to_install))
.build();
@ -354,7 +399,10 @@ public class InstallManagerService extends Service {
private static void addToActive(String urlString, App app, Apk apk) {
ACTIVE_APKS.put(urlString, apk);
ACTIVE_APPS.put(app.packageName, app);
TEMP_HACK_APP_NAMES.put(urlString, app.name); // TODO delete me once InstallerService exists
}
private static Apk getFromActive(String urlString) {
return ACTIVE_APKS.get(urlString);
}
/**

View File

@ -0,0 +1,264 @@
/*
* Copyright (C) 2008 The Android Open Source Project
* Copyright (C) 2016 Hans-Christoph Steiner
* Copyright (C) 2016 Dominik Schürmann <dominik@dominikschuermann.de>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.fdroid.fdroid.installer;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.Uri;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
import android.os.PatternMatcher;
import android.os.Process;
import android.support.v4.content.LocalBroadcastManager;
import android.text.TextUtils;
import android.util.Log;
import org.fdroid.fdroid.Utils;
import java.io.File;
/**
* InstallerService based on DownloaderService
*/
public class InstallerService extends Service {
private static final String TAG = "InstallerService";
private static final String ACTION_INSTALL = "org.fdroid.fdroid.installer.InstallerService.action.INSTALL";
private static final String ACTION_UNINSTALL = "org.fdroid.fdroid.installer.InstallerService.action.UNINSTALL";
private volatile Looper serviceLooper;
private static volatile ServiceHandler serviceHandler;
private LocalBroadcastManager localBroadcastManager;
private final class ServiceHandler extends Handler {
ServiceHandler(Looper looper) {
super(looper);
}
@Override
public void handleMessage(Message msg) {
Utils.debugLog(TAG, "Handling message with ID of " + msg.what);
handleIntent((Intent) msg.obj);
stopSelf(msg.arg1);
}
}
@Override
public void onCreate() {
super.onCreate();
Utils.debugLog(TAG, "Creating installer service.");
HandlerThread thread = new HandlerThread(TAG, Process.THREAD_PRIORITY_BACKGROUND);
thread.start();
serviceLooper = thread.getLooper();
serviceHandler = new ServiceHandler(serviceLooper);
localBroadcastManager = LocalBroadcastManager.getInstance(this);
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Utils.debugLog(TAG, "Received Intent for installing/uninstalling: " + intent + " (with a startId of " + startId + ")");
if (ACTION_INSTALL.equals(intent.getAction())) {
Uri uri = intent.getData();
Message msg = serviceHandler.obtainMessage();
msg.arg1 = startId;
msg.obj = intent;
msg.what = uri.hashCode();
serviceHandler.sendMessage(msg);
Utils.debugLog(TAG, "Start install of " + uri.toString());
} else if (ACTION_UNINSTALL.equals(intent.getAction())) {
String packageName = intent.getStringExtra(InstallHelper.EXTRA_UNINSTALL_PACKAGE_NAME);
Message msg = serviceHandler.obtainMessage();
msg.arg1 = startId;
msg.obj = intent;
msg.what = packageName.hashCode();
serviceHandler.sendMessage(msg);
Utils.debugLog(TAG, "Start uninstall of " + packageName);
} else {
Log.e(TAG, "Received Intent with unknown action: " + intent);
}
return START_REDELIVER_INTENT; // if killed before completion, retry Intent
}
@Override
public void onDestroy() {
Utils.debugLog(TAG, "Destroying installer service. Will move to background and stop our Looper.");
serviceLooper.quit(); //NOPMD - this is copied from IntentService, no super call needed
}
/**
* This service does not use binding, so no need to implement this method
*/
@Override
public IBinder onBind(Intent intent) {
return null;
}
protected void handleIntent(Intent intent) {
switch (intent.getAction()) {
case ACTION_INSTALL: {
Uri uri = intent.getData();
Uri originatingUri = intent.getParcelableExtra(InstallHelper.EXTRA_ORIGINATING_URI);
sendBroadcastInstall(uri, originatingUri, InstallHelper.ACTION_INSTALL_STARTED);
Utils.debugLog(TAG, "ACTION_INSTALL uri: " + uri + " file: " + new File(uri.getPath()));
// TODO: rework for uri
Uri sanitizedUri = null;
try {
File file = InstallHelper.preparePackage(this, new File(uri.getPath()), null,
originatingUri.toString());
sanitizedUri = Uri.fromFile(file);
} catch (Installer.InstallFailedException e) {
e.printStackTrace();
}
Utils.debugLog(TAG, "ACTION_INSTALL sanitizedUri: " + sanitizedUri);
Intent installIntent = new Intent(this, AndroidInstallerActivity.class);
installIntent.setAction(AndroidInstallerActivity.ACTION_INSTALL_PACKAGE);
installIntent.putExtra(AndroidInstallerActivity.EXTRA_ORIGINATING_URI, originatingUri);
installIntent.setData(sanitizedUri);
PendingIntent installPendingIntent = PendingIntent.getActivity(this.getApplicationContext(),
uri.hashCode(),
installIntent,
PendingIntent.FLAG_UPDATE_CURRENT);
sendBroadcastInstall(uri, originatingUri, InstallHelper.ACTION_INSTALL_USER_INTERACTION,
installPendingIntent);
break;
}
case ACTION_UNINSTALL: {
String packageName =
intent.getStringExtra(InstallHelper.EXTRA_UNINSTALL_PACKAGE_NAME);
sendBroadcastUninstall(packageName, InstallHelper.ACTION_UNINSTALL_STARTED);
Intent installIntent = new Intent(this, AndroidInstallerActivity.class);
installIntent.setAction(AndroidInstallerActivity.ACTION_UNINSTALL_PACKAGE);
installIntent.putExtra(
AndroidInstallerActivity.EXTRA_UNINSTALL_PACKAGE_NAME, packageName);
PendingIntent uninstallPendingIntent = PendingIntent.getActivity(this.getApplicationContext(),
packageName.hashCode(),
installIntent,
PendingIntent.FLAG_UPDATE_CURRENT);
sendBroadcastUninstall(packageName, InstallHelper.ACTION_UNINSTALL_USER_INTERACTION,
uninstallPendingIntent);
break;
}
}
}
private void sendBroadcastInstall(Uri uri, Uri originatingUri, String action,
PendingIntent pendingIntent) {
sendBroadcastInstall(uri, originatingUri, action, pendingIntent, null);
}
private void sendBroadcastInstall(Uri uri, Uri originatingUri, String action) {
sendBroadcastInstall(uri, originatingUri, action, null, null);
}
private void sendBroadcastInstall(Uri uri, Uri originatingUri, String action,
PendingIntent pendingIntent, String errorMessage) {
Intent intent = new Intent(action);
intent.setData(uri);
intent.putExtra(InstallHelper.EXTRA_ORIGINATING_URI, originatingUri);
intent.putExtra(InstallHelper.EXTRA_USER_INTERACTION_PI, pendingIntent);
if (!TextUtils.isEmpty(errorMessage)) {
intent.putExtra(InstallHelper.EXTRA_ERROR_MESSAGE, errorMessage);
}
localBroadcastManager.sendBroadcast(intent);
}
private void sendBroadcastUninstall(String packageName, String action) {
sendBroadcastUninstall(packageName, action, null, null);
}
private void sendBroadcastUninstall(String packageName, String action,
PendingIntent pendingIntent) {
sendBroadcastUninstall(packageName, action, pendingIntent, null);
}
private void sendBroadcastUninstall(String packageName, String action,
PendingIntent pendingIntent, String errorMessage) {
Uri uri = Uri.fromParts("package", packageName, null);
Intent intent = new Intent(action);
intent.setData(uri); // for broadcast filtering
intent.putExtra(InstallHelper.EXTRA_UNINSTALL_PACKAGE_NAME, packageName);
intent.putExtra(InstallHelper.EXTRA_USER_INTERACTION_PI, pendingIntent);
if (!TextUtils.isEmpty(errorMessage)) {
intent.putExtra(InstallHelper.EXTRA_ERROR_MESSAGE, errorMessage);
}
localBroadcastManager.sendBroadcast(intent);
}
public static void install(Context context, Uri uri, Uri originatingUri) {
Intent intent = new Intent(context, InstallerService.class);
intent.setAction(ACTION_INSTALL);
intent.setData(uri);
intent.putExtra(InstallHelper.EXTRA_ORIGINATING_URI, originatingUri);
context.startService(intent);
}
public static void uninstall(Context context, String packageName) {
Intent intent = new Intent(context, InstallerService.class);
intent.setAction(ACTION_UNINSTALL);
intent.putExtra(InstallHelper.EXTRA_UNINSTALL_PACKAGE_NAME, packageName);
context.startService(intent);
}
public static IntentFilter getInstallIntentFilter(Uri uri) {
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(InstallHelper.ACTION_INSTALL_STARTED);
intentFilter.addAction(InstallHelper.ACTION_INSTALL_COMPLETE);
intentFilter.addAction(InstallHelper.ACTION_INSTALL_INTERRUPTED);
intentFilter.addAction(InstallHelper.ACTION_INSTALL_USER_INTERACTION);
intentFilter.addDataScheme(uri.getScheme());
intentFilter.addDataPath(uri.getPath(), PatternMatcher.PATTERN_LITERAL);
return intentFilter;
}
public static IntentFilter getUninstallIntentFilter(String packageName) {
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(InstallHelper.ACTION_UNINSTALL_STARTED);
intentFilter.addAction(InstallHelper.ACTION_UNINSTALL_COMPLETE);
intentFilter.addAction(InstallHelper.ACTION_UNINSTALL_INTERRUPTED);
intentFilter.addAction(InstallHelper.ACTION_UNINSTALL_USER_INTERACTION);
intentFilter.addDataScheme("package");
intentFilter.addDataPath(packageName, PatternMatcher.PATTERN_LITERAL);
return intentFilter;
}
}

View File

@ -370,6 +370,8 @@
<string name="perms_description_app">Provided by %1$s.</string>
<string name="downloading">Downloading…</string>
<string name="downloading_apk">Downloading %1$s</string>
<string name="installing">Installing…</string>
<string name="uninstalling">Uninstalling…</string>
<string name="interval_never">Never</string>
<string name="interval_1h">Hourly</string>