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 01/21] 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 From a2356b05d64ff232d6c8d817f73e7e492a5e4f12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Sch=C3=BCrmann?= Date: Mon, 23 May 2016 20:48:45 +0300 Subject: [PATCH 02/21] Refactor installer package --- .../java/org/fdroid/fdroid/AppDetails.java | 26 +- .../main/java/org/fdroid/fdroid/Utils.java | 2 +- .../installer/AndroidInstallerActivity.java | 85 ++--- .../fdroid/installer/DefaultInstaller.java | 147 ++++---- .../installer/DefaultSdk14Installer.java | 134 ------- .../fdroid/installer/InstallHelper.java | 128 ------- .../installer/InstallManagerService.java | 48 ++- .../fdroid/fdroid/installer/Installer.java | 338 +++++++++--------- .../fdroid/installer/InstallerFactory.java | 61 ++++ .../fdroid/installer/InstallerService.java | 131 +------ .../fdroid/installer/PrivilegedInstaller.java | 137 +++---- .../views/swap/SwapWorkflowActivity.java | 63 +++- 12 files changed, 473 insertions(+), 827 deletions(-) delete mode 100644 app/src/main/java/org/fdroid/fdroid/installer/DefaultSdk14Installer.java delete mode 100644 app/src/main/java/org/fdroid/fdroid/installer/InstallHelper.java create mode 100644 app/src/main/java/org/fdroid/fdroid/installer/InstallerFactory.java diff --git a/app/src/main/java/org/fdroid/fdroid/AppDetails.java b/app/src/main/java/org/fdroid/fdroid/AppDetails.java index 379937361..d3a16343f 100644 --- a/app/src/main/java/org/fdroid/fdroid/AppDetails.java +++ b/app/src/main/java/org/fdroid/fdroid/AppDetails.java @@ -86,7 +86,7 @@ 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.Installer; import org.fdroid.fdroid.installer.InstallManagerService; import org.fdroid.fdroid.installer.InstallerService; import org.fdroid.fdroid.net.Downloader; @@ -532,7 +532,7 @@ public class AppDetails extends AppCompatActivity { Uri localUri = Uri.fromFile(new File(intent.getStringExtra(Downloader.EXTRA_DOWNLOAD_PATH))); localBroadcastManager.registerReceiver(installReceiver, - InstallerService.getInstallIntentFilter(localUri)); + Installer.getInstallIntentFilter(localUri)); } }; @@ -555,12 +555,12 @@ public class AppDetails extends AppCompatActivity { @Override public void onReceive(Context context, Intent intent) { switch (intent.getAction()) { - case InstallHelper.ACTION_INSTALL_STARTED: { + case Installer.ACTION_INSTALL_STARTED: { headerFragment.startProgress(); headerFragment.showIndeterminateProgress(getString(R.string.installing)); break; } - case InstallHelper.ACTION_INSTALL_COMPLETE: { + case Installer.ACTION_INSTALL_COMPLETE: { headerFragment.removeProgress(); localBroadcastManager.unregisterReceiver(this); @@ -569,7 +569,7 @@ public class AppDetails extends AppCompatActivity { onAppChanged(); break; } - case InstallHelper.ACTION_INSTALL_INTERRUPTED: { + case Installer.ACTION_INSTALL_INTERRUPTED: { headerFragment.removeProgress(); localBroadcastManager.unregisterReceiver(this); @@ -613,9 +613,9 @@ public class AppDetails extends AppCompatActivity { // }); break; } - case InstallHelper.ACTION_INSTALL_USER_INTERACTION: { + case Installer.ACTION_INSTALL_USER_INTERACTION: { PendingIntent installPendingIntent = - intent.getParcelableExtra(InstallHelper.EXTRA_USER_INTERACTION_PI); + intent.getParcelableExtra(Installer.EXTRA_USER_INTERACTION_PI); try { installPendingIntent.send(); @@ -636,19 +636,19 @@ public class AppDetails extends AppCompatActivity { @Override public void onReceive(Context context, Intent intent) { switch (intent.getAction()) { - case InstallHelper.ACTION_UNINSTALL_STARTED: { + case Installer.ACTION_UNINSTALL_STARTED: { headerFragment.startProgress(); headerFragment.showIndeterminateProgress(getString(R.string.uninstalling)); break; } - case InstallHelper.ACTION_UNINSTALL_COMPLETE: { + case Installer.ACTION_UNINSTALL_COMPLETE: { headerFragment.removeProgress(); localBroadcastManager.unregisterReceiver(this); onAppChanged(); break; } - case InstallHelper.ACTION_UNINSTALL_INTERRUPTED: { + case Installer.ACTION_UNINSTALL_INTERRUPTED: { headerFragment.removeProgress(); localBroadcastManager.unregisterReceiver(this); @@ -691,9 +691,9 @@ public class AppDetails extends AppCompatActivity { // }); break; } - case InstallHelper.ACTION_UNINSTALL_USER_INTERACTION: { + case Installer.ACTION_UNINSTALL_USER_INTERACTION: { PendingIntent uninstallPendingIntent = - intent.getParcelableExtra(InstallHelper.EXTRA_USER_INTERACTION_PI); + intent.getParcelableExtra(Installer.EXTRA_USER_INTERACTION_PI); try { uninstallPendingIntent.send(); @@ -1038,7 +1038,7 @@ public class AppDetails extends AppCompatActivity { private void uninstallApk(String packageName) { localBroadcastManager.registerReceiver(uninstallReceiver, - InstallerService.getUninstallIntentFilter(packageName)); + Installer.getUninstallIntentFilter(packageName)); InstallerService.uninstall(context, packageName); } diff --git a/app/src/main/java/org/fdroid/fdroid/Utils.java b/app/src/main/java/org/fdroid/fdroid/Utils.java index c93a6e466..31a48692e 100644 --- a/app/src/main/java/org/fdroid/fdroid/Utils.java +++ b/app/src/main/java/org/fdroid/fdroid/Utils.java @@ -270,7 +270,7 @@ public final class Utils { * This location is only for caching, do not install directly from this location * because if the file is on the External Storage, any other app could swap out * the APK while the install was in process, allowing malware to install things. - * Using {@link org.fdroid.fdroid.installer.Installer#installPackage(File, String, String)} + * Using {@link Installer#installPackage(File, String, String)} * is fine since that does the right thing. */ public static File getApkCacheDir(Context context) { diff --git a/app/src/main/java/org/fdroid/fdroid/installer/AndroidInstallerActivity.java b/app/src/main/java/org/fdroid/fdroid/installer/AndroidInstallerActivity.java index 4764941cc..efed31af4 100644 --- a/app/src/main/java/org/fdroid/fdroid/installer/AndroidInstallerActivity.java +++ b/app/src/main/java/org/fdroid/fdroid/installer/AndroidInstallerActivity.java @@ -28,8 +28,6 @@ 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; @@ -49,18 +47,19 @@ public class AndroidInstallerActivity extends FragmentActivity { 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; + // for the broadcasts + private DefaultInstaller installer; + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - localBroadcastManager = LocalBroadcastManager.getInstance(this); + installer = new DefaultInstaller(this); Intent intent = getIntent(); String action = intent.getAction(); @@ -131,12 +130,12 @@ public class AndroidInstallerActivity extends FragmentActivity { startActivityForResult(intent, REQUEST_CODE_INSTALL); } catch (ActivityNotFoundException e) { Log.e(TAG, "ActivityNotFoundException", e); - sendBroadcastInstall(uri, originatingUri, InstallHelper.ACTION_INSTALL_INTERRUPTED, + installer.sendBroadcastInstall(uri, originatingUri, Installer.ACTION_INSTALL_INTERRUPTED, "This Android rom does not support ACTION_INSTALL_PACKAGE!"); finish(); } - sendBroadcastInstall(mInstallUri, mInstallOriginatingUri, - InstallHelper.ACTION_INSTALL_STARTED); + installer.sendBroadcastInstall(mInstallUri, mInstallOriginatingUri, + Installer.ACTION_INSTALL_STARTED); } protected void uninstallPackage(String packageName) { @@ -147,7 +146,7 @@ public class AndroidInstallerActivity extends FragmentActivity { getPackageManager().getPackageInfo(packageName, 0); } catch (PackageManager.NameNotFoundException e) { Log.e(TAG, "NameNotFoundException", e); - sendBroadcastUninstall(packageName, InstallHelper.ACTION_UNINSTALL_INTERRUPTED, + installer.sendBroadcastUninstall(packageName, Installer.ACTION_UNINSTALL_INTERRUPTED, "Package that is scheduled for uninstall is not installed!"); finish(); return; @@ -167,7 +166,7 @@ public class AndroidInstallerActivity extends FragmentActivity { startActivityForResult(intent, REQUEST_CODE_UNINSTALL); } catch (ActivityNotFoundException e) { Log.e(TAG, "ActivityNotFoundException", e); - sendBroadcastUninstall(packageName, InstallHelper.ACTION_UNINSTALL_INTERRUPTED, + installer.sendBroadcastUninstall(packageName, Installer.ACTION_UNINSTALL_INTERRUPTED, "This Android rom does not support ACTION_UNINSTALL_PACKAGE!"); finish(); } @@ -183,34 +182,34 @@ public class AndroidInstallerActivity extends FragmentActivity { * never executed on Androids < 4.0 */ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) { - sendBroadcastInstall(mInstallUri, mInstallOriginatingUri, - InstallHelper.ACTION_INSTALL_COMPLETE); + installer.sendBroadcastInstall(mInstallUri, mInstallOriginatingUri, + Installer.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); + installer.sendBroadcastInstall(mInstallUri, mInstallOriginatingUri, + Installer.ACTION_INSTALL_COMPLETE); break; } switch (resultCode) { case Activity.RESULT_OK: { - sendBroadcastInstall(mInstallUri, mInstallOriginatingUri, - InstallHelper.ACTION_INSTALL_COMPLETE); + installer.sendBroadcastInstall(mInstallUri, mInstallOriginatingUri, + Installer.ACTION_INSTALL_COMPLETE); break; } case Activity.RESULT_CANCELED: { - sendBroadcastInstall(mInstallUri, mInstallOriginatingUri, - InstallHelper.ACTION_INSTALL_INTERRUPTED); + installer.sendBroadcastInstall(mInstallUri, mInstallOriginatingUri, + Installer.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"); + installer.sendBroadcastInstall(mInstallUri, mInstallOriginatingUri, + Installer.ACTION_INSTALL_INTERRUPTED, "error"); break; } } @@ -220,28 +219,28 @@ public class AndroidInstallerActivity extends FragmentActivity { 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); + installer.sendBroadcastUninstall(mUninstallPackageName, + Installer.ACTION_UNINSTALL_COMPLETE); break; } switch (resultCode) { case Activity.RESULT_OK: { - sendBroadcastUninstall(mUninstallPackageName, - InstallHelper.ACTION_UNINSTALL_COMPLETE); + installer.sendBroadcastUninstall(mUninstallPackageName, + Installer.ACTION_UNINSTALL_COMPLETE); break; } case Activity.RESULT_CANCELED: { - sendBroadcastUninstall(mUninstallPackageName, - InstallHelper.ACTION_UNINSTALL_INTERRUPTED); + installer.sendBroadcastUninstall(mUninstallPackageName, + Installer.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, + installer.sendBroadcastUninstall(mUninstallPackageName, + Installer.ACTION_UNINSTALL_INTERRUPTED, "error"); break; } @@ -258,34 +257,4 @@ public class AndroidInstallerActivity extends FragmentActivity { 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/DefaultInstaller.java b/app/src/main/java/org/fdroid/fdroid/installer/DefaultInstaller.java index a6fa2b9be..a424460dc 100644 --- a/app/src/main/java/org/fdroid/fdroid/installer/DefaultInstaller.java +++ b/app/src/main/java/org/fdroid/fdroid/installer/DefaultInstaller.java @@ -1,100 +1,97 @@ -/* - * Copyright (C) 2014 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.app.Activity; -import android.content.ActivityNotFoundException; +import android.app.PendingIntent; +import android.content.Context; import android.content.Intent; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager; import android.net.Uri; +import org.fdroid.fdroid.BuildConfig; +import org.fdroid.fdroid.Utils; +import org.fdroid.fdroid.privileged.install.InstallExtensionDialogActivity; + import java.io.File; -/** - * For Android < 4: Default Installer using the public PackageManager API of - * Android to install/delete packages. This starts a Activity from the Android - * OS showing all permissions/changed permissions. The the user needs to - * manually press an install button, this Installer cannot be used for - * unattended installations. - */ public class DefaultInstaller extends Installer { - private final Activity mActivity; - public DefaultInstaller(Activity activity, PackageManager pm, InstallerCallback callback) - throws InstallFailedException { - super(activity, pm, callback); - this.mActivity = activity; + private static final String TAG = "DefaultInstaller"; + + DefaultInstaller(Context context) { + super(context); } - private static final int REQUEST_CODE_INSTALL = 0; - private static final int REQUEST_CODE_DELETE = 1; - @Override - protected void installPackageInternal(File apkFile) throws InstallFailedException { - Intent intent = new Intent(); - intent.setAction(Intent.ACTION_VIEW); - intent.setDataAndType(Uri.fromFile(apkFile), - "application/vnd.android.package-archive"); + protected void installPackage(Uri uri, Uri originatingUri, String packageName) { + + sendBroadcastInstall(uri, originatingUri, Installer.ACTION_INSTALL_STARTED); + + Utils.debugLog(TAG, "ACTION_INSTALL uri: " + uri + " file: " + new File(uri.getPath())); + + // TODO: rework for uri + File sanitizedFile = null; try { - mActivity.startActivityForResult(intent, REQUEST_CODE_INSTALL); - } catch (ActivityNotFoundException e) { - throw new InstallFailedException(e); + sanitizedFile = Installer.prepareApkFile(mContext, new File(uri.getPath()), packageName); + } catch (Installer.InstallFailedException e) { + e.printStackTrace(); } - } + Uri sanitizedUri = Uri.fromFile(sanitizedFile); - @Override - protected void deletePackageInternal(String packageName) throws InstallFailedException { - try { - PackageInfo pkgInfo = mPm.getPackageInfo(packageName, 0); + Utils.debugLog(TAG, "ACTION_INSTALL sanitizedUri: " + sanitizedUri); - Uri uri = Uri.fromParts("package", pkgInfo.packageName, null); - Intent intent = new Intent(Intent.ACTION_DELETE, uri); - try { - mActivity.startActivityForResult(intent, REQUEST_CODE_DELETE); - } catch (ActivityNotFoundException e) { - throw new InstallFailedException(e); + Intent installIntent; + // 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(mContext); + if (!BuildConfig.DEBUG && !signatureVerifier.hasFDroidSignature(sanitizedFile)) { + throw new RuntimeException("APK signature of extension not correct!"); } - } catch (PackageManager.NameNotFoundException e) { - // already checked in super class + + installIntent = new Intent(mContext, InstallExtensionDialogActivity.class); + installIntent.setAction(InstallExtensionDialogActivity.ACTION_INSTALL); + installIntent.putExtra(InstallExtensionDialogActivity.EXTRA_INSTALL_APK, + sanitizedFile.getAbsolutePath()); + } else { + installIntent = new Intent(mContext, AndroidInstallerActivity.class); + installIntent.setAction(AndroidInstallerActivity.ACTION_INSTALL_PACKAGE); + installIntent.putExtra(AndroidInstallerActivity.EXTRA_ORIGINATING_URI, originatingUri); + installIntent.setData(sanitizedUri); } + + PendingIntent installPendingIntent = PendingIntent.getActivity( + mContext.getApplicationContext(), + uri.hashCode(), + installIntent, + PendingIntent.FLAG_UPDATE_CURRENT); + + sendBroadcastInstall(uri, originatingUri, + Installer.ACTION_INSTALL_USER_INTERACTION, installPendingIntent); + } @Override - public boolean handleOnActivityResult(int requestCode, int resultCode, Intent data) { - /** - * resultCode is always 0 on Android < 4.0. See - * com.android.packageinstaller.PackageInstallerActivity: setResult is - * never executed on Androids before 4.0 - */ - switch (requestCode) { - case REQUEST_CODE_INSTALL: - mCallback.onSuccess(InstallerCallback.OPERATION_INSTALL); + protected void uninstallPackage(String packageName) { + sendBroadcastUninstall(packageName, Installer.ACTION_UNINSTALL_STARTED); - return true; - case REQUEST_CODE_DELETE: - mCallback.onSuccess(InstallerCallback.OPERATION_DELETE); - - return true; - default: - return false; + Intent uninstallIntent; + // special case: F-Droid Privileged Extension + if (packageName != null && packageName.equals(PrivilegedInstaller.PRIVILEGED_EXTENSION_PACKAGE_NAME)) { + uninstallIntent = new Intent(mContext, InstallExtensionDialogActivity.class); + uninstallIntent.setAction(InstallExtensionDialogActivity.ACTION_UNINSTALL); + } else { + uninstallIntent = new Intent(mContext, AndroidInstallerActivity.class); + uninstallIntent.setAction(AndroidInstallerActivity.ACTION_UNINSTALL_PACKAGE); + uninstallIntent.putExtra( + AndroidInstallerActivity.EXTRA_UNINSTALL_PACKAGE_NAME, packageName); } + PendingIntent uninstallPendingIntent = PendingIntent.getActivity( + mContext.getApplicationContext(), + packageName.hashCode(), + uninstallIntent, + PendingIntent.FLAG_UPDATE_CURRENT); + + sendBroadcastUninstall(packageName, + Installer.ACTION_UNINSTALL_USER_INTERACTION, uninstallPendingIntent); } } diff --git a/app/src/main/java/org/fdroid/fdroid/installer/DefaultSdk14Installer.java b/app/src/main/java/org/fdroid/fdroid/installer/DefaultSdk14Installer.java deleted file mode 100644 index 6ca757dc6..000000000 --- a/app/src/main/java/org/fdroid/fdroid/installer/DefaultSdk14Installer.java +++ /dev/null @@ -1,134 +0,0 @@ -/* - * Copyright (C) 2014 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.TargetApi; -import android.app.Activity; -import android.content.ActivityNotFoundException; -import android.content.Intent; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager; -import android.net.Uri; -import android.os.Build; - -import java.io.File; - -/** - * For Android >= 4.0: Default Installer using the public PackageManager API of - * Android to install/delete packages. This starts a Activity from the Android - * OS showing all permissions/changed permissions. The the user needs to - * manually press an install button, this Installer cannot be used for - * unattended installations. - */ -@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) -public class DefaultSdk14Installer extends Installer { - private final Activity mActivity; - - public DefaultSdk14Installer(Activity activity, PackageManager pm, InstallerCallback callback) - throws InstallFailedException { - super(activity, pm, callback); - this.mActivity = activity; - } - - private static final int REQUEST_CODE_INSTALL = 0; - private static final int REQUEST_CODE_DELETE = 1; - - @SuppressWarnings("deprecation") - @Override - protected void installPackageInternal(File apkFile) throws InstallFailedException { - Intent intent = new Intent(); - intent.setAction(Intent.ACTION_INSTALL_PACKAGE); - intent.setData(Uri.fromFile(apkFile)); - // 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 < 16) { - // deprecated in Android 4.1 - intent.putExtra(Intent.EXTRA_ALLOW_REPLACE, true); - } - try { - mActivity.startActivityForResult(intent, REQUEST_CODE_INSTALL); - } catch (ActivityNotFoundException e) { - throw new InstallFailedException(e); - } - } - - @Override - protected void deletePackageInternal(String packageName) throws InstallFailedException { - try { - PackageInfo pkgInfo = mPm.getPackageInfo(packageName, 0); - - Uri uri = Uri.fromParts("package", pkgInfo.packageName, null); - Intent intent = new Intent(Intent.ACTION_UNINSTALL_PACKAGE, uri); - intent.putExtra(Intent.EXTRA_RETURN_RESULT, true); - try { - mActivity.startActivityForResult(intent, REQUEST_CODE_DELETE); - } catch (ActivityNotFoundException e) { - throw new InstallFailedException(e); - } - } catch (PackageManager.NameNotFoundException e) { - // already checked in super class - } - } - - @Override - public boolean handleOnActivityResult(int requestCode, int resultCode, Intent data) { - switch (requestCode) { - case REQUEST_CODE_INSTALL: - if (resultCode == Activity.RESULT_OK) { - mCallback.onSuccess(InstallerCallback.OPERATION_INSTALL); - } else if (resultCode == Activity.RESULT_CANCELED) { - mCallback.onError(InstallerCallback.OPERATION_INSTALL, - InstallerCallback.ERROR_CODE_CANCELED); - } else { - mCallback.onError(InstallerCallback.OPERATION_INSTALL, - InstallerCallback.ERROR_CODE_OTHER); - } - // Fallback on N for https://gitlab.com/fdroid/fdroidclient/issues/631 - if ("N".equals(Build.VERSION.CODENAME)) { - mCallback.onSuccess(InstallerCallback.OPERATION_INSTALL); - } - - return true; - case REQUEST_CODE_DELETE: - if (resultCode == Activity.RESULT_OK) { - mCallback.onSuccess(InstallerCallback.OPERATION_DELETE); - } else if (resultCode == Activity.RESULT_CANCELED) { - mCallback.onError(InstallerCallback.OPERATION_DELETE, - InstallerCallback.ERROR_CODE_CANCELED); - } else { - // UninstallAppProgress actually returns - // Activity.RESULT_FIRST_USER if something breaks - mCallback.onError(InstallerCallback.OPERATION_DELETE, - InstallerCallback.ERROR_CODE_OTHER); - } - - return true; - default: - return false; - } - } -} diff --git a/app/src/main/java/org/fdroid/fdroid/installer/InstallHelper.java b/app/src/main/java/org/fdroid/fdroid/installer/InstallHelper.java deleted file mode 100644 index db1781039..000000000 --- a/app/src/main/java/org/fdroid/fdroid/installer/InstallHelper.java +++ /dev/null @@ -1,128 +0,0 @@ -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 82eeb125d..2630c6081 100644 --- a/app/src/main/java/org/fdroid/fdroid/installer/InstallManagerService.java +++ b/app/src/main/java/org/fdroid/fdroid/installer/InstallManagerService.java @@ -168,7 +168,7 @@ public class InstallManagerService extends Service { sendBroadcast(intent.getData(), Downloader.ACTION_STARTED, apkFilePath); sendBroadcast(intent.getData(), Downloader.ACTION_COMPLETE, apkFilePath); } else { - Utils.debugLog(TAG, " delete and download again " + urlString + " " + apkFilePath); + Utils.debugLog(TAG, "delete and download again " + urlString + " " + apkFilePath); apkFilePath.delete(); DownloaderService.queue(this, urlString); } @@ -233,14 +233,15 @@ public class InstallManagerService extends Service { unregisterDownloaderReceivers(intent.getDataString()); registerInstallerReceivers(localUri); - InstallerService.install(context, localUri, originatingUri); + Apk apk = ACTIVE_APKS.get(originatingUri.toString()); + InstallerService.install(context, localUri, originatingUri, apk.packageName); } }; BroadcastReceiver interruptedReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { String urlString = intent.getDataString(); - Apk apk = removeFromActive(urlString); + removeFromActive(urlString); unregisterDownloaderReceivers(urlString); cancelNotification(urlString); } @@ -266,16 +267,12 @@ public class InstallManagerService extends Service { @Override public void onReceive(Context context, Intent intent) { switch (intent.getAction()) { - case InstallHelper.ACTION_INSTALL_STARTED: { - Utils.debugLog(TAG, "ACTION_INSTALL_STARTED"); - + case Installer.ACTION_INSTALL_STARTED: { break; } - case InstallHelper.ACTION_INSTALL_COMPLETE: { - Utils.debugLog(TAG, "ACTION_INSTALL_COMPLETE"); - + case Installer.ACTION_INSTALL_COMPLETE: { Uri originatingUri = - intent.getParcelableExtra(InstallHelper.EXTRA_ORIGINATING_URI); + intent.getParcelableExtra(Installer.EXTRA_ORIGINATING_URI); String urlString = originatingUri.toString(); removeFromActive(urlString); @@ -283,29 +280,24 @@ public class InstallManagerService extends Service { break; } - case InstallHelper.ACTION_INSTALL_INTERRUPTED: { - Utils.debugLog(TAG, "ACTION_INSTALL_INTERRUPTED"); - + case Installer.ACTION_INSTALL_INTERRUPTED: { localBroadcastManager.unregisterReceiver(this); break; } - case InstallHelper.ACTION_INSTALL_USER_INTERACTION: { - Utils.debugLog(TAG, "ACTION_INSTALL_USER_INTERACTION"); - + case Installer.ACTION_INSTALL_USER_INTERACTION: { Uri originatingUri = - intent.getParcelableExtra(InstallHelper.EXTRA_ORIGINATING_URI); + intent.getParcelableExtra(Installer.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); + intent.getParcelableExtra(Installer.EXTRA_USER_INTERACTION_PI); + Utils.debugLog(TAG, "originatingUri: " + originatingUri); + Apk apk = getFromActive(originatingUri.toString()); + // show notification if app details is not visible if (AppDetails.isAppVisible(apk.packageName)) { - cancelNotification(urlString); + cancelNotification(originatingUri.toString()); } else { - notifyDownloadComplete(apk, urlString, installPendingIntent); + notifyDownloadComplete(apk, originatingUri.toString(), installPendingIntent); } break; @@ -318,7 +310,7 @@ public class InstallManagerService extends Service { }; localBroadcastManager.registerReceiver(installReceiver, - InstallerService.getInstallIntentFilter(uri)); + Installer.getInstallIntentFilter(uri)); } private NotificationCompat.Builder createNotificationBuilder(String urlString, Apk apk) { @@ -327,7 +319,7 @@ public class InstallManagerService extends Service { .setAutoCancel(false) .setOngoing(true) .setContentIntent(getAppDetailsIntent(downloadUrlId, apk)) - .setContentTitle(getString(R.string.downloading_apk, getAppName(urlString, apk))) + .setContentTitle(getString(R.string.downloading_apk, getAppName(apk))) .addAction(R.drawable.ic_cancel_black_24dp, getString(R.string.cancel), DownloaderService.getCancelPendingIntent(this, urlString)) .setSmallIcon(android.R.drawable.stat_sys_download) @@ -335,7 +327,7 @@ public class InstallManagerService extends Service { .setProgress(100, 0, true); } - private String getAppName(String urlString, Apk apk) { + private String getAppName(Apk apk) { App app = ACTIVE_APPS.get(apk.packageName); return app.name; } @@ -371,7 +363,7 @@ public class InstallManagerService extends Service { title = String.format(getString(R.string.tap_to_update_format), pm.getApplicationLabel(pm.getApplicationInfo(apk.packageName, 0))); } catch (PackageManager.NameNotFoundException e) { - title = String.format(getString(R.string.tap_to_install_format), getAppName(urlString, apk)); + title = String.format(getString(R.string.tap_to_install_format), getAppName(apk)); } int downloadUrlId = urlString.hashCode(); diff --git a/app/src/main/java/org/fdroid/fdroid/installer/Installer.java b/app/src/main/java/org/fdroid/fdroid/installer/Installer.java index f9800f74f..458cd1f3a 100644 --- a/app/src/main/java/org/fdroid/fdroid/installer/Installer.java +++ b/app/src/main/java/org/fdroid/fdroid/installer/Installer.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2014 Dominik Schürmann + * Copyright (C) 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 @@ -19,47 +19,55 @@ package org.fdroid.fdroid.installer; -import android.app.Activity; -import android.app.NotificationManager; +import android.app.PendingIntent; import android.content.Context; import android.content.Intent; +import android.content.IntentFilter; import android.content.pm.PackageManager; +import android.net.Uri; +import android.os.PatternMatcher; +import android.support.v4.content.LocalBroadcastManager; import android.text.TextUtils; -import android.util.Log; 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.Preferences; -import org.fdroid.fdroid.Utils; import org.fdroid.fdroid.data.Apk; import org.fdroid.fdroid.data.ApkProvider; import org.fdroid.fdroid.data.SanitizedFile; -import org.fdroid.fdroid.privileged.install.InstallExtensionDialogActivity; +import org.fdroid.fdroid.privileged.views.AppDiff; +import org.fdroid.fdroid.privileged.views.AppSecurityPermissions; +import org.fdroid.fdroid.privileged.views.InstallConfirmActivity; import java.io.File; import java.io.IOException; import java.security.NoSuchAlgorithmException; import java.util.Map; -/** - * Abstract Installer class. Also provides static methods to automatically - * instantiate a working Installer based on F-Droids granted permissions. - */ public abstract class Installer { final Context mContext; final PackageManager mPm; - final InstallerCallback mCallback; + private LocalBroadcastManager localBroadcastManager; - private static final String TAG = "Installer"; + public static final String ACTION_INSTALL_STARTED = "org.fdroid.fdroid.installer.Installer.action.INSTALL_STARTED"; + public static final String ACTION_INSTALL_COMPLETE = "org.fdroid.fdroid.installer.Installer.action.INSTALL_COMPLETE"; + public static final String ACTION_INSTALL_INTERRUPTED = "org.fdroid.fdroid.installer.Installer.action.INSTALL_INTERRUPTED"; + 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"; /** - * This is thrown when an Installer is not compatible with the Android OS it - * is running on. This could be due to a broken superuser in case of - * RootInstaller or due to an incompatible Android version in case of - * SystemPermissionInstaller + * 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_PACKAGE_NAME = "org.fdroid.fdroid.installer.InstallerService.extra.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 class InstallFailedException extends Exception { private static final long serialVersionUID = -8343133906463328027L; @@ -77,97 +85,28 @@ public abstract class Installer { * Callback from Installer. NOTE: This callback can be in a different thread * than the UI thread */ - public interface InstallerCallback { +// public interface InstallerCallback { +// +// int OPERATION_INSTALL = 1; +// int OPERATION_DELETE = 2; +// +// // Avoid using [-1,1] as they may conflict with Activity.RESULT_* +// int ERROR_CODE_CANCELED = 2; +// int ERROR_CODE_OTHER = 3; +// int ERROR_CODE_CANNOT_PARSE = 4; +// +// void onSuccess(int operation); +// +// void onError(int operation, int errorCode); +// } - int OPERATION_INSTALL = 1; - int OPERATION_DELETE = 2; - - // Avoid using [-1,1] as they may conflict with Activity.RESULT_* - int ERROR_CODE_CANCELED = 2; - int ERROR_CODE_OTHER = 3; - int ERROR_CODE_CANNOT_PARSE = 4; - - void onSuccess(int operation); - - void onError(int operation, int errorCode); - } - - Installer(Context context, PackageManager pm, InstallerCallback callback) - throws InstallFailedException { + Installer(Context context) { this.mContext = context; - this.mPm = pm; - this.mCallback = callback; + this.mPm = context.getPackageManager(); + localBroadcastManager = LocalBroadcastManager.getInstance(context); } - public static Installer getActivityInstaller(Activity activity, InstallerCallback callback) { - return getActivityInstaller(activity, activity.getPackageManager(), callback); - } - - /** - * Creates a new Installer for installing/deleting processes starting from - * an Activity - */ - public static Installer getActivityInstaller(Activity activity, PackageManager pm, - InstallerCallback callback) { - - // system permissions and pref enabled -> SystemInstaller - boolean isSystemInstallerEnabled = Preferences.get().isPrivilegedInstallerEnabled(); - if (isSystemInstallerEnabled) { - if (PrivilegedInstaller.isExtensionInstalledCorrectly(activity) - == PrivilegedInstaller.IS_EXTENSION_INSTALLED_YES) { - Utils.debugLog(TAG, "system permissions -> SystemInstaller"); - - try { - return new PrivilegedInstaller(activity, pm, callback); - } catch (InstallFailedException e) { - Log.e(TAG, "Android not compatible with SystemInstaller!", e); - } - } else { - Log.e(TAG, "SystemInstaller is enabled in prefs, but system-perms are not granted!"); - } - } - - // else -> DefaultInstaller - if (android.os.Build.VERSION.SDK_INT >= 14) { - // Default installer on Android >= 4.0 - try { - Utils.debugLog(TAG, "try default installer for android >= 14"); - - return new DefaultSdk14Installer(activity, pm, callback); - } catch (InstallFailedException e) { - Log.e(TAG, "Android not compatible with DefaultInstallerSdk14!", e); - } - } else { - // Default installer on Android < 4.0 (android-14) - try { - Utils.debugLog(TAG, "try default installer for android < 14"); - - return new DefaultInstaller(activity, pm, callback); - } catch (InstallFailedException e) { - Log.e(TAG, "Android not compatible with DefaultInstaller!", e); - } - } - - // this should not happen! - return null; - } - - /** - * 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); - } - - /** - * This is the safe, single point of entry for submitting an APK file to be installed. - */ - public void installPackage(File apkFile, String packageName, String urlString) + public static SanitizedFile prepareApkFile(Context context, File apkFile, String packageName) throws InstallFailedException { SanitizedFile apkToInstall = null; try { @@ -182,7 +121,7 @@ public abstract class Installer { throw new InstallFailedException(apkFile + " is missing versionCode!"); } int versionCode = (Integer) attributes.get("versionCode"); - Apk apk = ApkProvider.Helper.find(mContext, packageName, versionCode, new String[]{ + Apk apk = ApkProvider.Helper.find(context, packageName, versionCode, new String[]{ ApkProvider.DataColumns.HASH, ApkProvider.DataColumns.HASH_TYPE, }); @@ -190,7 +129,8 @@ public abstract class Installer { * 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", mContext.getFilesDir())); + apkToInstall = SanitizedFile.knownSanitized(File.createTempFile("install-", ".apk", + context.getFilesDir())); FileUtils.copyFile(apkFile, apkToInstall); if (!verifyApkFile(apkToInstall, apk.hash, apk.hashType)) { FileUtils.deleteQuietly(apkFile); @@ -198,36 +138,14 @@ public abstract class Installer { } 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(mContext); - if (!BuildConfig.DEBUG && !signatureVerifier.hasFDroidSignature(apkToInstall)) { - throw new InstallFailedException("APK signature of extension not correct!"); - } - - Activity activity = (Activity) mContext; - Intent installIntent = new Intent(activity, InstallExtensionDialogActivity.class); - installIntent.setAction(InstallExtensionDialogActivity.ACTION_INSTALL); - installIntent.putExtra(InstallExtensionDialogActivity.EXTRA_INSTALL_APK, apkToInstall.getAbsolutePath()); - activity.startActivity(installIntent); - return; - } - // 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. apkToInstall.setReadable(true, false); - installPackageInternal(apkToInstall); - NotificationManager nm = (NotificationManager) - mContext.getSystemService(Context.NOTIFICATION_SERVICE); - nm.cancel(urlString.hashCode()); - } catch (NumberFormatException | NoSuchAlgorithmException | IOException e) { + } catch (NumberFormatException | IOException | NoSuchAlgorithmException e) { throw new InstallFailedException(e); } catch (ClassCastException e) { throw new InstallFailedException("F-Droid Privileged can only be updated using an activity!"); @@ -248,41 +166,139 @@ public abstract class Installer { } }.start(); } + + return apkToInstall; } - public void deletePackage(String packageName) throws InstallFailedException { - // check if package exists before proceeding... - try { - mPm.getPackageInfo(packageName, 0); - } catch (PackageManager.NameNotFoundException e) { - Log.e(TAG, "Couldn't find package " + packageName + " to delete."); - return; - } - - // special case: F-Droid Privileged Extension - if (packageName != null && packageName.equals(PrivilegedInstaller.PRIVILEGED_EXTENSION_PACKAGE_NAME)) { - Activity activity; - try { - activity = (Activity) mContext; - } catch (ClassCastException e) { - Utils.debugLog(TAG, "F-Droid Privileged can only be uninstalled using an activity!"); - return; - } - - Intent uninstallIntent = new Intent(activity, InstallExtensionDialogActivity.class); - uninstallIntent.setAction(InstallExtensionDialogActivity.ACTION_UNINSTALL); - activity.startActivity(uninstallIntent); - return; - } - - deletePackageInternal(packageName); + public PendingIntent getPermissionScreen(Apk apk) { + // old code: +// Uri packageUri = Uri.fromFile(apkFile); +// int count = newPermissionCount(packageUri); +// if (count < 0) { +// mCallback.onError(InstallerCallback.OPERATION_INSTALL, +// InstallerCallback.ERROR_CODE_CANNOT_PARSE); +// return; +// } +// if (count > 0) { +// Intent intent = new Intent(mContext, InstallConfirmActivity.class); +// intent.setData(packageUri); +// mActivity.startActivityForResult(intent, REQUEST_CONFIRM_PERMS); +// } else { +// try { +// doInstallPackageInternal(packageUri); +// } catch (InstallFailedException e) { +// mCallback.onError(InstallerCallback.OPERATION_INSTALL, +// InstallerCallback.ERROR_CODE_OTHER); +// } +// } + return null; } - protected abstract void installPackageInternal(File apkFile) - throws InstallFailedException; - protected abstract void deletePackageInternal(String packageName) - throws InstallFailedException; + private int newPermissionCount(Uri packageUri) { + AppDiff appDiff = new AppDiff(mContext.getPackageManager(), packageUri); + if (appDiff.mPkgInfo == null) { + // could not get diff because we couldn't parse the package + return -1; + } + AppSecurityPermissions perms = new AppSecurityPermissions(mContext, appDiff.mPkgInfo); + if (appDiff.mInstalledAppInfo != null) { + // update to an existing app + return perms.getPermissionCount(AppSecurityPermissions.WHICH_NEW); + } + // default: even if there aren't any permissions, we want to make the + // user always confirm installing new apps + return 1; + } + + /** + * 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); + } + + public void sendBroadcastInstall(Uri uri, Uri originatingUri, String action, + PendingIntent pendingIntent) { + sendBroadcastInstall(uri, originatingUri, action, pendingIntent, null); + } + + public void sendBroadcastInstall(Uri uri, Uri originatingUri, String action) { + sendBroadcastInstall(uri, originatingUri, action, null, null); + } + + public void sendBroadcastInstall(Uri uri, Uri originatingUri, String action, String errorMessage) { + sendBroadcastInstall(uri, originatingUri, action, null, errorMessage); + } + + public void sendBroadcastInstall(Uri uri, Uri originatingUri, String action, + PendingIntent pendingIntent, String errorMessage) { + Intent intent = new Intent(action); + intent.setData(uri); + intent.putExtra(Installer.EXTRA_ORIGINATING_URI, originatingUri); + intent.putExtra(Installer.EXTRA_USER_INTERACTION_PI, pendingIntent); + if (!TextUtils.isEmpty(errorMessage)) { + intent.putExtra(Installer.EXTRA_ERROR_MESSAGE, errorMessage); + } + localBroadcastManager.sendBroadcast(intent); + } + + public void sendBroadcastUninstall(String packageName, String action, String errorMessage) { + sendBroadcastUninstall(packageName, action, null, errorMessage); + } + + public void sendBroadcastUninstall(String packageName, String action) { + sendBroadcastUninstall(packageName, action, null, null); + } + + public void sendBroadcastUninstall(String packageName, String action, + PendingIntent pendingIntent) { + sendBroadcastUninstall(packageName, action, pendingIntent, null); + } + + public 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(Installer.EXTRA_PACKAGE_NAME, packageName); + intent.putExtra(Installer.EXTRA_USER_INTERACTION_PI, pendingIntent); + if (!TextUtils.isEmpty(errorMessage)) { + intent.putExtra(Installer.EXTRA_ERROR_MESSAGE, errorMessage); + } + localBroadcastManager.sendBroadcast(intent); + } + + 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.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; + } + + protected abstract void installPackage(Uri uri, Uri originatingUri, String packageName); + + protected abstract void uninstallPackage(String packageName); - public abstract boolean handleOnActivityResult(int requestCode, int resultCode, Intent data); } diff --git a/app/src/main/java/org/fdroid/fdroid/installer/InstallerFactory.java b/app/src/main/java/org/fdroid/fdroid/installer/InstallerFactory.java new file mode 100644 index 000000000..52663b42b --- /dev/null +++ b/app/src/main/java/org/fdroid/fdroid/installer/InstallerFactory.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 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.content.Context; +import android.util.Log; + +import org.fdroid.fdroid.Preferences; +import org.fdroid.fdroid.Utils; + +public class InstallerFactory { + + private static final String TAG = "InstallerFactory"; + + public static Installer create(Context context) { + Installer installer; + + if (isPrivilegedInstallerEnabled()) { + if (PrivilegedInstaller.isExtensionInstalledCorrectly(context) + == PrivilegedInstaller.IS_EXTENSION_INSTALLED_YES) { + Utils.debugLog(TAG, "privileged extension correctly installed -> PrivilegedInstaller"); + + installer = new PrivilegedInstaller(context); + } else { + Log.e(TAG, "PrivilegedInstaller is enabled in prefs, but permissions are not granted!"); + + // fallback to default installer + installer = new DefaultInstaller(context); + } + } else { + installer = new DefaultInstaller(context); + } + + return installer; + } + + /** + * Extension has privileged permissions and preference is enabled? + */ + private static boolean isPrivilegedInstallerEnabled() { + return Preferences.get().isPrivilegedInstallerEnabled(); + } + +} diff --git a/app/src/main/java/org/fdroid/fdroid/installer/InstallerService.java b/app/src/main/java/org/fdroid/fdroid/installer/InstallerService.java index 13fbc7694..323356284 100644 --- a/app/src/main/java/org/fdroid/fdroid/installer/InstallerService.java +++ b/app/src/main/java/org/fdroid/fdroid/installer/InstallerService.java @@ -18,27 +18,21 @@ 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 */ @@ -51,6 +45,7 @@ public class InstallerService extends Service { private volatile Looper serviceLooper; private static volatile ServiceHandler serviceHandler; private LocalBroadcastManager localBroadcastManager; + private Installer installer; private final class ServiceHandler extends Handler { ServiceHandler(Looper looper) { @@ -76,6 +71,7 @@ public class InstallerService extends Service { serviceLooper = thread.getLooper(); serviceHandler = new ServiceHandler(serviceLooper); localBroadcastManager = LocalBroadcastManager.getInstance(this); + installer = InstallerFactory.create(this); } @Override @@ -92,7 +88,7 @@ public class InstallerService extends Service { 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); + String packageName = intent.getStringExtra(Installer.EXTRA_PACKAGE_NAME); Message msg = serviceHandler.obtainMessage(); msg.arg1 = startId; @@ -125,140 +121,37 @@ public class InstallerService extends Service { 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); + Uri originatingUri = intent.getParcelableExtra(Installer.EXTRA_ORIGINATING_URI); + String packageName = intent.getStringExtra(Installer.EXTRA_PACKAGE_NAME); + installer.installPackage(uri, originatingUri, packageName); 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); - + intent.getStringExtra(Installer.EXTRA_PACKAGE_NAME); + installer.uninstallPackage(packageName); 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) { + public static void install(Context context, Uri uri, Uri originatingUri, String packageName) { Intent intent = new Intent(context, InstallerService.class); intent.setAction(ACTION_INSTALL); intent.setData(uri); - intent.putExtra(InstallHelper.EXTRA_ORIGINATING_URI, originatingUri); + intent.putExtra(Installer.EXTRA_ORIGINATING_URI, originatingUri); + intent.putExtra(Installer.EXTRA_PACKAGE_NAME, packageName); 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); + intent.putExtra(Installer.EXTRA_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/java/org/fdroid/fdroid/installer/PrivilegedInstaller.java b/app/src/main/java/org/fdroid/fdroid/installer/PrivilegedInstaller.java index 324ed7767..14eee9c5e 100644 --- a/app/src/main/java/org/fdroid/fdroid/installer/PrivilegedInstaller.java +++ b/app/src/main/java/org/fdroid/fdroid/installer/PrivilegedInstaller.java @@ -20,7 +20,6 @@ package org.fdroid.fdroid.installer; -import android.app.Activity; import android.content.ComponentName; import android.content.Context; import android.content.DialogInterface; @@ -39,9 +38,6 @@ import org.fdroid.fdroid.R; import org.fdroid.fdroid.Utils; import org.fdroid.fdroid.privileged.IPrivilegedCallback; import org.fdroid.fdroid.privileged.IPrivilegedService; -import org.fdroid.fdroid.privileged.views.AppDiff; -import org.fdroid.fdroid.privileged.views.AppSecurityPermissions; -import org.fdroid.fdroid.privileged.views.InstallConfirmActivity; import java.io.File; @@ -52,7 +48,7 @@ import java.io.File; *

  • android.permission.INSTALL_PACKAGES
  • *
  • android.permission.DELETE_PACKAGES
  • * - * + *

    * Both permissions are protected by systemOrSignature (in newer versions: * system|signature) and only granted on F-Droid's install in the following * cases: @@ -62,7 +58,7 @@ import java.io.File; *

  • On Android < 4.4 also when moved into /system/app/
  • *
  • On Android >= 4.4 also when moved into /system/priv-app/
  • * - * + *

    * Sources for Android 4.4 change: * https://groups.google.com/forum/#!msg/android- * security-discuss/r7uL_OEMU5c/LijNHvxeV80J @@ -76,19 +72,13 @@ public class PrivilegedInstaller extends Installer { private static final String PRIVILEGED_EXTENSION_SERVICE_INTENT = "org.fdroid.fdroid.privileged.IPrivilegedService"; public static final String PRIVILEGED_EXTENSION_PACKAGE_NAME = "org.fdroid.fdroid.privileged"; - private final Activity mActivity; - - private static final int REQUEST_CONFIRM_PERMS = 0; - public static final int IS_EXTENSION_INSTALLED_NO = 0; public static final int IS_EXTENSION_INSTALLED_YES = 1; public static final int IS_EXTENSION_INSTALLED_SIGNATURE_PROBLEM = 2; public static final int IS_EXTENSION_INSTALLED_PERMISSIONS_PROBLEM = 3; - public PrivilegedInstaller(Activity activity, PackageManager pm, - InstallerCallback callback) throws InstallFailedException { - super(activity, pm, callback); - this.mActivity = activity; + public PrivilegedInstaller(Context context) { + super(context); } public static boolean isExtensionInstalled(Context context) { @@ -155,46 +145,10 @@ public class PrivilegedInstaller extends Installer { return IS_EXTENSION_INSTALLED_YES; } + @Override - protected void installPackageInternal(File apkFile) throws InstallFailedException { - Uri packageUri = Uri.fromFile(apkFile); - int count = newPermissionCount(packageUri); - if (count < 0) { - mCallback.onError(InstallerCallback.OPERATION_INSTALL, - InstallerCallback.ERROR_CODE_CANNOT_PARSE); - return; - } - if (count > 0) { - Intent intent = new Intent(mContext, InstallConfirmActivity.class); - intent.setData(packageUri); - mActivity.startActivityForResult(intent, REQUEST_CONFIRM_PERMS); - } else { - try { - doInstallPackageInternal(packageUri); - } catch (InstallFailedException e) { - mCallback.onError(InstallerCallback.OPERATION_INSTALL, - InstallerCallback.ERROR_CODE_OTHER); - } - } - } + protected void installPackage(final Uri uri, Uri originatingUri, String packageName) { - private int newPermissionCount(Uri packageUri) { - AppDiff appDiff = new AppDiff(mContext.getPackageManager(), packageUri); - if (appDiff.mPkgInfo == null) { - // could not get diff because we couldn't parse the package - return -1; - } - AppSecurityPermissions perms = new AppSecurityPermissions(mContext, appDiff.mPkgInfo); - if (appDiff.mInstalledAppInfo != null) { - // update to an existing app - return perms.getPermissionCount(AppSecurityPermissions.WHICH_NEW); - } - // default: even if there aren't any permissions, we want to make the - // user always confirm installing new apps - return 1; - } - - private void doInstallPackageInternal(final Uri packageURI) throws InstallFailedException { ServiceConnection mServiceConnection = new ServiceConnection() { public void onServiceConnected(ComponentName name, IBinder service) { IPrivilegedService privService = IPrivilegedService.Stub.asInterface(service); @@ -205,17 +159,17 @@ public class PrivilegedInstaller extends Installer { // TODO: propagate other return codes? if (returnCode == INSTALL_SUCCEEDED) { Utils.debugLog(TAG, "Install succeeded"); - mCallback.onSuccess(InstallerCallback.OPERATION_INSTALL); +// mCallback.onSuccess(InstallerCallback.OPERATION_INSTALL); } else { Log.e(TAG, "Install failed with returnCode " + returnCode); - mCallback.onError(InstallerCallback.OPERATION_INSTALL, - InstallerCallback.ERROR_CODE_OTHER); +// mCallback.onError(InstallerCallback.OPERATION_INSTALL, +// InstallerCallback.ERROR_CODE_OTHER); } } }; try { - privService.installPackage(packageURI, INSTALL_REPLACE_EXISTING, null, callback); + privService.installPackage(uri, INSTALL_REPLACE_EXISTING, null, callback); } catch (RemoteException e) { Log.e(TAG, "RemoteException", e); } @@ -232,8 +186,7 @@ public class PrivilegedInstaller extends Installer { } @Override - protected void deletePackageInternal(final String packageName) - throws InstallFailedException { + protected void uninstallPackage(final String packageName) { ApplicationInfo appInfo; try { //noinspection WrongConstant (lint is actually wrong here!) @@ -248,8 +201,8 @@ public class PrivilegedInstaller extends Installer { if (isSystem && !isUpdate) { // Cannot remove system apps unless we're uninstalling updates - mCallback.onError(InstallerCallback.OPERATION_DELETE, - InstallerCallback.ERROR_CODE_OTHER); +// mCallback.onError(InstallerCallback.OPERATION_DELETE, +// InstallerCallback.ERROR_CODE_OTHER); return; } @@ -270,8 +223,8 @@ public class PrivilegedInstaller extends Installer { try { doDeletePackageInternal(packageName); } catch (InstallFailedException e) { - mCallback.onError(InstallerCallback.OPERATION_DELETE, - InstallerCallback.ERROR_CODE_OTHER); +// mCallback.onError(InstallerCallback.OPERATION_DELETE, +// InstallerCallback.ERROR_CODE_OTHER); } } }); @@ -280,8 +233,8 @@ public class PrivilegedInstaller extends Installer { @Override public void onClick(DialogInterface dialog, int which) { dialog.cancel(); - mCallback.onError(InstallerCallback.OPERATION_DELETE, - InstallerCallback.ERROR_CODE_CANCELED); +// mCallback.onError(InstallerCallback.OPERATION_DELETE, +// InstallerCallback.ERROR_CODE_CANCELED); } }); builder.setMessage(messageId); @@ -301,11 +254,11 @@ public class PrivilegedInstaller extends Installer { if (returnCode == DELETE_SUCCEEDED) { Utils.debugLog(TAG, "Delete succeeded"); - mCallback.onSuccess(InstallerCallback.OPERATION_DELETE); +// mCallback.onSuccess(InstallerCallback.OPERATION_DELETE); } else { Log.e(TAG, "Delete failed with returnCode " + returnCode); - mCallback.onError(InstallerCallback.OPERATION_DELETE, - InstallerCallback.ERROR_CODE_OTHER); +// mCallback.onError(InstallerCallback.OPERATION_DELETE, +// InstallerCallback.ERROR_CODE_OTHER); } } }; @@ -327,30 +280,30 @@ public class PrivilegedInstaller extends Installer { Context.BIND_AUTO_CREATE); } - @Override - public boolean handleOnActivityResult(int requestCode, int resultCode, Intent data) { - switch (requestCode) { - case REQUEST_CONFIRM_PERMS: - if (resultCode == Activity.RESULT_OK) { - final Uri packageUri = data.getData(); - try { - doInstallPackageInternal(packageUri); - } catch (InstallFailedException e) { - mCallback.onError(InstallerCallback.OPERATION_INSTALL, - InstallerCallback.ERROR_CODE_OTHER); - } - } else if (resultCode == InstallConfirmActivity.RESULT_CANNOT_PARSE) { - mCallback.onError(InstallerCallback.OPERATION_INSTALL, - InstallerCallback.ERROR_CODE_CANNOT_PARSE); - } else { // Activity.RESULT_CANCELED - mCallback.onError(InstallerCallback.OPERATION_INSTALL, - InstallerCallback.ERROR_CODE_CANCELED); - } - return true; - default: - return false; - } - } +// @Override +// public boolean handleOnActivityResult(int requestCode, int resultCode, Intent data) { +// switch (requestCode) { +// case REQUEST_CONFIRM_PERMS: +// if (resultCode == Activity.RESULT_OK) { +// final Uri packageUri = data.getData(); +// try { +// doInstallPackageInternal(packageUri); +// } catch (InstallFailedException e) { +// mCallback.onError(InstallerCallback.OPERATION_INSTALL, +// InstallerCallback.ERROR_CODE_OTHER); +// } +// } else if (resultCode == InstallConfirmActivity.RESULT_CANNOT_PARSE) { +// mCallback.onError(InstallerCallback.OPERATION_INSTALL, +// InstallerCallback.ERROR_CODE_CANNOT_PARSE); +// } else { // Activity.RESULT_CANCELED +// mCallback.onError(InstallerCallback.OPERATION_INSTALL, +// InstallerCallback.ERROR_CODE_CANCELED); +// } +// return true; +// default: +// return false; +// } +// } public static final int INSTALL_REPLACE_EXISTING = 2; @@ -644,7 +597,7 @@ public class PrivilegedInstaller extends Installer { * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} * if the system failed to install the package because it is attempting to define a * permission that is already defined by some existing package. - * + *

    * The package name of the app which has already defined the permission is passed to * a {@link PackageInstallObserver}, if any, as the {@link #EXTRA_EXISTING_PACKAGE} * string extra; and the name of the permission being redefined is passed in the diff --git a/app/src/main/java/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java b/app/src/main/java/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java index d789ac7d0..695b6ce27 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java +++ b/app/src/main/java/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java @@ -1,6 +1,7 @@ package org.fdroid.fdroid.views.swap; import android.app.Activity; +import android.app.PendingIntent; import android.bluetooth.BluetoothAdapter; import android.content.BroadcastReceiver; import android.content.ComponentName; @@ -37,12 +38,14 @@ import org.fdroid.fdroid.NfcHelper; import org.fdroid.fdroid.Preferences; import org.fdroid.fdroid.R; import org.fdroid.fdroid.Utils; +import org.fdroid.fdroid.compat.PackageManagerCompat; import org.fdroid.fdroid.data.Apk; import org.fdroid.fdroid.data.ApkProvider; import org.fdroid.fdroid.data.App; import org.fdroid.fdroid.data.NewRepoConfig; import org.fdroid.fdroid.installer.InstallManagerService; import org.fdroid.fdroid.installer.Installer; +import org.fdroid.fdroid.installer.InstallerService; import org.fdroid.fdroid.localrepo.LocalRepoManager; import org.fdroid.fdroid.localrepo.SwapService; import org.fdroid.fdroid.localrepo.peers.Peer; @@ -786,25 +789,49 @@ public class SwapWorkflowActivity extends AppCompatActivity { } private void handleDownloadComplete(File apkFile, String packageName, String urlString) { + Uri originatingUri = Uri.parse(urlString); + Uri localUri = Uri.fromFile(apkFile); - try { - Installer.getActivityInstaller(this, new Installer.InstallerCallback() { - @Override - public void onSuccess(int operation) { - // TODO: Don't reload the view weely-neely, but rather get the view to listen - // for broadcasts that say the install was complete. - showRelevantView(true); - } - - @Override - public void onError(int operation, int errorCode) { - // TODO: Boo! - } - }).installPackage(apkFile, packageName, urlString); - localBroadcastManager.unregisterReceiver(downloadCompleteReceiver); - } catch (Installer.InstallFailedException e) { - // TODO: Handle exception properly - } + localBroadcastManager.registerReceiver(installReceiver, + Installer.getInstallIntentFilter(Uri.fromFile(apkFile))); + InstallerService.install(this, localUri, originatingUri, packageName); } + private final BroadcastReceiver installReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + switch (intent.getAction()) { + case Installer.ACTION_INSTALL_STARTED: { + break; + } + case Installer.ACTION_INSTALL_COMPLETE: { + localBroadcastManager.unregisterReceiver(this); + + showRelevantView(true); + break; + } + case Installer.ACTION_INSTALL_INTERRUPTED: { + localBroadcastManager.unregisterReceiver(this); + // TODO: handle errors! + break; + } + case Installer.ACTION_INSTALL_USER_INTERACTION: { + PendingIntent installPendingIntent = + intent.getParcelableExtra(Installer.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!"); + } + } + } + }; + } From bb273cd2f54b9b1f91fa9ca7c99adacef1c865f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Sch=C3=BCrmann?= Date: Mon, 23 May 2016 21:01:40 +0300 Subject: [PATCH 03/21] Use Uris more often instead of File --- .../fdroid/installer/DefaultInstaller.java | 18 +++----- .../fdroid/fdroid/installer/Installer.java | 43 ++++++------------- .../fdroid/installer/InstallerFactory.java | 1 + .../InstallExtensionDialogActivity.java | 5 ++- 4 files changed, 24 insertions(+), 43 deletions(-) diff --git a/app/src/main/java/org/fdroid/fdroid/installer/DefaultInstaller.java b/app/src/main/java/org/fdroid/fdroid/installer/DefaultInstaller.java index a424460dc..dac7c5bc3 100644 --- a/app/src/main/java/org/fdroid/fdroid/installer/DefaultInstaller.java +++ b/app/src/main/java/org/fdroid/fdroid/installer/DefaultInstaller.java @@ -4,6 +4,7 @@ import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.net.Uri; +import android.util.Log; import org.fdroid.fdroid.BuildConfig; import org.fdroid.fdroid.Utils; @@ -21,21 +22,17 @@ public class DefaultInstaller extends Installer { @Override protected void installPackage(Uri uri, Uri originatingUri, String packageName) { - sendBroadcastInstall(uri, originatingUri, Installer.ACTION_INSTALL_STARTED); Utils.debugLog(TAG, "ACTION_INSTALL uri: " + uri + " file: " + new File(uri.getPath())); - // TODO: rework for uri - File sanitizedFile = null; + Uri sanitizedUri; try { - sanitizedFile = Installer.prepareApkFile(mContext, new File(uri.getPath()), packageName); + sanitizedUri = Installer.prepareApkFile(mContext, uri, packageName); } catch (Installer.InstallFailedException e) { - e.printStackTrace(); + Log.e(TAG, "prepareApkFile failed", e); + return; } - Uri sanitizedUri = Uri.fromFile(sanitizedFile); - - Utils.debugLog(TAG, "ACTION_INSTALL sanitizedUri: " + sanitizedUri); Intent installIntent; // special case: F-Droid Privileged Extension @@ -44,14 +41,13 @@ public class DefaultInstaller extends Installer { // 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(mContext); - if (!BuildConfig.DEBUG && !signatureVerifier.hasFDroidSignature(sanitizedFile)) { + if (!BuildConfig.DEBUG && !signatureVerifier.hasFDroidSignature(new File(sanitizedUri.getPath()))) { throw new RuntimeException("APK signature of extension not correct!"); } installIntent = new Intent(mContext, InstallExtensionDialogActivity.class); installIntent.setAction(InstallExtensionDialogActivity.ACTION_INSTALL); - installIntent.putExtra(InstallExtensionDialogActivity.EXTRA_INSTALL_APK, - sanitizedFile.getAbsolutePath()); + installIntent.setData(sanitizedUri); } else { installIntent = new Intent(mContext, AndroidInstallerActivity.class); installIntent.setAction(AndroidInstallerActivity.ACTION_INSTALL_PACKAGE); diff --git a/app/src/main/java/org/fdroid/fdroid/installer/Installer.java b/app/src/main/java/org/fdroid/fdroid/installer/Installer.java index 458cd1f3a..b53d5e06e 100644 --- a/app/src/main/java/org/fdroid/fdroid/installer/Installer.java +++ b/app/src/main/java/org/fdroid/fdroid/installer/Installer.java @@ -37,7 +37,6 @@ import org.fdroid.fdroid.data.ApkProvider; import org.fdroid.fdroid.data.SanitizedFile; import org.fdroid.fdroid.privileged.views.AppDiff; import org.fdroid.fdroid.privileged.views.AppSecurityPermissions; -import org.fdroid.fdroid.privileged.views.InstallConfirmActivity; import java.io.File; import java.io.IOException; @@ -81,44 +80,28 @@ public abstract class Installer { } } - /** - * Callback from Installer. NOTE: This callback can be in a different thread - * than the UI thread - */ -// public interface InstallerCallback { -// -// int OPERATION_INSTALL = 1; -// int OPERATION_DELETE = 2; -// -// // Avoid using [-1,1] as they may conflict with Activity.RESULT_* -// int ERROR_CODE_CANCELED = 2; -// int ERROR_CODE_OTHER = 3; -// int ERROR_CODE_CANNOT_PARSE = 4; -// -// void onSuccess(int operation); -// -// void onError(int operation, int errorCode); -// } - Installer(Context context) { this.mContext = context; this.mPm = context.getPackageManager(); localBroadcastManager = LocalBroadcastManager.getInstance(context); } - public static SanitizedFile prepareApkFile(Context context, File apkFile, String packageName) + public static Uri prepareApkFile(Context context, Uri uri, String packageName) throws InstallFailedException { - SanitizedFile apkToInstall = null; + + File apkFile = new File(uri.getPath()); + + SanitizedFile sanitizedApkFile = null; 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 InstallFailedException(apkFile + " has packageName that clashes with " + packageName); + throw new InstallFailedException(uri + " has packageName that clashes with " + packageName); } if (!attributes.containsKey("versionCode")) { - throw new InstallFailedException(apkFile + " is missing versionCode!"); + throw new InstallFailedException(uri + " is missing versionCode!"); } int versionCode = (Integer) attributes.get("versionCode"); Apk apk = ApkProvider.Helper.find(context, packageName, versionCode, new String[]{ @@ -129,10 +112,10 @@ public abstract class Installer { * 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", + sanitizedApkFile = SanitizedFile.knownSanitized(File.createTempFile("install-", ".apk", context.getFilesDir())); - FileUtils.copyFile(apkFile, apkToInstall); - if (!verifyApkFile(apkToInstall, apk.hash, apk.hashType)) { + FileUtils.copyFile(apkFile, sanitizedApkFile); + if (!verifyApkFile(sanitizedApkFile, apk.hash, apk.hashType)) { FileUtils.deleteQuietly(apkFile); throw new InstallFailedException(apkFile + " failed to verify!"); } @@ -143,7 +126,7 @@ public abstract class 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. - apkToInstall.setReadable(true, false); + sanitizedApkFile.setReadable(true, false); } catch (NumberFormatException | IOException | NoSuchAlgorithmException e) { throw new InstallFailedException(e); @@ -151,7 +134,7 @@ public abstract class Installer { throw new InstallFailedException("F-Droid Privileged can only be updated using an activity!"); } finally { // 20 minutes the start of the install process, delete the file - final File apkToDelete = apkToInstall; + final File apkToDelete = sanitizedApkFile; new Thread() { @Override public void run() { @@ -167,7 +150,7 @@ public abstract class Installer { }.start(); } - return apkToInstall; + return Uri.fromFile(sanitizedApkFile); } public PendingIntent getPermissionScreen(Apk apk) { diff --git a/app/src/main/java/org/fdroid/fdroid/installer/InstallerFactory.java b/app/src/main/java/org/fdroid/fdroid/installer/InstallerFactory.java index 52663b42b..1d6f6fc6e 100644 --- a/app/src/main/java/org/fdroid/fdroid/installer/InstallerFactory.java +++ b/app/src/main/java/org/fdroid/fdroid/installer/InstallerFactory.java @@ -40,6 +40,7 @@ public class InstallerFactory { installer = new PrivilegedInstaller(context); } else { Log.e(TAG, "PrivilegedInstaller is enabled in prefs, but permissions are not granted!"); + // TODO: better error handling? // fallback to default installer installer = new DefaultInstaller(context); diff --git a/app/src/main/java/org/fdroid/fdroid/privileged/install/InstallExtensionDialogActivity.java b/app/src/main/java/org/fdroid/fdroid/privileged/install/InstallExtensionDialogActivity.java index 5305483d2..fcecc45ec 100644 --- a/app/src/main/java/org/fdroid/fdroid/privileged/install/InstallExtensionDialogActivity.java +++ b/app/src/main/java/org/fdroid/fdroid/privileged/install/InstallExtensionDialogActivity.java @@ -43,6 +43,8 @@ import org.fdroid.fdroid.Preferences; import org.fdroid.fdroid.R; import org.fdroid.fdroid.installer.PrivilegedInstaller; +import java.io.File; + import eu.chainfire.libsuperuser.Shell; /** @@ -53,7 +55,6 @@ public class InstallExtensionDialogActivity extends FragmentActivity { private static final String TAG = "InstallIntoSystem"; public static final String ACTION_INSTALL = "install"; - public static final String EXTRA_INSTALL_APK = "apk_file"; public static final String ACTION_UNINSTALL = "uninstall"; public static final String ACTION_POST_INSTALL = "post_install"; @@ -73,7 +74,7 @@ public class InstallExtensionDialogActivity extends FragmentActivity { return; } - apkFile = getIntent().getStringExtra(EXTRA_INSTALL_APK); + apkFile = (new File(getIntent().getData().getPath())).getAbsolutePath(); switch (getIntent().getAction()) { case ACTION_UNINSTALL: From c5f90703708b83016d6fb11b3cb8cd199f24d9d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Sch=C3=BCrmann?= Date: Sat, 28 May 2016 21:23:34 +0300 Subject: [PATCH 04/21] Better error handling in privileged installer --- ...ity.java => DefaultInstallerActivity.java} | 2 +- .../fdroid/installer/PrivilegedInstaller.java | 44 ++++++++++--------- 2 files changed, 25 insertions(+), 21 deletions(-) rename app/src/main/java/org/fdroid/fdroid/installer/{AndroidInstallerActivity.java => DefaultInstallerActivity.java} (99%) diff --git a/app/src/main/java/org/fdroid/fdroid/installer/AndroidInstallerActivity.java b/app/src/main/java/org/fdroid/fdroid/installer/DefaultInstallerActivity.java similarity index 99% rename from app/src/main/java/org/fdroid/fdroid/installer/AndroidInstallerActivity.java rename to app/src/main/java/org/fdroid/fdroid/installer/DefaultInstallerActivity.java index efed31af4..aada32327 100644 --- a/app/src/main/java/org/fdroid/fdroid/installer/AndroidInstallerActivity.java +++ b/app/src/main/java/org/fdroid/fdroid/installer/DefaultInstallerActivity.java @@ -35,7 +35,7 @@ import org.fdroid.fdroid.Utils; /** * A transparent activity as a wrapper around AOSP's PackageInstaller Intents */ -public class AndroidInstallerActivity extends FragmentActivity { +public class DefaultInstallerActivity extends FragmentActivity { public static final String TAG = "AndroidInstallerAct"; public static final String ACTION_INSTALL_PACKAGE = "org.fdroid.fdroid.INSTALL_PACKAGE"; diff --git a/app/src/main/java/org/fdroid/fdroid/installer/PrivilegedInstaller.java b/app/src/main/java/org/fdroid/fdroid/installer/PrivilegedInstaller.java index 14eee9c5e..e1158aa67 100644 --- a/app/src/main/java/org/fdroid/fdroid/installer/PrivilegedInstaller.java +++ b/app/src/main/java/org/fdroid/fdroid/installer/PrivilegedInstaller.java @@ -39,8 +39,6 @@ import org.fdroid.fdroid.Utils; import org.fdroid.fdroid.privileged.IPrivilegedCallback; import org.fdroid.fdroid.privileged.IPrivilegedService; -import java.io.File; - /** * Installer based on using internal hidden APIs of the Android OS, which are * protected by the permissions @@ -133,8 +131,7 @@ public class PrivilegedInstaller extends Installer { synchronized (mutex) { try { mutex.wait(3000); - } catch (InterruptedException e) { - // don't care + } catch (InterruptedException ignored) { } } @@ -147,7 +144,15 @@ public class PrivilegedInstaller extends Installer { @Override - protected void installPackage(final Uri uri, Uri originatingUri, String packageName) { + protected void installPackage(final Uri uri, final Uri originatingUri, String packageName) { + + final Uri sanitizedUri; + try { + sanitizedUri = Installer.prepareApkFile(mContext, uri, packageName); + } catch (Installer.InstallFailedException e) { + Log.e(TAG, "prepareApkFile failed", e); + return; + } ServiceConnection mServiceConnection = new ServiceConnection() { public void onServiceConnected(ComponentName name, IBinder service) { @@ -156,20 +161,20 @@ public class PrivilegedInstaller extends Installer { IPrivilegedCallback callback = new IPrivilegedCallback.Stub() { @Override public void handleResult(String packageName, int returnCode) throws RemoteException { - // TODO: propagate other return codes? + // TODO: propagate other return codes! if (returnCode == INSTALL_SUCCEEDED) { Utils.debugLog(TAG, "Install succeeded"); -// mCallback.onSuccess(InstallerCallback.OPERATION_INSTALL); + sendBroadcastInstall(uri, originatingUri, ACTION_INSTALL_COMPLETE); } else { Log.e(TAG, "Install failed with returnCode " + returnCode); -// mCallback.onError(InstallerCallback.OPERATION_INSTALL, -// InstallerCallback.ERROR_CODE_OTHER); + sendBroadcastInstall(uri, originatingUri, ACTION_INSTALL_INTERRUPTED, + "Install failed with returnCode " + returnCode); } } }; try { - privService.installPackage(uri, INSTALL_REPLACE_EXISTING, null, callback); + privService.installPackage(sanitizedUri, INSTALL_REPLACE_EXISTING, null, callback); } catch (RemoteException e) { Log.e(TAG, "RemoteException", e); } @@ -201,8 +206,8 @@ public class PrivilegedInstaller extends Installer { if (isSystem && !isUpdate) { // Cannot remove system apps unless we're uninstalling updates -// mCallback.onError(InstallerCallback.OPERATION_DELETE, -// InstallerCallback.ERROR_CODE_OTHER); + sendBroadcastUninstall(packageName, ACTION_UNINSTALL_INTERRUPTED, + "Cannot remove system apps unless we're uninstalling updates"); return; } @@ -213,6 +218,7 @@ public class PrivilegedInstaller extends Installer { messageId = R.string.uninstall_confirm; } + // TODO: move this to methods in activity/ Installer with activity context! final AlertDialog.Builder builder = new AlertDialog.Builder(mContext); builder.setTitle(appInfo.loadLabel(mPm)); builder.setIcon(appInfo.loadIcon(mPm)); @@ -223,8 +229,8 @@ public class PrivilegedInstaller extends Installer { try { doDeletePackageInternal(packageName); } catch (InstallFailedException e) { -// mCallback.onError(InstallerCallback.OPERATION_DELETE, -// InstallerCallback.ERROR_CODE_OTHER); + sendBroadcastUninstall(packageName, ACTION_UNINSTALL_INTERRUPTED, + "uninstall failed"); } } }); @@ -233,8 +239,7 @@ public class PrivilegedInstaller extends Installer { @Override public void onClick(DialogInterface dialog, int which) { dialog.cancel(); -// mCallback.onError(InstallerCallback.OPERATION_DELETE, -// InstallerCallback.ERROR_CODE_CANCELED); + sendBroadcastUninstall(packageName, ACTION_UNINSTALL_INTERRUPTED); } }); builder.setMessage(messageId); @@ -253,12 +258,11 @@ public class PrivilegedInstaller extends Installer { // TODO: propagate other return codes? if (returnCode == DELETE_SUCCEEDED) { Utils.debugLog(TAG, "Delete succeeded"); - -// mCallback.onSuccess(InstallerCallback.OPERATION_DELETE); + sendBroadcastUninstall(packageName, ACTION_UNINSTALL_COMPLETE); } else { Log.e(TAG, "Delete failed with returnCode " + returnCode); -// mCallback.onError(InstallerCallback.OPERATION_DELETE, -// InstallerCallback.ERROR_CODE_OTHER); + sendBroadcastUninstall(packageName, ACTION_UNINSTALL_INTERRUPTED, + "Uninstall failed with returnCode " + returnCode); } } }; From de1d310499ecffdcd3e8f7a33c11385e6b7d77f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Sch=C3=BCrmann?= Date: Sat, 28 May 2016 21:23:48 +0300 Subject: [PATCH 05/21] Move installation of extension into own Installer --- app/src/main/AndroidManifest.xml | 2 +- .../fdroid/installer/DefaultInstaller.java | 32 +++++-- .../fdroid/installer/ExtensionInstaller.java | 94 +++++++++++++++++++ .../fdroid/installer/InstallerFactory.java | 9 +- .../fdroid/installer/InstallerService.java | 8 +- 5 files changed, 131 insertions(+), 14 deletions(-) create mode 100644 app/src/main/java/org/fdroid/fdroid/installer/ExtensionInstaller.java diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index c5734f480..6f5d2d622 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -402,7 +402,7 @@ diff --git a/app/src/main/java/org/fdroid/fdroid/installer/DefaultInstaller.java b/app/src/main/java/org/fdroid/fdroid/installer/DefaultInstaller.java index dac7c5bc3..d31d2641b 100644 --- a/app/src/main/java/org/fdroid/fdroid/installer/DefaultInstaller.java +++ b/app/src/main/java/org/fdroid/fdroid/installer/DefaultInstaller.java @@ -1,3 +1,22 @@ +/* + * Copyright (C) 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.app.PendingIntent; @@ -49,9 +68,9 @@ public class DefaultInstaller extends Installer { installIntent.setAction(InstallExtensionDialogActivity.ACTION_INSTALL); installIntent.setData(sanitizedUri); } else { - installIntent = new Intent(mContext, AndroidInstallerActivity.class); - installIntent.setAction(AndroidInstallerActivity.ACTION_INSTALL_PACKAGE); - installIntent.putExtra(AndroidInstallerActivity.EXTRA_ORIGINATING_URI, originatingUri); + installIntent = new Intent(mContext, DefaultInstallerActivity.class); + installIntent.setAction(DefaultInstallerActivity.ACTION_INSTALL_PACKAGE); + installIntent.putExtra(DefaultInstallerActivity.EXTRA_ORIGINATING_URI, originatingUri); installIntent.setData(sanitizedUri); } @@ -63,7 +82,6 @@ public class DefaultInstaller extends Installer { sendBroadcastInstall(uri, originatingUri, Installer.ACTION_INSTALL_USER_INTERACTION, installPendingIntent); - } @Override @@ -76,10 +94,10 @@ public class DefaultInstaller extends Installer { uninstallIntent = new Intent(mContext, InstallExtensionDialogActivity.class); uninstallIntent.setAction(InstallExtensionDialogActivity.ACTION_UNINSTALL); } else { - uninstallIntent = new Intent(mContext, AndroidInstallerActivity.class); - uninstallIntent.setAction(AndroidInstallerActivity.ACTION_UNINSTALL_PACKAGE); + uninstallIntent = new Intent(mContext, DefaultInstallerActivity.class); + uninstallIntent.setAction(DefaultInstallerActivity.ACTION_UNINSTALL_PACKAGE); uninstallIntent.putExtra( - AndroidInstallerActivity.EXTRA_UNINSTALL_PACKAGE_NAME, packageName); + DefaultInstallerActivity.EXTRA_UNINSTALL_PACKAGE_NAME, packageName); } PendingIntent uninstallPendingIntent = PendingIntent.getActivity( mContext.getApplicationContext(), diff --git a/app/src/main/java/org/fdroid/fdroid/installer/ExtensionInstaller.java b/app/src/main/java/org/fdroid/fdroid/installer/ExtensionInstaller.java new file mode 100644 index 000000000..887d18290 --- /dev/null +++ b/app/src/main/java/org/fdroid/fdroid/installer/ExtensionInstaller.java @@ -0,0 +1,94 @@ +/* + * Copyright (C) 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.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.util.Log; + +import org.fdroid.fdroid.BuildConfig; +import org.fdroid.fdroid.privileged.install.InstallExtensionDialogActivity; + +import java.io.File; + +/** + * Special Installer that is only useful to install the Privileged Extension apk + */ +public class ExtensionInstaller extends Installer { + + private static final String TAG = "ExtensionInstaller"; + + ExtensionInstaller(Context context) { + super(context); + } + + @Override + protected void installPackage(Uri uri, Uri originatingUri, String packageName) { + sendBroadcastInstall(uri, originatingUri, Installer.ACTION_INSTALL_STARTED); + + Uri sanitizedUri; + try { + sanitizedUri = Installer.prepareApkFile(mContext, uri, packageName); + } catch (InstallFailedException e) { + Log.e(TAG, "prepareApkFile failed", e); + return; + } + + // 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(mContext); + if (!BuildConfig.DEBUG && !signatureVerifier.hasFDroidSignature(new File(sanitizedUri.getPath()))) { + throw new RuntimeException("APK signature of extension not correct!"); + } + Intent installIntent; + installIntent = new Intent(mContext, InstallExtensionDialogActivity.class); + installIntent.setAction(InstallExtensionDialogActivity.ACTION_INSTALL); + installIntent.setData(sanitizedUri); + + PendingIntent installPendingIntent = PendingIntent.getActivity( + mContext.getApplicationContext(), + uri.hashCode(), + installIntent, + PendingIntent.FLAG_UPDATE_CURRENT); + + sendBroadcastInstall(uri, originatingUri, + Installer.ACTION_INSTALL_USER_INTERACTION, installPendingIntent); + } + + @Override + protected void uninstallPackage(String packageName) { + sendBroadcastUninstall(packageName, Installer.ACTION_UNINSTALL_STARTED); + + Intent uninstallIntent; + uninstallIntent = new Intent(mContext, InstallExtensionDialogActivity.class); + uninstallIntent.setAction(InstallExtensionDialogActivity.ACTION_UNINSTALL); + + PendingIntent uninstallPendingIntent = PendingIntent.getActivity( + mContext.getApplicationContext(), + packageName.hashCode(), + uninstallIntent, + PendingIntent.FLAG_UPDATE_CURRENT); + + sendBroadcastUninstall(packageName, + Installer.ACTION_UNINSTALL_USER_INTERACTION, uninstallPendingIntent); + } +} diff --git a/app/src/main/java/org/fdroid/fdroid/installer/InstallerFactory.java b/app/src/main/java/org/fdroid/fdroid/installer/InstallerFactory.java index 1d6f6fc6e..3f57b26e4 100644 --- a/app/src/main/java/org/fdroid/fdroid/installer/InstallerFactory.java +++ b/app/src/main/java/org/fdroid/fdroid/installer/InstallerFactory.java @@ -30,9 +30,16 @@ public class InstallerFactory { private static final String TAG = "InstallerFactory"; public static Installer create(Context context) { + return create(context, null); + } + + public static Installer create(Context context, String packageName) { Installer installer; - if (isPrivilegedInstallerEnabled()) { + if (packageName != null && packageName.equals(PrivilegedInstaller.PRIVILEGED_EXTENSION_PACKAGE_NAME)) { + // special case: F-Droid Privileged Extension + installer = new ExtensionInstaller(context); + } else if (isPrivilegedInstallerEnabled()) { if (PrivilegedInstaller.isExtensionInstalledCorrectly(context) == PrivilegedInstaller.IS_EXTENSION_INSTALLED_YES) { Utils.debugLog(TAG, "privileged extension correctly installed -> PrivilegedInstaller"); diff --git a/app/src/main/java/org/fdroid/fdroid/installer/InstallerService.java b/app/src/main/java/org/fdroid/fdroid/installer/InstallerService.java index 323356284..5e8fa5652 100644 --- a/app/src/main/java/org/fdroid/fdroid/installer/InstallerService.java +++ b/app/src/main/java/org/fdroid/fdroid/installer/InstallerService.java @@ -71,7 +71,6 @@ public class InstallerService extends Service { serviceLooper = thread.getLooper(); serviceHandler = new ServiceHandler(serviceLooper); localBroadcastManager = LocalBroadcastManager.getInstance(this); - installer = InstallerFactory.create(this); } @Override @@ -118,20 +117,19 @@ public class InstallerService extends Service { } protected void handleIntent(Intent intent) { + String packageName = intent.getStringExtra(Installer.EXTRA_PACKAGE_NAME); + installer = InstallerFactory.create(this, packageName); + switch (intent.getAction()) { case ACTION_INSTALL: { Uri uri = intent.getData(); Uri originatingUri = intent.getParcelableExtra(Installer.EXTRA_ORIGINATING_URI); - String packageName = intent.getStringExtra(Installer.EXTRA_PACKAGE_NAME); installer.installPackage(uri, originatingUri, packageName); break; } case ACTION_UNINSTALL: { - String packageName = - intent.getStringExtra(Installer.EXTRA_PACKAGE_NAME); - installer.uninstallPackage(packageName); break; } From 4e8e14802969b11e8fc3e9e3df8e30bacdbffd26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Sch=C3=BCrmann?= Date: Sat, 28 May 2016 22:21:48 +0300 Subject: [PATCH 06/21] Display install errors as notify/dialog --- .../java/org/fdroid/fdroid/AppDetails.java | 115 +++++------------- .../fdroid/installer/DefaultInstaller.java | 45 ++----- .../installer/DefaultInstallerActivity.java | 11 +- .../fdroid/installer/ExtensionInstaller.java | 19 +-- .../installer/InstallManagerService.java | 45 ++++++- .../fdroid/fdroid/installer/Installer.java | 3 + .../fdroid/installer/PrivilegedInstaller.java | 5 + app/src/main/res/values/strings.xml | 5 +- 8 files changed, 112 insertions(+), 136 deletions(-) diff --git a/app/src/main/java/org/fdroid/fdroid/AppDetails.java b/app/src/main/java/org/fdroid/fdroid/AppDetails.java index d3a16343f..79efbc780 100644 --- a/app/src/main/java/org/fdroid/fdroid/AppDetails.java +++ b/app/src/main/java/org/fdroid/fdroid/AppDetails.java @@ -562,55 +562,28 @@ public class AppDetails extends AppCompatActivity { } case Installer.ACTION_INSTALL_COMPLETE: { headerFragment.removeProgress(); + localBroadcastManager.unregisterReceiver(this); - - PackageManagerCompat.setInstaller(packageManager, app.packageName); - - onAppChanged(); break; } case Installer.ACTION_INSTALL_INTERRUPTED: { headerFragment.removeProgress(); + onAppChanged(); + + String errorMessage = + intent.getStringExtra(Installer.EXTRA_ERROR_MESSAGE); + + if (!TextUtils.isEmpty(errorMessage)) { + Log.e(TAG, "Installer aborted with errorMessage: " + errorMessage); + + AlertDialog.Builder alertBuilder = new AlertDialog.Builder(AppDetails.this); + alertBuilder.setTitle(R.string.install_error_notify_title); + alertBuilder.setMessage(errorMessage); + alertBuilder.setNeutralButton(android.R.string.ok, null); + alertBuilder.create().show(); + } + 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 Installer.ACTION_INSTALL_USER_INTERACTION: { @@ -643,52 +616,28 @@ public class AppDetails extends AppCompatActivity { } case Installer.ACTION_UNINSTALL_COMPLETE: { headerFragment.removeProgress(); - localBroadcastManager.unregisterReceiver(this); - onAppChanged(); + + localBroadcastManager.unregisterReceiver(this); break; } case Installer.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(); -// } -// }); + String errorMessage = + intent.getStringExtra(Installer.EXTRA_ERROR_MESSAGE); + + if (!TextUtils.isEmpty(errorMessage)) { + Log.e(TAG, "Installer aborted with errorMessage: " + errorMessage); + + AlertDialog.Builder alertBuilder = new AlertDialog.Builder(AppDetails.this); + alertBuilder.setTitle(R.string.uninstall_error_notify_title); + alertBuilder.setMessage(errorMessage); + alertBuilder.setNeutralButton(android.R.string.ok, null); + alertBuilder.create().show(); + } + + localBroadcastManager.unregisterReceiver(this); break; } case Installer.ACTION_UNINSTALL_USER_INTERACTION: { diff --git a/app/src/main/java/org/fdroid/fdroid/installer/DefaultInstaller.java b/app/src/main/java/org/fdroid/fdroid/installer/DefaultInstaller.java index d31d2641b..d43f6d040 100644 --- a/app/src/main/java/org/fdroid/fdroid/installer/DefaultInstaller.java +++ b/app/src/main/java/org/fdroid/fdroid/installer/DefaultInstaller.java @@ -25,9 +25,7 @@ import android.content.Intent; import android.net.Uri; import android.util.Log; -import org.fdroid.fdroid.BuildConfig; import org.fdroid.fdroid.Utils; -import org.fdroid.fdroid.privileged.install.InstallExtensionDialogActivity; import java.io.File; @@ -43,36 +41,22 @@ public class DefaultInstaller extends Installer { protected void installPackage(Uri uri, Uri originatingUri, String packageName) { sendBroadcastInstall(uri, originatingUri, Installer.ACTION_INSTALL_STARTED); - Utils.debugLog(TAG, "ACTION_INSTALL uri: " + uri + " file: " + new File(uri.getPath())); + Utils.debugLog(TAG, "DefaultInstaller uri: " + uri + " file: " + new File(uri.getPath())); Uri sanitizedUri; try { sanitizedUri = Installer.prepareApkFile(mContext, uri, packageName); } catch (Installer.InstallFailedException e) { Log.e(TAG, "prepareApkFile failed", e); + sendBroadcastInstall(uri, originatingUri, Installer.ACTION_INSTALL_INTERRUPTED, + e.getMessage()); return; } - Intent installIntent; - // 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(mContext); - if (!BuildConfig.DEBUG && !signatureVerifier.hasFDroidSignature(new File(sanitizedUri.getPath()))) { - throw new RuntimeException("APK signature of extension not correct!"); - } - - installIntent = new Intent(mContext, InstallExtensionDialogActivity.class); - installIntent.setAction(InstallExtensionDialogActivity.ACTION_INSTALL); - installIntent.setData(sanitizedUri); - } else { - installIntent = new Intent(mContext, DefaultInstallerActivity.class); - installIntent.setAction(DefaultInstallerActivity.ACTION_INSTALL_PACKAGE); - installIntent.putExtra(DefaultInstallerActivity.EXTRA_ORIGINATING_URI, originatingUri); - installIntent.setData(sanitizedUri); - } + Intent installIntent = new Intent(mContext, DefaultInstallerActivity.class); + installIntent.setAction(DefaultInstallerActivity.ACTION_INSTALL_PACKAGE); + installIntent.putExtra(DefaultInstallerActivity.EXTRA_ORIGINATING_URI, originatingUri); + installIntent.setData(sanitizedUri); PendingIntent installPendingIntent = PendingIntent.getActivity( mContext.getApplicationContext(), @@ -88,17 +72,10 @@ public class DefaultInstaller extends Installer { protected void uninstallPackage(String packageName) { sendBroadcastUninstall(packageName, Installer.ACTION_UNINSTALL_STARTED); - Intent uninstallIntent; - // special case: F-Droid Privileged Extension - if (packageName != null && packageName.equals(PrivilegedInstaller.PRIVILEGED_EXTENSION_PACKAGE_NAME)) { - uninstallIntent = new Intent(mContext, InstallExtensionDialogActivity.class); - uninstallIntent.setAction(InstallExtensionDialogActivity.ACTION_UNINSTALL); - } else { - uninstallIntent = new Intent(mContext, DefaultInstallerActivity.class); - uninstallIntent.setAction(DefaultInstallerActivity.ACTION_UNINSTALL_PACKAGE); - uninstallIntent.putExtra( - DefaultInstallerActivity.EXTRA_UNINSTALL_PACKAGE_NAME, packageName); - } + Intent uninstallIntent = new Intent(mContext, DefaultInstallerActivity.class); + uninstallIntent.setAction(DefaultInstallerActivity.ACTION_UNINSTALL_PACKAGE); + uninstallIntent.putExtra( + DefaultInstallerActivity.EXTRA_UNINSTALL_PACKAGE_NAME, packageName); PendingIntent uninstallPendingIntent = PendingIntent.getActivity( mContext.getApplicationContext(), packageName.hashCode(), diff --git a/app/src/main/java/org/fdroid/fdroid/installer/DefaultInstallerActivity.java b/app/src/main/java/org/fdroid/fdroid/installer/DefaultInstallerActivity.java index aada32327..eb5e1ccb5 100644 --- a/app/src/main/java/org/fdroid/fdroid/installer/DefaultInstallerActivity.java +++ b/app/src/main/java/org/fdroid/fdroid/installer/DefaultInstallerActivity.java @@ -30,6 +30,7 @@ import android.os.Bundle; import android.support.v4.app.FragmentActivity; import android.util.Log; +import org.fdroid.fdroid.R; import org.fdroid.fdroid.Utils; /** @@ -207,9 +208,10 @@ public class DefaultInstallerActivity extends FragmentActivity { } default: case Activity.RESULT_FIRST_USER: { - // AOSP actually returns Activity.RESULT_FIRST_USER if something breaks + // AOSP returns Activity.RESULT_FIRST_USER on error installer.sendBroadcastInstall(mInstallUri, mInstallOriginatingUri, - Installer.ACTION_INSTALL_INTERRUPTED, "error"); + Installer.ACTION_INSTALL_INTERRUPTED, + getString(R.string.install_error_unknown)); break; } } @@ -237,11 +239,10 @@ public class DefaultInstallerActivity extends FragmentActivity { } default: case Activity.RESULT_FIRST_USER: { - // AOSP UninstallAppProgress actually returns - // Activity.RESULT_FIRST_USER if something breaks + // AOSP UninstallAppProgress returns RESULT_FIRST_USER on error installer.sendBroadcastUninstall(mUninstallPackageName, Installer.ACTION_UNINSTALL_INTERRUPTED, - "error"); + getString(R.string.uninstall_error_unknown)); break; } } diff --git a/app/src/main/java/org/fdroid/fdroid/installer/ExtensionInstaller.java b/app/src/main/java/org/fdroid/fdroid/installer/ExtensionInstaller.java index 887d18290..c778a6897 100644 --- a/app/src/main/java/org/fdroid/fdroid/installer/ExtensionInstaller.java +++ b/app/src/main/java/org/fdroid/fdroid/installer/ExtensionInstaller.java @@ -43,13 +43,13 @@ public class ExtensionInstaller extends Installer { @Override protected void installPackage(Uri uri, Uri originatingUri, String packageName) { - sendBroadcastInstall(uri, originatingUri, Installer.ACTION_INSTALL_STARTED); - Uri sanitizedUri; try { sanitizedUri = Installer.prepareApkFile(mContext, uri, packageName); } catch (InstallFailedException e) { Log.e(TAG, "prepareApkFile failed", e); + sendBroadcastInstall(uri, originatingUri, Installer.ACTION_INSTALL_INTERRUPTED, + e.getMessage()); return; } @@ -57,10 +57,10 @@ public class ExtensionInstaller extends Installer { // NOTE: Disabled for debug builds to be able to use official extension from repo ApkSignatureVerifier signatureVerifier = new ApkSignatureVerifier(mContext); if (!BuildConfig.DEBUG && !signatureVerifier.hasFDroidSignature(new File(sanitizedUri.getPath()))) { - throw new RuntimeException("APK signature of extension not correct!"); + sendBroadcastInstall(uri, originatingUri, Installer.ACTION_INSTALL_INTERRUPTED, + "APK signature of extension not correct!"); } - Intent installIntent; - installIntent = new Intent(mContext, InstallExtensionDialogActivity.class); + Intent installIntent = new Intent(mContext, InstallExtensionDialogActivity.class); installIntent.setAction(InstallExtensionDialogActivity.ACTION_INSTALL); installIntent.setData(sanitizedUri); @@ -72,14 +72,16 @@ public class ExtensionInstaller extends Installer { sendBroadcastInstall(uri, originatingUri, Installer.ACTION_INSTALL_USER_INTERACTION, installPendingIntent); + + // don't use broadcasts for the rest of this special installer + sendBroadcastInstall(uri, originatingUri, Installer.ACTION_INSTALL_COMPLETE); } @Override protected void uninstallPackage(String packageName) { sendBroadcastUninstall(packageName, Installer.ACTION_UNINSTALL_STARTED); - Intent uninstallIntent; - uninstallIntent = new Intent(mContext, InstallExtensionDialogActivity.class); + Intent uninstallIntent = new Intent(mContext, InstallExtensionDialogActivity.class); uninstallIntent.setAction(InstallExtensionDialogActivity.ACTION_UNINSTALL); PendingIntent uninstallPendingIntent = PendingIntent.getActivity( @@ -90,5 +92,8 @@ public class ExtensionInstaller extends Installer { sendBroadcastUninstall(packageName, Installer.ACTION_UNINSTALL_USER_INTERACTION, uninstallPendingIntent); + + // don't use broadcasts for the rest of this special installer + sendBroadcastUninstall(packageName, Installer.ACTION_UNINSTALL_COMPLETE); } } 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 2630c6081..8dcaa8001 100644 --- a/app/src/main/java/org/fdroid/fdroid/installer/InstallManagerService.java +++ b/app/src/main/java/org/fdroid/fdroid/installer/InstallManagerService.java @@ -19,6 +19,7 @@ import android.text.TextUtils; import org.fdroid.fdroid.AppDetails; import org.fdroid.fdroid.R; import org.fdroid.fdroid.Utils; +import org.fdroid.fdroid.compat.PackageManagerCompat; import org.fdroid.fdroid.data.Apk; import org.fdroid.fdroid.data.App; import org.fdroid.fdroid.net.Downloader; @@ -274,15 +275,26 @@ public class InstallManagerService extends Service { Uri originatingUri = intent.getParcelableExtra(Installer.EXTRA_ORIGINATING_URI); String urlString = originatingUri.toString(); - removeFromActive(urlString); + Apk apk = removeFromActive(urlString); + + PackageManagerCompat.setInstaller(getPackageManager(), apk.packageName); localBroadcastManager.unregisterReceiver(this); - break; } case Installer.ACTION_INSTALL_INTERRUPTED: { - localBroadcastManager.unregisterReceiver(this); + Uri originatingUri = + intent.getParcelableExtra(Installer.EXTRA_ORIGINATING_URI); + String urlString = originatingUri.toString(); + String errorMessage = + intent.getStringExtra(Installer.EXTRA_ERROR_MESSAGE); + if (!TextUtils.isEmpty(errorMessage)) { + App app = getAppFromActive(urlString); + notifyError(app, urlString, errorMessage, false); + } + + localBroadcastManager.unregisterReceiver(this); break; } case Installer.ACTION_INSTALL_USER_INTERACTION: { @@ -292,7 +304,7 @@ public class InstallManagerService extends Service { intent.getParcelableExtra(Installer.EXTRA_USER_INTERACTION_PI); Utils.debugLog(TAG, "originatingUri: " + originatingUri); - Apk apk = getFromActive(originatingUri.toString()); + Apk apk = getApkFromActive(originatingUri.toString()); // show notification if app details is not visible if (AppDetails.isAppVisible(apk.packageName)) { cancelNotification(originatingUri.toString()); @@ -379,6 +391,25 @@ public class InstallManagerService extends Service { notificationManager.notify(downloadUrlId, notification); } + private void notifyError(App app, String urlString, String text, boolean uninstall) { + String title; + if (uninstall) { + title = String.format(getString(R.string.uninstall_error_notify_title), app.name); + } else { + title = String.format(getString(R.string.install_error_notify_title), app.name); + } + + int downloadUrlId = urlString.hashCode(); + NotificationCompat.Builder builder = + new NotificationCompat.Builder(this) + .setAutoCancel(true) + .setContentTitle(title) + .setSmallIcon(R.drawable.ic_issues) + .setContentText(text); + NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); + nm.notify(downloadUrlId, builder.build()); + } + /** * Cancel the {@link Notification} tied to {@code urlString}, which is the * unique ID used to represent a given APK file. {@link String#hashCode()} @@ -393,7 +424,7 @@ public class InstallManagerService extends Service { ACTIVE_APPS.put(app.packageName, app); } - private static Apk getFromActive(String urlString) { + private static Apk getApkFromActive(String urlString) { return ACTIVE_APKS.get(urlString); } @@ -404,6 +435,10 @@ public class InstallManagerService extends Service { * {@link BroadcastReceiver}s, in which case {@code urlString} would not * find anything in the active maps. */ + private static App getAppFromActive(String urlString) { + return ACTIVE_APPS.get(getApkFromActive(urlString).packageName); + } + private static Apk removeFromActive(String urlString) { Apk apk = ACTIVE_APKS.remove(urlString); if (apk != null) { diff --git a/app/src/main/java/org/fdroid/fdroid/installer/Installer.java b/app/src/main/java/org/fdroid/fdroid/installer/Installer.java index b53d5e06e..3663ecd5c 100644 --- a/app/src/main/java/org/fdroid/fdroid/installer/Installer.java +++ b/app/src/main/java/org/fdroid/fdroid/installer/Installer.java @@ -160,6 +160,9 @@ public abstract class Installer { // if (count < 0) { // mCallback.onError(InstallerCallback.OPERATION_INSTALL, // InstallerCallback.ERROR_CODE_CANNOT_PARSE); + +// install_error_cannot_parse + // return; // } // if (count > 0) { diff --git a/app/src/main/java/org/fdroid/fdroid/installer/PrivilegedInstaller.java b/app/src/main/java/org/fdroid/fdroid/installer/PrivilegedInstaller.java index e1158aa67..4307b98a4 100644 --- a/app/src/main/java/org/fdroid/fdroid/installer/PrivilegedInstaller.java +++ b/app/src/main/java/org/fdroid/fdroid/installer/PrivilegedInstaller.java @@ -151,6 +151,8 @@ public class PrivilegedInstaller extends Installer { sanitizedUri = Installer.prepareApkFile(mContext, uri, packageName); } catch (Installer.InstallFailedException e) { Log.e(TAG, "prepareApkFile failed", e); + sendBroadcastInstall(uri, originatingUri, Installer.ACTION_INSTALL_INTERRUPTED, + e.getMessage()); return; } @@ -299,6 +301,9 @@ public class PrivilegedInstaller extends Installer { // } else if (resultCode == InstallConfirmActivity.RESULT_CANNOT_PARSE) { // mCallback.onError(InstallerCallback.OPERATION_INSTALL, // InstallerCallback.ERROR_CODE_CANNOT_PARSE); + +// install_error_cannot_parse + // } else { // Activity.RESULT_CANCELED // mCallback.onError(InstallerCallback.OPERATION_INSTALL, // InstallerCallback.ERROR_CODE_CANCELED); diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d07727e72..a9b4acd8e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -267,10 +267,8 @@ Requesting root access… Root access denied Either your Android device is not rooted or you have denied root access for F-Droid. - Install error Failed to install due to an unknown error An error occurred while parsing the package - Uninstall error Failed to uninstall due to an unknown error F-Droid Privileged Extension is not available This option is only available when F-Droid Privileged Extension is installed. @@ -365,6 +363,9 @@ Download completed, tap to install Download unsuccessful Waiting to start download… + Error installing %s + Error uninstalling %s + New: Provided by %1$s. From 592cd0424a02b871822f3d1648d0e524cae04a6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Sch=C3=BCrmann?= Date: Sun, 29 May 2016 14:08:36 +0300 Subject: [PATCH 07/21] Better error handling for priv installer --- .../java/org/fdroid/fdroid/AppDetails.java | 6 +- .../fdroid/installer/PrivilegedInstaller.java | 571 ++++++------------ 2 files changed, 203 insertions(+), 374 deletions(-) diff --git a/app/src/main/java/org/fdroid/fdroid/AppDetails.java b/app/src/main/java/org/fdroid/fdroid/AppDetails.java index 79efbc780..1595b40a8 100644 --- a/app/src/main/java/org/fdroid/fdroid/AppDetails.java +++ b/app/src/main/java/org/fdroid/fdroid/AppDetails.java @@ -576,8 +576,12 @@ public class AppDetails extends AppCompatActivity { if (!TextUtils.isEmpty(errorMessage)) { Log.e(TAG, "Installer aborted with errorMessage: " + errorMessage); + String title = String.format( + getString(R.string.install_error_notify_title), + app.name); + AlertDialog.Builder alertBuilder = new AlertDialog.Builder(AppDetails.this); - alertBuilder.setTitle(R.string.install_error_notify_title); + alertBuilder.setTitle(title); alertBuilder.setMessage(errorMessage); alertBuilder.setNeutralButton(android.R.string.ok, null); alertBuilder.create().show(); diff --git a/app/src/main/java/org/fdroid/fdroid/installer/PrivilegedInstaller.java b/app/src/main/java/org/fdroid/fdroid/installer/PrivilegedInstaller.java index 4307b98a4..8c3dfcdb4 100644 --- a/app/src/main/java/org/fdroid/fdroid/installer/PrivilegedInstaller.java +++ b/app/src/main/java/org/fdroid/fdroid/installer/PrivilegedInstaller.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2014 Dominik Schürmann + * Copyright (C) 2014-2016 Dominik Schürmann * Copyright (C) 2015 Daniel Martí * * This program is free software; you can redistribute it and/or @@ -39,6 +39,8 @@ import org.fdroid.fdroid.Utils; import org.fdroid.fdroid.privileged.IPrivilegedCallback; import org.fdroid.fdroid.privileged.IPrivilegedService; +import java.util.HashMap; + /** * Installer based on using internal hidden APIs of the Android OS, which are * protected by the permissions @@ -48,14 +50,8 @@ import org.fdroid.fdroid.privileged.IPrivilegedService; * *

    * Both permissions are protected by systemOrSignature (in newer versions: - * system|signature) and only granted on F-Droid's install in the following - * cases: - *

      - *
    • On all Android versions if F-Droid is pre-deployed as a - * system-application with the Rom
    • - *
    • On Android < 4.4 also when moved into /system/app/
    • - *
    • On Android >= 4.4 also when moved into /system/priv-app/
    • - *
    + * system|signature). Thus, this installer works only when the "F-Droid Privileged + * Extension" is installed into the system. *

    * Sources for Android 4.4 change: * https://groups.google.com/forum/#!msg/android- @@ -75,6 +71,189 @@ public class PrivilegedInstaller extends Installer { public static final int IS_EXTENSION_INSTALLED_SIGNATURE_PROBLEM = 2; public static final int IS_EXTENSION_INSTALLED_PERMISSIONS_PROBLEM = 3; + // From AOSP source code + public static final int ACTION_INSTALL_REPLACE_EXISTING = 2; + + /** + * Following return codes are copied from AOSP 5.1 source code + */ + public static final int INSTALL_SUCCEEDED = 1; + public static final int INSTALL_FAILED_ALREADY_EXISTS = -1; + public static final int INSTALL_FAILED_INVALID_APK = -2; + public static final int INSTALL_FAILED_INVALID_URI = -3; + public static final int INSTALL_FAILED_INSUFFICIENT_STORAGE = -4; + public static final int INSTALL_FAILED_DUPLICATE_PACKAGE = -5; + public static final int INSTALL_FAILED_NO_SHARED_USER = -6; + public static final int INSTALL_FAILED_UPDATE_INCOMPATIBLE = -7; + public static final int INSTALL_FAILED_SHARED_USER_INCOMPATIBLE = -8; + public static final int INSTALL_FAILED_MISSING_SHARED_LIBRARY = -9; + public static final int INSTALL_FAILED_REPLACE_COULDNT_DELETE = -10; + public static final int INSTALL_FAILED_DEXOPT = -11; + public static final int INSTALL_FAILED_OLDER_SDK = -12; + public static final int INSTALL_FAILED_CONFLICTING_PROVIDER = -13; + public static final int INSTALL_FAILED_NEWER_SDK = -14; + public static final int INSTALL_FAILED_TEST_ONLY = -15; + public static final int INSTALL_FAILED_CPU_ABI_INCOMPATIBLE = -16; + public static final int INSTALL_FAILED_MISSING_FEATURE = -17; + public static final int INSTALL_FAILED_CONTAINER_ERROR = -18; + public static final int INSTALL_FAILED_INVALID_INSTALL_LOCATION = -19; + public static final int INSTALL_FAILED_MEDIA_UNAVAILABLE = -20; + public static final int INSTALL_FAILED_VERIFICATION_TIMEOUT = -21; + public static final int INSTALL_FAILED_VERIFICATION_FAILURE = -22; + public static final int INSTALL_FAILED_PACKAGE_CHANGED = -23; + public static final int INSTALL_FAILED_UID_CHANGED = -24; + public static final int INSTALL_FAILED_VERSION_DOWNGRADE = -25; + public static final int INSTALL_PARSE_FAILED_NOT_APK = -100; + public static final int INSTALL_PARSE_FAILED_BAD_MANIFEST = -101; + public static final int INSTALL_PARSE_FAILED_UNEXPECTED_EXCEPTION = -102; + public static final int INSTALL_PARSE_FAILED_NO_CERTIFICATES = -103; + public static final int INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES = -104; + public static final int INSTALL_PARSE_FAILED_CERTIFICATE_ENCODING = -105; + public static final int INSTALL_PARSE_FAILED_BAD_PACKAGE_NAME = -106; + public static final int INSTALL_PARSE_FAILED_BAD_SHARED_USER_ID = -107; + public static final int INSTALL_PARSE_FAILED_MANIFEST_MALFORMED = -108; + public static final int INSTALL_PARSE_FAILED_MANIFEST_EMPTY = -109; + public static final int INSTALL_FAILED_INTERNAL_ERROR = -110; + public static final int INSTALL_FAILED_USER_RESTRICTED = -111; + public static final int INSTALL_FAILED_DUPLICATE_PERMISSION = -112; + public static final int INSTALL_FAILED_NO_MATCHING_ABIS = -113; + /** + * Internal return code for NativeLibraryHelper methods to indicate that the package + * being processed did not contain any native code. This is placed here only so that + * it can belong to the same value space as the other install failure codes. + */ + public static final int NO_NATIVE_LIBRARIES = -114; + public static final int INSTALL_FAILED_ABORTED = -115; + + private static final HashMap sInstallReturnCodes; + + static { + // Descriptions extracted from the source code comments in AOSP + sInstallReturnCodes = new HashMap<>(); + sInstallReturnCodes.put(INSTALL_SUCCEEDED, + "Success"); + sInstallReturnCodes.put(INSTALL_FAILED_ALREADY_EXISTS, + "Package is already installed."); + sInstallReturnCodes.put(INSTALL_FAILED_INVALID_APK, + "The package archive file is invalid."); + sInstallReturnCodes.put(INSTALL_FAILED_INVALID_URI, + "The URI passed in is invalid."); + sInstallReturnCodes.put(INSTALL_FAILED_INSUFFICIENT_STORAGE, + "The package manager service found that the device didn't have enough " + + "storage space to install the app."); + sInstallReturnCodes.put(INSTALL_FAILED_DUPLICATE_PACKAGE, + "A package is already installed with the same name."); + sInstallReturnCodes.put(INSTALL_FAILED_NO_SHARED_USER, + "The requested shared user does not exist."); + sInstallReturnCodes.put(INSTALL_FAILED_UPDATE_INCOMPATIBLE, + "A previously installed package of the same name has a different signature than " + + "the new package (and the old package's data was not removed)."); + sInstallReturnCodes.put(INSTALL_FAILED_SHARED_USER_INCOMPATIBLE, + "The new package is requested a shared user which is already installed on " + + "the device and does not have matching signature."); + sInstallReturnCodes.put(INSTALL_FAILED_MISSING_SHARED_LIBRARY, + "The new package uses a shared library that is not available."); + sInstallReturnCodes.put(INSTALL_FAILED_REPLACE_COULDNT_DELETE, + "Unknown"); // wrong comment in source + sInstallReturnCodes.put(INSTALL_FAILED_DEXOPT, + "The package failed while optimizing and validating its dex files, either " + + "because there was not enough storage or the validation failed."); + sInstallReturnCodes.put(INSTALL_FAILED_OLDER_SDK, + "The new package failed because the current SDK version is older than that " + + "required by the package."); + sInstallReturnCodes.put(INSTALL_FAILED_CONFLICTING_PROVIDER, + "The new package failed because it contains a content provider with the same " + + "authority as a provider already installed in the system."); + sInstallReturnCodes.put(INSTALL_FAILED_NEWER_SDK, + "The new package failed because the current SDK version is newer than that " + + "required by the package."); + sInstallReturnCodes.put(INSTALL_FAILED_TEST_ONLY, + "The new package failed because it has specified that it is a test-only package " + + "and the caller has not supplied the {@link #INSTALL_ALLOW_TEST} flag."); + sInstallReturnCodes.put(INSTALL_FAILED_CPU_ABI_INCOMPATIBLE, + "The package being installed contains native code, but none that is compatible " + + "with the device's CPU_ABI."); + sInstallReturnCodes.put(INSTALL_FAILED_MISSING_FEATURE, + "The new package uses a feature that is not available."); + sInstallReturnCodes.put(INSTALL_FAILED_CONTAINER_ERROR, + "A secure container mount point couldn't be accessed on external media."); + sInstallReturnCodes.put(INSTALL_FAILED_INVALID_INSTALL_LOCATION, + "The new package couldn't be installed in the specified install location."); + sInstallReturnCodes.put(INSTALL_FAILED_MEDIA_UNAVAILABLE, + "The new package couldn't be installed in the specified install location " + + "because the media is not available."); + sInstallReturnCodes.put(INSTALL_FAILED_VERIFICATION_TIMEOUT, + "The new package couldn't be installed because the verification timed out."); + sInstallReturnCodes.put(INSTALL_FAILED_VERIFICATION_FAILURE, + "The new package couldn't be installed because the verification did not succeed."); + sInstallReturnCodes.put(INSTALL_FAILED_PACKAGE_CHANGED, + "The package changed from what the calling program expected."); + sInstallReturnCodes.put(INSTALL_FAILED_UID_CHANGED, + "The new package is assigned a different UID than it previously held."); + sInstallReturnCodes.put(INSTALL_FAILED_VERSION_DOWNGRADE, + "The new package has an older version code than the currently installed package."); + sInstallReturnCodes.put(INSTALL_PARSE_FAILED_NOT_APK, + "The parser was given a path that is not a file, or does not end with the " + + "expected '.apk' extension."); + sInstallReturnCodes.put(INSTALL_PARSE_FAILED_BAD_MANIFEST, + "the parser was unable to retrieve the AndroidManifest.xml file."); + sInstallReturnCodes.put(INSTALL_PARSE_FAILED_UNEXPECTED_EXCEPTION, + "The parser encountered an unexpected exception."); + sInstallReturnCodes.put(INSTALL_PARSE_FAILED_NO_CERTIFICATES, + "The parser did not find any certificates in the .apk."); + sInstallReturnCodes.put(INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES, + "The parser found inconsistent certificates on the files in the .apk."); + sInstallReturnCodes.put(INSTALL_PARSE_FAILED_CERTIFICATE_ENCODING, + "The parser encountered a CertificateEncodingException in one of the files in " + + "the .apk."); + sInstallReturnCodes.put(INSTALL_PARSE_FAILED_BAD_PACKAGE_NAME, + "The parser encountered a bad or missing package name in the manifest."); + sInstallReturnCodes.put(INSTALL_PARSE_FAILED_BAD_SHARED_USER_ID, + "The parser encountered a bad shared user id name in the manifest."); + sInstallReturnCodes.put(INSTALL_PARSE_FAILED_MANIFEST_MALFORMED, + "The parser encountered some structural problem in the manifest."); + sInstallReturnCodes.put(INSTALL_PARSE_FAILED_MANIFEST_EMPTY, + "The parser did not find any actionable tags (instrumentation or application) " + + "in the manifest."); + sInstallReturnCodes.put(INSTALL_FAILED_INTERNAL_ERROR, + "The system failed to install the package because of system issues."); + sInstallReturnCodes.put(INSTALL_FAILED_USER_RESTRICTED, + "The system failed to install the package because the user is restricted from " + + "installing apps."); + sInstallReturnCodes.put(INSTALL_FAILED_DUPLICATE_PERMISSION, + "The system failed to install the package because it is attempting to define a " + + "permission that is already defined by some existing package."); + sInstallReturnCodes.put(INSTALL_FAILED_NO_MATCHING_ABIS, + "The system failed to install the package because its packaged native code did " + + "not match any of the ABIs supported by the system."); + } + + public static final int DELETE_SUCCEEDED = 1; + public static final int DELETE_FAILED_INTERNAL_ERROR = -1; + public static final int DELETE_FAILED_DEVICE_POLICY_MANAGER = -2; + public static final int DELETE_FAILED_USER_RESTRICTED = -3; + public static final int DELETE_FAILED_OWNER_BLOCKED = -4; + public static final int DELETE_FAILED_ABORTED = -5; + + private static final HashMap sUninstallReturnCodes; + + static { + // Descriptions extrgacted from the source code comments in AOSP + sUninstallReturnCodes = new HashMap<>(); + sUninstallReturnCodes.put(DELETE_SUCCEEDED, + "Success"); + sUninstallReturnCodes.put(DELETE_FAILED_INTERNAL_ERROR, + " the system failed to delete the package for an unspecified reason."); + sUninstallReturnCodes.put(DELETE_FAILED_DEVICE_POLICY_MANAGER, + "the system failed to delete the package because it is the active " + + "DevicePolicy manager."); + sUninstallReturnCodes.put(DELETE_FAILED_USER_RESTRICTED, + "the system failed to delete the package since the user is restricted."); + sUninstallReturnCodes.put(DELETE_FAILED_OWNER_BLOCKED, + "the system failed to delete the package because a profile or " + + "device owner has marked the package as uninstallable."); + } + public PrivilegedInstaller(Context context) { super(context); } @@ -163,22 +342,23 @@ public class PrivilegedInstaller extends Installer { IPrivilegedCallback callback = new IPrivilegedCallback.Stub() { @Override public void handleResult(String packageName, int returnCode) throws RemoteException { - // TODO: propagate other return codes! if (returnCode == INSTALL_SUCCEEDED) { - Utils.debugLog(TAG, "Install succeeded"); sendBroadcastInstall(uri, originatingUri, ACTION_INSTALL_COMPLETE); } else { - Log.e(TAG, "Install failed with returnCode " + returnCode); sendBroadcastInstall(uri, originatingUri, ACTION_INSTALL_INTERRUPTED, - "Install failed with returnCode " + returnCode); + "Error " + returnCode + ": " + + sInstallReturnCodes.get(returnCode)); } } }; try { - privService.installPackage(sanitizedUri, INSTALL_REPLACE_EXISTING, null, callback); + privService.installPackage(sanitizedUri, ACTION_INSTALL_REPLACE_EXISTING, + null, callback); } catch (RemoteException e) { Log.e(TAG, "RemoteException", e); + sendBroadcastInstall(uri, originatingUri, ACTION_INSTALL_INTERRUPTED, + "connecting to privileged service failed"); } } @@ -257,14 +437,12 @@ public class PrivilegedInstaller extends Installer { IPrivilegedCallback callback = new IPrivilegedCallback.Stub() { @Override public void handleResult(String packageName, int returnCode) throws RemoteException { - // TODO: propagate other return codes? if (returnCode == DELETE_SUCCEEDED) { - Utils.debugLog(TAG, "Delete succeeded"); sendBroadcastUninstall(packageName, ACTION_UNINSTALL_COMPLETE); } else { - Log.e(TAG, "Delete failed with returnCode " + returnCode); sendBroadcastUninstall(packageName, ACTION_UNINSTALL_INTERRUPTED, - "Uninstall failed with returnCode " + returnCode); + "Error " + returnCode + ": " + + sUninstallReturnCodes.get(returnCode)); } } }; @@ -273,6 +451,8 @@ public class PrivilegedInstaller extends Installer { privService.deletePackage(packageName, 0, callback); } catch (RemoteException e) { Log.e(TAG, "RemoteException", e); + sendBroadcastUninstall(packageName, ACTION_UNINSTALL_INTERRUPTED, + "connecting to privileged service failed"); } } @@ -314,360 +494,5 @@ public class PrivilegedInstaller extends Installer { // } // } - public static final int INSTALL_REPLACE_EXISTING = 2; - - /** - * Following return codes are copied from Android 5.1 source code - */ - - /** - * Installation return code: this is passed to the {@link IPackageInstallObserver} by - * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} on success. - */ - public static final int INSTALL_SUCCEEDED = 1; - - /** - * Installation return code: this is passed to the {@link IPackageInstallObserver} by - * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if the package is - * already installed. - */ - public static final int INSTALL_FAILED_ALREADY_EXISTS = -1; - - /** - * Installation return code: this is passed to the {@link IPackageInstallObserver} by - * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if the package archive - * file is invalid. - */ - public static final int INSTALL_FAILED_INVALID_APK = -2; - - /** - * Installation return code: this is passed to the {@link IPackageInstallObserver} by - * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if the URI passed in - * is invalid. - */ - public static final int INSTALL_FAILED_INVALID_URI = -3; - - /** - * Installation return code: this is passed to the {@link IPackageInstallObserver} by - * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if the package manager - * service found that the device didn't have enough storage space to install the app. - */ - public static final int INSTALL_FAILED_INSUFFICIENT_STORAGE = -4; - - /** - * Installation return code: this is passed to the {@link IPackageInstallObserver} by - * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if a - * package is already installed with the same name. - */ - public static final int INSTALL_FAILED_DUPLICATE_PACKAGE = -5; - - /** - * Installation return code: this is passed to the {@link IPackageInstallObserver} by - * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if - * the requested shared user does not exist. - */ - public static final int INSTALL_FAILED_NO_SHARED_USER = -6; - - /** - * Installation return code: this is passed to the {@link IPackageInstallObserver} by - * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if - * a previously installed package of the same name has a different signature - * than the new package (and the old package's data was not removed). - */ - public static final int INSTALL_FAILED_UPDATE_INCOMPATIBLE = -7; - - /** - * Installation return code: this is passed to the {@link IPackageInstallObserver} by - * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if - * the new package is requested a shared user which is already installed on the - * device and does not have matching signature. - */ - public static final int INSTALL_FAILED_SHARED_USER_INCOMPATIBLE = -8; - - /** - * Installation return code: this is passed to the {@link IPackageInstallObserver} by - * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if - * the new package uses a shared library that is not available. - */ - public static final int INSTALL_FAILED_MISSING_SHARED_LIBRARY = -9; - - /** - * Installation return code: this is passed to the {@link IPackageInstallObserver} by - * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if - * the new package uses a shared library that is not available. - */ - public static final int INSTALL_FAILED_REPLACE_COULDNT_DELETE = -10; - - /** - * Installation return code: this is passed to the {@link IPackageInstallObserver} by - * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if - * the new package failed while optimizing and validating its dex files, - * either because there was not enough storage or the validation failed. - */ - public static final int INSTALL_FAILED_DEXOPT = -11; - - /** - * Installation return code: this is passed to the {@link IPackageInstallObserver} by - * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if - * the new package failed because the current SDK version is older than - * that required by the package. - */ - public static final int INSTALL_FAILED_OLDER_SDK = -12; - - /** - * Installation return code: this is passed to the {@link IPackageInstallObserver} by - * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if - * the new package failed because it contains a content provider with the - * same authority as a provider already installed in the system. - */ - public static final int INSTALL_FAILED_CONFLICTING_PROVIDER = -13; - - /** - * Installation return code: this is passed to the {@link IPackageInstallObserver} by - * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if - * the new package failed because the current SDK version is newer than - * that required by the package. - */ - public static final int INSTALL_FAILED_NEWER_SDK = -14; - - /** - * Installation return code: this is passed to the {@link IPackageInstallObserver} by - * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if - * the new package failed because it has specified that it is a test-only - * package and the caller has not supplied the {@link #INSTALL_ALLOW_TEST} - * flag. - */ - public static final int INSTALL_FAILED_TEST_ONLY = -15; - - /** - * Installation return code: this is passed to the {@link IPackageInstallObserver} by - * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if - * the package being installed contains native code, but none that is - * compatible with the device's CPU_ABI. - */ - public static final int INSTALL_FAILED_CPU_ABI_INCOMPATIBLE = -16; - - /** - * Installation return code: this is passed to the {@link IPackageInstallObserver} by - * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if - * the new package uses a feature that is not available. - */ - public static final int INSTALL_FAILED_MISSING_FEATURE = -17; - - // ------ Errors related to sdcard - /** - * Installation return code: this is passed to the {@link IPackageInstallObserver} by - * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if - * a secure container mount point couldn't be accessed on external media. - */ - public static final int INSTALL_FAILED_CONTAINER_ERROR = -18; - - /** - * Installation return code: this is passed to the {@link IPackageInstallObserver} by - * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if - * the new package couldn't be installed in the specified install - * location. - */ - public static final int INSTALL_FAILED_INVALID_INSTALL_LOCATION = -19; - - /** - * Installation return code: this is passed to the {@link IPackageInstallObserver} by - * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if - * the new package couldn't be installed in the specified install - * location because the media is not available. - */ - public static final int INSTALL_FAILED_MEDIA_UNAVAILABLE = -20; - - /** - * Installation return code: this is passed to the {@link IPackageInstallObserver} by - * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if - * the new package couldn't be installed because the verification timed out. - */ - public static final int INSTALL_FAILED_VERIFICATION_TIMEOUT = -21; - - /** - * Installation return code: this is passed to the {@link IPackageInstallObserver} by - * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if - * the new package couldn't be installed because the verification did not succeed. - */ - public static final int INSTALL_FAILED_VERIFICATION_FAILURE = -22; - - /** - * Installation return code: this is passed to the {@link IPackageInstallObserver} by - * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if - * the package changed from what the calling program expected. - */ - public static final int INSTALL_FAILED_PACKAGE_CHANGED = -23; - - /** - * Installation return code: this is passed to the {@link IPackageInstallObserver} by - * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if - * the new package is assigned a different UID than it previously held. - */ - public static final int INSTALL_FAILED_UID_CHANGED = -24; - - /** - * Installation return code: this is passed to the {@link IPackageInstallObserver} by - * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if - * the new package has an older version code than the currently installed package. - */ - public static final int INSTALL_FAILED_VERSION_DOWNGRADE = -25; - - /** - * Installation parse return code: this is passed to the {@link IPackageInstallObserver} by - * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} - * if the parser was given a path that is not a file, or does not end with the expected - * '.apk' extension. - */ - public static final int INSTALL_PARSE_FAILED_NOT_APK = -100; - - /** - * Installation parse return code: this is passed to the {@link IPackageInstallObserver} by - * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} - * if the parser was unable to retrieve the AndroidManifest.xml file. - */ - public static final int INSTALL_PARSE_FAILED_BAD_MANIFEST = -101; - - /** - * Installation parse return code: this is passed to the {@link IPackageInstallObserver} by - * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} - * if the parser encountered an unexpected exception. - */ - public static final int INSTALL_PARSE_FAILED_UNEXPECTED_EXCEPTION = -102; - - /** - * Installation parse return code: this is passed to the {@link IPackageInstallObserver} by - * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} - * if the parser did not find any certificates in the .apk. - */ - public static final int INSTALL_PARSE_FAILED_NO_CERTIFICATES = -103; - - /** - * Installation parse return code: this is passed to the {@link IPackageInstallObserver} by - * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} - * if the parser found inconsistent certificates on the files in the .apk. - */ - public static final int INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES = -104; - - /** - * Installation parse return code: this is passed to the {@link IPackageInstallObserver} by - * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} - * if the parser encountered a CertificateEncodingException in one of the - * files in the .apk. - */ - public static final int INSTALL_PARSE_FAILED_CERTIFICATE_ENCODING = -105; - - /** - * Installation parse return code: this is passed to the {@link IPackageInstallObserver} by - * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} - * if the parser encountered a bad or missing package name in the manifest. - */ - public static final int INSTALL_PARSE_FAILED_BAD_PACKAGE_NAME = -106; - - /** - * Installation parse return code: this is passed to the {@link IPackageInstallObserver} by - * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} - * if the parser encountered a bad shared user id name in the manifest. - */ - public static final int INSTALL_PARSE_FAILED_BAD_SHARED_USER_ID = -107; - - /** - * Installation parse return code: this is passed to the {@link IPackageInstallObserver} by - * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} - * if the parser encountered some structural problem in the manifest. - */ - public static final int INSTALL_PARSE_FAILED_MANIFEST_MALFORMED = -108; - - /** - * Installation parse return code: this is passed to the {@link IPackageInstallObserver} by - * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} - * if the parser did not find any actionable tags (instrumentation or application) - * in the manifest. - */ - public static final int INSTALL_PARSE_FAILED_MANIFEST_EMPTY = -109; - - /** - * Installation failed return code: this is passed to the {@link IPackageInstallObserver} by - * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} - * if the system failed to install the package because of system issues. - */ - public static final int INSTALL_FAILED_INTERNAL_ERROR = -110; - - /** - * Installation failed return code: this is passed to the {@link IPackageInstallObserver} by - * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} - * if the system failed to install the package because the user is restricted from installing - * apps. - */ - public static final int INSTALL_FAILED_USER_RESTRICTED = -111; - - /** - * Installation failed return code: this is passed to the {@link IPackageInstallObserver} by - * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} - * if the system failed to install the package because it is attempting to define a - * permission that is already defined by some existing package. - *

    - * The package name of the app which has already defined the permission is passed to - * a {@link PackageInstallObserver}, if any, as the {@link #EXTRA_EXISTING_PACKAGE} - * string extra; and the name of the permission being redefined is passed in the - * {@link #EXTRA_EXISTING_PERMISSION} string extra. - */ - public static final int INSTALL_FAILED_DUPLICATE_PERMISSION = -112; - - /** - * Installation failed return code: this is passed to the {@link IPackageInstallObserver} by - * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} - * if the system failed to install the package because its packaged native code did not - * match any of the ABIs supported by the system. - */ - public static final int INSTALL_FAILED_NO_MATCHING_ABIS = -113; - - /** - * Internal return code for NativeLibraryHelper methods to indicate that the package - * being processed did not contain any native code. This is placed here only so that - * it can belong to the same value space as the other install failure codes. - */ - public static final int NO_NATIVE_LIBRARIES = -114; - - public static final int INSTALL_FAILED_ABORTED = -115; - - /** - * Return code for when package deletion succeeds. This is passed to the - * {@link IPackageDeleteObserver} by {@link #deletePackage()} if the system - * succeeded in deleting the package. - */ - public static final int DELETE_SUCCEEDED = 1; - - /** - * Deletion failed return code: this is passed to the - * {@link IPackageDeleteObserver} by {@link #deletePackage()} if the system - * failed to delete the package for an unspecified reason. - */ - public static final int DELETE_FAILED_INTERNAL_ERROR = -1; - - /** - * Deletion failed return code: this is passed to the - * {@link IPackageDeleteObserver} by {@link #deletePackage()} if the system - * failed to delete the package because it is the active DevicePolicy - * manager. - */ - public static final int DELETE_FAILED_DEVICE_POLICY_MANAGER = -2; - - /** - * Deletion failed return code: this is passed to the - * {@link IPackageDeleteObserver} by {@link #deletePackage()} if the system - * failed to delete the package since the user is restricted. - */ - public static final int DELETE_FAILED_USER_RESTRICTED = -3; - - /** - * Deletion failed return code: this is passed to the - * {@link IPackageDeleteObserver} by {@link #deletePackage()} if the system - * failed to delete the package because a profile - * or device owner has marked the package as uninstallable. - */ - public static final int DELETE_FAILED_OWNER_BLOCKED = -4; - - public static final int DELETE_FAILED_ABORTED = -5; } From 2776b860505f2ff4b3f1fe3280e68b3212155678 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Sch=C3=BCrmann?= Date: Sun, 29 May 2016 23:34:00 +0300 Subject: [PATCH 08/21] Redesign PrivilegedInstaller * use new local broadcasts * show permission screen before download * display permission screen as dialog --- app/src/main/AndroidManifest.xml | 6 + .../java/org/fdroid/fdroid/AppDetails.java | 46 +++++- .../java/org/fdroid/fdroid/FDroidApp.java | 17 +++ .../main/java/org/fdroid/fdroid/Utils.java | 14 ++ .../org/fdroid/fdroid/data/ApkProvider.java | 6 +- .../fdroid/installer/DefaultInstaller.java | 5 + .../fdroid/installer/ExtensionInstaller.java | 5 + .../fdroid/fdroid/installer/Installer.java | 69 +++++---- .../fdroid/installer/PrivilegedInstaller.java | 37 +---- .../fdroid/privileged/views/AppDiff.java | 32 +++- .../views/AppSecurityPermissions.java | 3 +- .../views/InstallConfirmActivity.java | 55 +++++-- .../views/UninstallDialogActivity.java | 106 +++++++++++++ .../main/res/layout-v11/install_confirm.xml | 142 +++++++++--------- .../main/res/layout/install_app_details.xml | 70 ++++----- app/src/main/res/layout/install_confirm.xml | 139 ++++++++--------- app/src/main/res/layout/install_start.xml | 17 +-- app/src/main/res/values/strings.xml | 6 +- app/src/main/res/values/styles.xml | 14 ++ 19 files changed, 517 insertions(+), 272 deletions(-) create mode 100644 app/src/main/java/org/fdroid/fdroid/privileged/views/UninstallDialogActivity.java diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 6f5d2d622..b4ae7cd49 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -316,12 +316,18 @@ + toArrayList() { + ArrayList out = new ArrayList<>(); + for (String element : this) { + out.add(element); + } + return out; + } + + public String[] toArray() { + ArrayList list = toArrayList(); + return list.toArray(new String[list.size()]); + } + public boolean contains(String v) { for (final String s : this) { if (s.equals(v)) { diff --git a/app/src/main/java/org/fdroid/fdroid/data/ApkProvider.java b/app/src/main/java/org/fdroid/fdroid/data/ApkProvider.java index f1629c657..e1395f92d 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/ApkProvider.java +++ b/app/src/main/java/org/fdroid/fdroid/data/ApkProvider.java @@ -107,8 +107,12 @@ public class ApkProvider extends FDroidProvider { } public static Apk find(Context context, String packageName, int versionCode, String[] projection) { - ContentResolver resolver = context.getContentResolver(); final Uri uri = getContentUri(packageName, versionCode); + return find(context, uri, projection); + } + + public static Apk find(Context context, Uri uri, String[] projection) { + ContentResolver resolver = context.getContentResolver(); Cursor cursor = resolver.query(uri, projection, null, null, null); Apk apk = null; if (cursor != null) { diff --git a/app/src/main/java/org/fdroid/fdroid/installer/DefaultInstaller.java b/app/src/main/java/org/fdroid/fdroid/installer/DefaultInstaller.java index d43f6d040..169c2a07c 100644 --- a/app/src/main/java/org/fdroid/fdroid/installer/DefaultInstaller.java +++ b/app/src/main/java/org/fdroid/fdroid/installer/DefaultInstaller.java @@ -85,4 +85,9 @@ public class DefaultInstaller extends Installer { sendBroadcastUninstall(packageName, Installer.ACTION_UNINSTALL_USER_INTERACTION, uninstallPendingIntent); } + + @Override + protected boolean isUnattended() { + return false; + } } diff --git a/app/src/main/java/org/fdroid/fdroid/installer/ExtensionInstaller.java b/app/src/main/java/org/fdroid/fdroid/installer/ExtensionInstaller.java index c778a6897..8782085bf 100644 --- a/app/src/main/java/org/fdroid/fdroid/installer/ExtensionInstaller.java +++ b/app/src/main/java/org/fdroid/fdroid/installer/ExtensionInstaller.java @@ -96,4 +96,9 @@ public class ExtensionInstaller extends Installer { // don't use broadcasts for the rest of this special installer sendBroadcastUninstall(packageName, Installer.ACTION_UNINSTALL_COMPLETE); } + + @Override + protected boolean isUnattended() { + return false; + } } diff --git a/app/src/main/java/org/fdroid/fdroid/installer/Installer.java b/app/src/main/java/org/fdroid/fdroid/installer/Installer.java index 3663ecd5c..1a8e1de9a 100644 --- a/app/src/main/java/org/fdroid/fdroid/installer/Installer.java +++ b/app/src/main/java/org/fdroid/fdroid/installer/Installer.java @@ -37,6 +37,8 @@ import org.fdroid.fdroid.data.ApkProvider; import org.fdroid.fdroid.data.SanitizedFile; 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.File; import java.io.IOException; @@ -153,48 +155,55 @@ public abstract class Installer { return Uri.fromFile(sanitizedApkFile); } - public PendingIntent getPermissionScreen(Apk apk) { - // old code: -// Uri packageUri = Uri.fromFile(apkFile); -// int count = newPermissionCount(packageUri); -// if (count < 0) { -// mCallback.onError(InstallerCallback.OPERATION_INSTALL, -// InstallerCallback.ERROR_CODE_CANNOT_PARSE); + public Intent getPermissionScreen(Apk apk) { + if (!isUnattended()) { + return null; + } -// install_error_cannot_parse + int count = newPermissionCount(apk); + if (count > 0) { + Uri uri = ApkProvider.getContentUri(apk); + Intent intent = new Intent(mContext, InstallConfirmActivity.class); + intent.setData(uri); -// return; -// } -// if (count > 0) { -// Intent intent = new Intent(mContext, InstallConfirmActivity.class); -// intent.setData(packageUri); -// mActivity.startActivityForResult(intent, REQUEST_CONFIRM_PERMS); -// } else { -// try { -// doInstallPackageInternal(packageUri); -// } catch (InstallFailedException e) { -// mCallback.onError(InstallerCallback.OPERATION_INSTALL, -// InstallerCallback.ERROR_CODE_OTHER); -// } -// } - return null; + return intent; + } else { + // no permission screen needed! + return null; + } } + private int newPermissionCount(Apk apk) { + // TODO: requires targetSdk in Apk class/database +// boolean supportsRuntimePermissions = mPkgInfo.applicationInfo.targetSdkVersion +// >= Build.VERSION_CODES.M; +// if (supportsRuntimePermissions) { +// return 0; +// } - private int newPermissionCount(Uri packageUri) { - AppDiff appDiff = new AppDiff(mContext.getPackageManager(), packageUri); + AppDiff appDiff = new AppDiff(mContext.getPackageManager(), apk); if (appDiff.mPkgInfo == null) { // could not get diff because we couldn't parse the package - return -1; + throw new RuntimeException("cannot parse!"); } AppSecurityPermissions perms = new AppSecurityPermissions(mContext, appDiff.mPkgInfo); if (appDiff.mInstalledAppInfo != null) { // update to an existing app return perms.getPermissionCount(AppSecurityPermissions.WHICH_NEW); } - // default: even if there aren't any permissions, we want to make the - // user always confirm installing new apps - return 1; + // new app install + return perms.getPermissionCount(AppSecurityPermissions.WHICH_ALL); + } + + public Intent getUninstallScreen(String packageName) { + if (!isUnattended()) { + return null; + } + + Intent intent = new Intent(mContext, UninstallDialogActivity.class); + intent.putExtra(Installer.EXTRA_PACKAGE_NAME, packageName); + + return intent; } /** @@ -287,4 +296,6 @@ public abstract class Installer { protected abstract void uninstallPackage(String packageName); + protected abstract boolean isUnattended(); + } diff --git a/app/src/main/java/org/fdroid/fdroid/installer/PrivilegedInstaller.java b/app/src/main/java/org/fdroid/fdroid/installer/PrivilegedInstaller.java index 8c3dfcdb4..420adc0b5 100644 --- a/app/src/main/java/org/fdroid/fdroid/installer/PrivilegedInstaller.java +++ b/app/src/main/java/org/fdroid/fdroid/installer/PrivilegedInstaller.java @@ -238,7 +238,7 @@ public class PrivilegedInstaller extends Installer { private static final HashMap sUninstallReturnCodes; static { - // Descriptions extrgacted from the source code comments in AOSP + // Descriptions extracted from the source code comments in AOSP sUninstallReturnCodes = new HashMap<>(); sUninstallReturnCodes.put(DELETE_SUCCEEDED, "Success"); @@ -324,6 +324,7 @@ public class PrivilegedInstaller extends Installer { @Override protected void installPackage(final Uri uri, final Uri originatingUri, String packageName) { + sendBroadcastInstall(uri, originatingUri, Installer.ACTION_INSTALL_STARTED); final Uri sanitizedUri; try { @@ -374,6 +375,8 @@ public class PrivilegedInstaller extends Installer { @Override protected void uninstallPackage(final String packageName) { + sendBroadcastUninstall(packageName, Installer.ACTION_UNINSTALL_STARTED); + ApplicationInfo appInfo; try { //noinspection WrongConstant (lint is actually wrong here!) @@ -466,33 +469,9 @@ public class PrivilegedInstaller extends Installer { Context.BIND_AUTO_CREATE); } -// @Override -// public boolean handleOnActivityResult(int requestCode, int resultCode, Intent data) { -// switch (requestCode) { -// case REQUEST_CONFIRM_PERMS: -// if (resultCode == Activity.RESULT_OK) { -// final Uri packageUri = data.getData(); -// try { -// doInstallPackageInternal(packageUri); -// } catch (InstallFailedException e) { -// mCallback.onError(InstallerCallback.OPERATION_INSTALL, -// InstallerCallback.ERROR_CODE_OTHER); -// } -// } else if (resultCode == InstallConfirmActivity.RESULT_CANNOT_PARSE) { -// mCallback.onError(InstallerCallback.OPERATION_INSTALL, -// InstallerCallback.ERROR_CODE_CANNOT_PARSE); - -// install_error_cannot_parse - -// } else { // Activity.RESULT_CANCELED -// mCallback.onError(InstallerCallback.OPERATION_INSTALL, -// InstallerCallback.ERROR_CODE_CANCELED); -// } -// return true; -// default: -// return false; -// } -// } - + @Override + protected boolean isUnattended() { + return true; + } } diff --git a/app/src/main/java/org/fdroid/fdroid/privileged/views/AppDiff.java b/app/src/main/java/org/fdroid/fdroid/privileged/views/AppDiff.java index 096f0290b..46829a162 100644 --- a/app/src/main/java/org/fdroid/fdroid/privileged/views/AppDiff.java +++ b/app/src/main/java/org/fdroid/fdroid/privileged/views/AppDiff.java @@ -18,11 +18,18 @@ package org.fdroid.fdroid.privileged.views; +import android.annotation.TargetApi; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.net.Uri; +import android.os.Build; +import org.fdroid.fdroid.data.Apk; + +import java.util.ArrayList; + +@TargetApi(Build.VERSION_CODES.M) public class AppDiff { private final PackageManager mPm; @@ -30,6 +37,29 @@ public class AppDiff { public ApplicationInfo mInstalledAppInfo; + /** + * Constructor based on F-Droids Apk object + */ + public AppDiff(PackageManager mPm, Apk apk) { + this.mPm = mPm; + + if (apk.permissions == null) { + throw new RuntimeException("apk.permissions is null"); + } + mPkgInfo = new PackageInfo(); + mPkgInfo.packageName = apk.packageName; + mPkgInfo.applicationInfo = new ApplicationInfo(); + + // TODO: duplicate code with Permission.fdroidToAndroid + ArrayList permissionsFixed = new ArrayList<>(); + for (String perm : apk.permissions.toArrayList()) { + permissionsFixed.add("android.permission." + perm); + } + mPkgInfo.requestedPermissions = permissionsFixed.toArray(new String[permissionsFixed.size()]); + + init(); + } + public AppDiff(PackageManager mPm, Uri mPackageURI) { this.mPm = mPm; @@ -55,7 +85,7 @@ public class AppDiff { String pkgName = mPkgInfo.packageName; // Check if there is already a package on the device with this name // but it has been renamed to something else. - final String[] oldName = mPm.canonicalToCurrentPackageNames(new String[] {pkgName}); + final String[] oldName = mPm.canonicalToCurrentPackageNames(new String[]{pkgName}); if (oldName != null && oldName.length > 0 && oldName[0] != null) { pkgName = oldName[0]; mPkgInfo.packageName = pkgName; diff --git a/app/src/main/java/org/fdroid/fdroid/privileged/views/AppSecurityPermissions.java b/app/src/main/java/org/fdroid/fdroid/privileged/views/AppSecurityPermissions.java index b48108682..75981bbc5 100644 --- a/app/src/main/java/org/fdroid/fdroid/privileged/views/AppSecurityPermissions.java +++ b/app/src/main/java/org/fdroid/fdroid/privileged/views/AppSecurityPermissions.java @@ -235,8 +235,7 @@ public class AppSecurityPermissions { try { installedPkgInfo = mPm.getPackageInfo(info.packageName, PackageManager.GET_PERMISSIONS); - } catch (NameNotFoundException e) { - throw new RuntimeException("NameNotFoundException during GET_PERMISSIONS!"); + } catch (NameNotFoundException ignored) { } extractPerms(info, permSet, installedPkgInfo); } diff --git a/app/src/main/java/org/fdroid/fdroid/privileged/views/InstallConfirmActivity.java b/app/src/main/java/org/fdroid/fdroid/privileged/views/InstallConfirmActivity.java index ceddb8f43..3d983633b 100644 --- a/app/src/main/java/org/fdroid/fdroid/privileged/views/InstallConfirmActivity.java +++ b/app/src/main/java/org/fdroid/fdroid/privileged/views/InstallConfirmActivity.java @@ -18,16 +18,16 @@ package org.fdroid.fdroid.privileged.views; -import android.app.Activity; import android.content.Context; import android.content.DialogInterface; import android.content.DialogInterface.OnCancelListener; import android.content.Intent; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; -import android.graphics.drawable.Drawable; +import android.graphics.Bitmap; import android.net.Uri; import android.os.Bundle; +import android.support.v4.app.FragmentActivity; import android.support.v4.view.ViewPager; import android.view.LayoutInflater; import android.view.View; @@ -38,15 +38,23 @@ import android.widget.ImageView; import android.widget.TabHost; import android.widget.TextView; +import com.nostra13.universalimageloader.core.DisplayImageOptions; +import com.nostra13.universalimageloader.core.ImageLoader; +import com.nostra13.universalimageloader.core.assist.ImageScaleType; + import org.fdroid.fdroid.FDroidApp; import org.fdroid.fdroid.R; +import org.fdroid.fdroid.data.Apk; +import org.fdroid.fdroid.data.ApkProvider; +import org.fdroid.fdroid.data.App; +import org.fdroid.fdroid.data.AppProvider; /** * NOTES: * Parts are based on AOSP src/com/android/packageinstaller/PackageInstallerActivity.java * latest included commit: c23d802958158d522e7350321ad9ac6d43013883 */ -public class InstallConfirmActivity extends Activity implements OnCancelListener, OnClickListener { +public class InstallConfirmActivity extends FragmentActivity implements OnCancelListener, OnClickListener { public static final int RESULT_CANNOT_PARSE = RESULT_FIRST_USER + 1; @@ -67,16 +75,27 @@ public class InstallConfirmActivity extends Activity implements OnCancelListener private static final String TAB_ID_ALL = "all"; private static final String TAB_ID_NEW = "new"; + private App mApp; + + private final DisplayImageOptions displayImageOptions = new DisplayImageOptions.Builder() + .cacheInMemory(true) + .cacheOnDisk(true) + .imageScaleType(ImageScaleType.NONE) + .showImageOnLoading(R.drawable.ic_repo_app_default) + .showImageForEmptyUri(R.drawable.ic_repo_app_default) + .bitmapConfig(Bitmap.Config.RGB_565) + .build(); + private void startInstallConfirm() { - - final Drawable appIcon = mAppDiff.mPkgInfo.applicationInfo.loadIcon(mPm); - final String appLabel = (String) mAppDiff.mPkgInfo.applicationInfo.loadLabel(mPm); - View appSnippet = findViewById(R.id.app_snippet); - ((ImageView) appSnippet.findViewById(R.id.app_icon)).setImageDrawable(appIcon); - ((TextView) appSnippet.findViewById(R.id.app_name)).setText(appLabel); - + TextView appName = (TextView) appSnippet.findViewById(R.id.app_name); + ImageView appIcon = (ImageView) appSnippet.findViewById(R.id.app_icon); TabHost tabHost = (TabHost) findViewById(android.R.id.tabhost); + + appName.setText(mApp.name); + ImageLoader.getInstance().displayImage(mApp.iconUrlLarge, appIcon, + displayImageOptions); + tabHost.setup(); ViewPager viewPager = (ViewPager) findViewById(R.id.pager); TabsAdapter adapter = new TabsAdapter(this, tabHost, viewPager); @@ -136,7 +155,7 @@ public class InstallConfirmActivity extends Activity implements OnCancelListener : R.string.install_confirm_update_no_perms; } else { // This is a new application with no permissions. - msg = R.string.install_confirm_no_perms; + throw new RuntimeException("no permissions requested. This screen should not appear!"); } tabHost.setVisibility(View.GONE); findViewById(R.id.filler).setVisibility(View.VISIBLE); @@ -171,20 +190,28 @@ public class InstallConfirmActivity extends Activity implements OnCancelListener protected void onCreate(Bundle icicle) { super.onCreate(icicle); - ((FDroidApp) getApplication()).applyTheme(this); + ((FDroidApp) getApplication()).applyDialogTheme(this); mPm = getPackageManager(); intent = getIntent(); - Uri packageURI = intent.getData(); + Uri uri = intent.getData(); + Apk apk = ApkProvider.Helper.find(this, uri, ApkProvider.DataColumns.ALL); + mApp = AppProvider.Helper.findByPackageName(getContentResolver(), apk.packageName); - mAppDiff = new AppDiff(mPm, packageURI); + mAppDiff = new AppDiff(mPm, apk); if (mAppDiff.mPkgInfo == null) { setResult(RESULT_CANNOT_PARSE, intent); finish(); } setContentView(R.layout.install_start); + + // increase dialog to full width for now + // TODO: create a better design and minimum width for tablets + getWindow().setLayout(ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT); + mInstallConfirm = findViewById(R.id.install_confirm_panel); mInstallConfirm.setVisibility(View.INVISIBLE); diff --git a/app/src/main/java/org/fdroid/fdroid/privileged/views/UninstallDialogActivity.java b/app/src/main/java/org/fdroid/fdroid/privileged/views/UninstallDialogActivity.java new file mode 100644 index 000000000..f0d2bd04c --- /dev/null +++ b/app/src/main/java/org/fdroid/fdroid/privileged/views/UninstallDialogActivity.java @@ -0,0 +1,106 @@ +/* + * Copyright (C) 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.privileged.views; + +import android.app.Activity; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.support.v4.app.FragmentActivity; +import android.support.v7.app.AlertDialog; +import android.view.ContextThemeWrapper; + +import org.fdroid.fdroid.FDroidApp; +import org.fdroid.fdroid.R; +import org.fdroid.fdroid.installer.Installer; + +public class UninstallDialogActivity extends FragmentActivity { + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + final Intent intent = getIntent(); + final String packageName = intent.getStringExtra(Installer.EXTRA_PACKAGE_NAME); + + PackageManager pm = getPackageManager(); + + ApplicationInfo appInfo; + try { + //noinspection WrongConstant (lint is actually wrong here!) + appInfo = pm.getApplicationInfo(packageName, PackageManager.GET_UNINSTALLED_PACKAGES); + } catch (PackageManager.NameNotFoundException e) { + throw new RuntimeException("Failed to get ApplicationInfo for uninstalling"); + } + + final boolean isSystem = (appInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0; + final boolean isUpdate = (appInfo.flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0; + + if (isSystem && !isUpdate) { + // Cannot remove system apps unless we're uninstalling updates + throw new RuntimeException("Cannot remove system apps unless we're uninstalling updates"); + } + + int messageId; + if (isUpdate) { + messageId = R.string.uninstall_update_confirm; + } else { + messageId = R.string.uninstall_confirm; + } + + // hack to get theme applied (which is not automatically applied due to activity's Theme.NoDisplay + ContextThemeWrapper theme = new ContextThemeWrapper(this, FDroidApp.getCurThemeResId()); + + final AlertDialog.Builder builder = new AlertDialog.Builder(theme); + builder.setTitle(appInfo.loadLabel(pm)); + builder.setIcon(appInfo.loadIcon(pm)); + builder.setPositiveButton(android.R.string.ok, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + Intent data = new Intent(); + data.putExtra(Installer.EXTRA_PACKAGE_NAME, packageName); + setResult(Activity.RESULT_OK, intent); + finish(); + } + }); + builder.setNegativeButton(android.R.string.cancel, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + setResult(Activity.RESULT_CANCELED); + finish(); + } + }); + builder.setOnCancelListener( + new DialogInterface.OnCancelListener() { + @Override + public void onCancel(DialogInterface dialog) { + setResult(Activity.RESULT_CANCELED); + finish(); + } + }); + builder.setMessage(messageId); + builder.create().show(); + } +} diff --git a/app/src/main/res/layout-v11/install_confirm.xml b/app/src/main/res/layout-v11/install_confirm.xml index 7676b2b75..e0d978ff1 100644 --- a/app/src/main/res/layout-v11/install_confirm.xml +++ b/app/src/main/res/layout-v11/install_confirm.xml @@ -21,37 +21,35 @@ user before it is installed. --> - + + android:id="@+id/install_confirm" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:paddingLeft="16dp" + android:paddingRight="16dp" + android:paddingTop="4dip" + android:text="@string/install_confirm" + android:textAppearance="?android:attr/textAppearanceMedium" /> + android:id="@+id/divider" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="16dp" + android:background="?android:attr/dividerHorizontal" + android:visibility="gone" /> - + android:visibility="gone"> + android:layout_height="match_parent" + android:orientation="vertical"> - - + + + + android:layout_gravity="center" + android:orientation="horizontal" /> @@ -85,64 +87,68 @@ android:id="@android:id/tabcontent" android:layout_width="0dp" android:layout_height="0dp" - android:layout_weight="0"/> + android:layout_weight="0" /> + android:layout_weight="1" /> + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:divider="?android:attr/dividerHorizontal" + android:orientation="vertical" + android:showDividers="beginning"> + + + android:visibility="gone" /> - +