From 6d2f2d20a84521aea40bca4a95344a7ab7583146 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Sch=C3=BCrmann?= Date: Thu, 19 May 2016 00:32:55 +0300 Subject: [PATCH] InstallerService --- app/src/main/AndroidManifest.xml | 6 + .../java/org/fdroid/fdroid/AppDetails.java | 257 +++++++++++----- .../installer/AndroidInstallerActivity.java | 291 ++++++++++++++++++ .../fdroid/installer/InstallHelper.java | 128 ++++++++ .../installer/InstallManagerService.java | 116 +++++-- .../fdroid/installer/InstallerService.java | 264 ++++++++++++++++ app/src/main/res/values/strings.xml | 2 + 7 files changed, 947 insertions(+), 117 deletions(-) create mode 100644 app/src/main/java/org/fdroid/fdroid/installer/AndroidInstallerActivity.java create mode 100644 app/src/main/java/org/fdroid/fdroid/installer/InstallHelper.java create mode 100644 app/src/main/java/org/fdroid/fdroid/installer/InstallerService.java diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 85f2b0310..c5734f480 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -401,6 +401,9 @@ + @@ -440,6 +443,9 @@ + diff --git a/app/src/main/java/org/fdroid/fdroid/AppDetails.java b/app/src/main/java/org/fdroid/fdroid/AppDetails.java index 70d5f5454..379937361 100644 --- a/app/src/main/java/org/fdroid/fdroid/AppDetails.java +++ b/app/src/main/java/org/fdroid/fdroid/AppDetails.java @@ -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 diff --git a/app/src/main/java/org/fdroid/fdroid/installer/AndroidInstallerActivity.java b/app/src/main/java/org/fdroid/fdroid/installer/AndroidInstallerActivity.java new file mode 100644 index 000000000..4764941cc --- /dev/null +++ b/app/src/main/java/org/fdroid/fdroid/installer/AndroidInstallerActivity.java @@ -0,0 +1,291 @@ +/* + * Copyright (C) 2014-2016 Dominik Schürmann + * + * 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); + } + +} diff --git a/app/src/main/java/org/fdroid/fdroid/installer/InstallHelper.java b/app/src/main/java/org/fdroid/fdroid/installer/InstallHelper.java new file mode 100644 index 000000000..db1781039 --- /dev/null +++ b/app/src/main/java/org/fdroid/fdroid/installer/InstallHelper.java @@ -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 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); + } +} diff --git a/app/src/main/java/org/fdroid/fdroid/installer/InstallManagerService.java b/app/src/main/java/org/fdroid/fdroid/installer/InstallManagerService.java index 237a2584b..82eeb125d 100644 --- a/app/src/main/java/org/fdroid/fdroid/installer/InstallManagerService.java +++ b/app/src/main/java/org/fdroid/fdroid/installer/InstallManagerService.java @@ -85,18 +85,6 @@ public class InstallManagerService extends Service { */ private final HashMap 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. - *

- * TODO delete me once InstallerService exists - */ - private static final HashMap 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 */ - 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); } /** diff --git a/app/src/main/java/org/fdroid/fdroid/installer/InstallerService.java b/app/src/main/java/org/fdroid/fdroid/installer/InstallerService.java new file mode 100644 index 000000000..13fbc7694 --- /dev/null +++ b/app/src/main/java/org/fdroid/fdroid/installer/InstallerService.java @@ -0,0 +1,264 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * Copyright (C) 2016 Hans-Christoph Steiner + * Copyright (C) 2016 Dominik Schürmann + * + * 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; + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3dade6788..d07727e72 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -370,6 +370,8 @@ Provided by %1$s. Downloading… Downloading %1$s + Installing… + Uninstalling… Never Hourly