diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 85f2b0310..9fb2fe575 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -316,12 +316,18 @@
+
+
+
+
+
@@ -440,6 +454,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..5eb446118 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;
@@ -78,17 +79,16 @@ import com.nostra13.universalimageloader.core.ImageLoader;
import com.nostra13.universalimageloader.core.assist.ImageScaleType;
import org.fdroid.fdroid.Utils.CommaSeparatedList;
-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.AppProvider;
import org.fdroid.fdroid.data.InstalledAppProvider;
import org.fdroid.fdroid.data.RepoProvider;
-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.InstallManagerService;
+import org.fdroid.fdroid.installer.InstallerFactory;
+import org.fdroid.fdroid.installer.InstallerService;
import org.fdroid.fdroid.net.Downloader;
import org.fdroid.fdroid.net.DownloaderService;
@@ -101,6 +101,8 @@ public class AppDetails extends AppCompatActivity {
private static final String TAG = "AppDetails";
private static final int REQUEST_ENABLE_BLUETOOTH = 2;
+ private static final int REQUEST_PERMISSION_DIALOG = 3;
+ private static final int REQUEST_UNINSTALL_DIALOG = 4;
public static final String EXTRA_APPID = "appid";
public static final String EXTRA_FROM = "from";
@@ -319,7 +321,6 @@ public class AppDetails extends AppCompatActivity {
private int startingIgnoreThis;
private final Context context = this;
- private Installer installer;
private AppDetailsHeaderFragment headerFragment;
@@ -375,8 +376,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 +529,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,
+ Installer.getInstallIntentFilter(localUri));
}
};
@@ -555,6 +553,108 @@ public class AppDetails extends AppCompatActivity {
}
};
+ private final BroadcastReceiver installReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ switch (intent.getAction()) {
+ case Installer.ACTION_INSTALL_STARTED:
+ headerFragment.startProgress();
+ headerFragment.showIndeterminateProgress(getString(R.string.installing));
+ break;
+ case Installer.ACTION_INSTALL_COMPLETE:
+ headerFragment.removeProgress();
+
+ localBroadcastManager.unregisterReceiver(this);
+ break;
+ case Installer.ACTION_INSTALL_INTERRUPTED:
+ headerFragment.removeProgress();
+ onAppChanged();
+
+ String errorMessage =
+ intent.getStringExtra(Installer.EXTRA_ERROR_MESSAGE);
+
+ if (!TextUtils.isEmpty(errorMessage)) {
+ Log.e(TAG, "install 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(title);
+ alertBuilder.setMessage(errorMessage);
+ alertBuilder.setNeutralButton(android.R.string.ok, null);
+ alertBuilder.create().show();
+ }
+
+ localBroadcastManager.unregisterReceiver(this);
+ 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!");
+ }
+ }
+ };
+
+ private final BroadcastReceiver uninstallReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ switch (intent.getAction()) {
+ case Installer.ACTION_UNINSTALL_STARTED:
+ headerFragment.startProgress();
+ headerFragment.showIndeterminateProgress(getString(R.string.uninstalling));
+ break;
+ case Installer.ACTION_UNINSTALL_COMPLETE:
+ headerFragment.removeProgress();
+ onAppChanged();
+
+ localBroadcastManager.unregisterReceiver(this);
+ break;
+ case Installer.ACTION_UNINSTALL_INTERRUPTED:
+ headerFragment.removeProgress();
+
+ String errorMessage =
+ intent.getStringExtra(Installer.EXTRA_ERROR_MESSAGE);
+
+ if (!TextUtils.isEmpty(errorMessage)) {
+ Log.e(TAG, "uninstall 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:
+ PendingIntent uninstallPendingIntent =
+ intent.getParcelableExtra(Installer.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 +896,7 @@ public class AppDetails extends AppCompatActivity {
return true;
case UNINSTALL:
- removeApk(app.packageName);
+ uninstallApk(app.packageName);
return true;
case IGNOREALL:
@@ -875,76 +975,43 @@ public class AppDetails extends AppCompatActivity {
}
private void initiateInstall(Apk apk) {
+ Installer installer = InstallerFactory.create(this, apk.packageName);
+ Intent intent = installer.getPermissionScreen(apk);
+ if (intent != null) {
+ // permission screen required
+ Utils.debugLog(TAG, "permission screen required");
+ startActivityForResult(intent, REQUEST_PERMISSION_DIALOG);
+ return;
+ }
+
+ startInstall(apk);
+ }
+
+ private void startInstall(Apk apk) {
activeDownloadUrlString = apk.getUrl();
registerDownloaderReceivers();
headerFragment.startProgress();
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) {
+ Installer installer = InstallerFactory.create(this, packageName);
+ Intent intent = installer.getUninstallScreen(packageName);
+ if (intent != null) {
+ // uninstall screen required
+ Utils.debugLog(TAG, "screen screen required");
+ startActivityForResult(intent, REQUEST_UNINSTALL_DIALOG);
+ return;
}
+
+ startUninstall();
}
- 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 startUninstall() {
+ localBroadcastManager.registerReceiver(uninstallReceiver,
+ Installer.getUninstallIntentFilter(app.packageName));
+ InstallerService.uninstall(context, app.packageName);
+ }
private void launchApk(String packageName) {
Intent intent = packageManager.getLaunchIntentForPackage(packageName);
@@ -963,15 +1030,22 @@ 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);
break;
+ case REQUEST_PERMISSION_DIALOG:
+ if (resultCode == Activity.RESULT_OK) {
+ Uri uri = data.getData();
+ Apk apk = ApkProvider.Helper.find(this, uri, ApkProvider.DataColumns.ALL);
+ startInstall(apk);
+ }
+ break;
+ case REQUEST_UNINSTALL_DIALOG:
+ if (resultCode == Activity.RESULT_OK) {
+ startUninstall();
+ }
+ break;
}
}
@@ -1606,7 +1680,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 +1709,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/FDroidApp.java b/app/src/main/java/org/fdroid/fdroid/FDroidApp.java
index df5e614c1..95e0ea708 100644
--- a/app/src/main/java/org/fdroid/fdroid/FDroidApp.java
+++ b/app/src/main/java/org/fdroid/fdroid/FDroidApp.java
@@ -125,6 +125,23 @@ public class FDroidApp extends Application {
}
}
+ public void applyDialogTheme(Activity activity) {
+ activity.setTheme(getCurDialogThemeResId());
+ }
+
+ public static int getCurDialogThemeResId() {
+ switch (curTheme) {
+ case light:
+ return R.style.MinWithDialogBaseThemeLight;
+ case dark:
+ return R.style.MinWithDialogBaseThemeDark;
+ case night:
+ return R.style.MinWithDialogBaseThemeDark;
+ default:
+ return R.style.MinWithDialogBaseThemeLight;
+ }
+ }
+
public static void enableSpongyCastle() {
Security.addProvider(SPONGYCASTLE_PROVIDER);
}
diff --git a/app/src/main/java/org/fdroid/fdroid/Utils.java b/app/src/main/java/org/fdroid/fdroid/Utils.java
index c93a6e466..73b81d805 100644
--- a/app/src/main/java/org/fdroid/fdroid/Utils.java
+++ b/app/src/main/java/org/fdroid/fdroid/Utils.java
@@ -57,6 +57,7 @@ import java.security.cert.CertificateEncodingException;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
+import java.util.ArrayList;
import java.util.Date;
import java.util.Formatter;
import java.util.Iterator;
@@ -270,7 +271,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) {
@@ -457,6 +458,19 @@ public final class Utils {
return splitter.iterator();
}
+ public ArrayList 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/ApkSignatureVerifier.java b/app/src/main/java/org/fdroid/fdroid/installer/ApkSignatureVerifier.java
index 27b6aeb3f..50af70798 100644
--- a/app/src/main/java/org/fdroid/fdroid/installer/ApkSignatureVerifier.java
+++ b/app/src/main/java/org/fdroid/fdroid/installer/ApkSignatureVerifier.java
@@ -42,12 +42,12 @@ public class ApkSignatureVerifier {
private static final String TAG = "ApkSignatureVerifier";
- private final Context mContext;
- private final PackageManager mPm;
+ private final Context context;
+ private final PackageManager pm;
ApkSignatureVerifier(Context context) {
- mContext = context;
- mPm = context.getPackageManager();
+ this.context = context;
+ pm = context.getPackageManager();
}
public boolean hasFDroidSignature(File apkFile) {
@@ -66,7 +66,7 @@ public class ApkSignatureVerifier {
private byte[] getApkSignature(File apkFile) {
final String pkgPath = apkFile.getAbsolutePath();
- PackageInfo pkgInfo = mPm.getPackageArchiveInfo(pkgPath, PackageManager.GET_SIGNATURES);
+ PackageInfo pkgInfo = pm.getPackageArchiveInfo(pkgPath, PackageManager.GET_SIGNATURES);
return signatureToBytes(pkgInfo.signatures);
}
@@ -74,7 +74,7 @@ public class ApkSignatureVerifier {
try {
// we do check the byte array of *all* signatures
@SuppressLint("PackageManagerGetSignatures")
- PackageInfo pkgInfo = mPm.getPackageInfo(mContext.getPackageName(),
+ PackageInfo pkgInfo = pm.getPackageInfo(context.getPackageName(),
PackageManager.GET_SIGNATURES);
return signatureToBytes(pkgInfo.signatures);
} catch (PackageManager.NameNotFoundException e) {
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..c6d73654f 100644
--- a/app/src/main/java/org/fdroid/fdroid/installer/DefaultInstaller.java
+++ b/app/src/main/java/org/fdroid/fdroid/installer/DefaultInstaller.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,82 +19,82 @@
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 android.util.Log;
+
+import org.fdroid.fdroid.Utils;
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.
+ * The default installer of F-Droid. It uses the normal Intents APIs of Android
+ * to install apks. Its main inner workings are encapsulated in DefaultInstallerActivity.
+ *
+ * This is installer requires user interaction and thus install/uninstall directly
+ * return PendingIntents.
*/
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, "DefaultInstaller uri: " + uri + " file: " + new File(uri.getPath()));
+
+ Uri sanitizedUri;
try {
- mActivity.startActivityForResult(intent, REQUEST_CODE_INSTALL);
- } catch (ActivityNotFoundException e) {
- throw new InstallFailedException(e);
+ sanitizedUri = Installer.prepareApkFile(context, uri, packageName);
+ } catch (Installer.InstallFailedException e) {
+ Log.e(TAG, "prepareApkFile failed", e);
+ sendBroadcastInstall(uri, originatingUri, Installer.ACTION_INSTALL_INTERRUPTED,
+ e.getMessage());
+ return;
}
+
+ Intent installIntent = new Intent(context, DefaultInstallerActivity.class);
+ installIntent.setAction(DefaultInstallerActivity.ACTION_INSTALL_PACKAGE);
+ installIntent.putExtra(DefaultInstallerActivity.EXTRA_ORIGINATING_URI, originatingUri);
+ installIntent.setData(sanitizedUri);
+
+ PendingIntent installPendingIntent = PendingIntent.getActivity(
+ context.getApplicationContext(),
+ uri.hashCode(),
+ installIntent,
+ PendingIntent.FLAG_UPDATE_CURRENT);
+
+ sendBroadcastInstall(uri, originatingUri,
+ Installer.ACTION_INSTALL_USER_INTERACTION, installPendingIntent);
}
@Override
- protected void deletePackageInternal(String packageName) throws InstallFailedException {
- try {
- PackageInfo pkgInfo = mPm.getPackageInfo(packageName, 0);
+ protected void uninstallPackage(String packageName) {
+ sendBroadcastUninstall(packageName, Installer.ACTION_UNINSTALL_STARTED);
- 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);
- }
- } catch (PackageManager.NameNotFoundException e) {
- // already checked in super class
- }
+ Intent uninstallIntent = new Intent(context, DefaultInstallerActivity.class);
+ uninstallIntent.setAction(DefaultInstallerActivity.ACTION_UNINSTALL_PACKAGE);
+ uninstallIntent.putExtra(
+ DefaultInstallerActivity.EXTRA_UNINSTALL_PACKAGE_NAME, packageName);
+ PendingIntent uninstallPendingIntent = PendingIntent.getActivity(
+ context.getApplicationContext(),
+ packageName.hashCode(),
+ uninstallIntent,
+ PendingIntent.FLAG_UPDATE_CURRENT);
+
+ sendBroadcastUninstall(packageName,
+ Installer.ACTION_UNINSTALL_USER_INTERACTION, uninstallPendingIntent);
}
@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);
-
- return true;
- case REQUEST_CODE_DELETE:
- mCallback.onSuccess(InstallerCallback.OPERATION_DELETE);
-
- return true;
- default:
- return false;
- }
+ protected boolean isUnattended() {
+ return false;
}
}
diff --git a/app/src/main/java/org/fdroid/fdroid/installer/DefaultInstallerActivity.java b/app/src/main/java/org/fdroid/fdroid/installer/DefaultInstallerActivity.java
new file mode 100644
index 000000000..6e1e64ff4
--- /dev/null
+++ b/app/src/main/java/org/fdroid/fdroid/installer/DefaultInstallerActivity.java
@@ -0,0 +1,241 @@
+/*
+ * 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.util.Log;
+
+import org.fdroid.fdroid.R;
+
+/**
+ * A transparent activity as a wrapper around Android's PackageInstaller Intents
+ */
+public class DefaultInstallerActivity 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 Uri installOriginatingUri;
+ private Uri installUri;
+
+ private String uninstallPackageName;
+
+ // for the broadcasts
+ private DefaultInstaller installer;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ installer = new DefaultInstaller(this);
+
+ Intent intent = getIntent();
+ String action = intent.getAction();
+ if (ACTION_INSTALL_PACKAGE.equals(action)) {
+ installUri = intent.getData();
+ installOriginatingUri = intent.getParcelableExtra(EXTRA_ORIGINATING_URI);
+
+ installPackage(installUri, installOriginatingUri);
+ } else if (ACTION_UNINSTALL_PACKAGE.equals(action)) {
+ uninstallPackageName = intent.getStringExtra(EXTRA_UNINSTALL_PACKAGE_NAME);
+
+ uninstallPackage(uninstallPackageName);
+ } else {
+ throw new IllegalStateException("Intent action not specified!");
+ }
+ }
+
+ @SuppressLint("InlinedApi")
+ private void installPackage(Uri uri, Uri originatingUri) {
+ 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);
+ installer.sendBroadcastInstall(uri, originatingUri, Installer.ACTION_INSTALL_INTERRUPTED,
+ "This Android rom does not support ACTION_INSTALL_PACKAGE!");
+ finish();
+ }
+ installer.sendBroadcastInstall(installUri, installOriginatingUri,
+ Installer.ACTION_INSTALL_STARTED);
+ }
+
+ protected void uninstallPackage(String packageName) {
+ // check that the package is installed
+ try {
+ getPackageManager().getPackageInfo(packageName, 0);
+ } catch (PackageManager.NameNotFoundException e) {
+ Log.e(TAG, "NameNotFoundException", e);
+ installer.sendBroadcastUninstall(packageName, Installer.ACTION_UNINSTALL_INTERRUPTED,
+ "Package that is scheduled for uninstall is not installed!");
+ finish();
+ return;
+ }
+
+ Uri uri = Uri.fromParts("package", packageName, null);
+ Intent intent = new Intent();
+ 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);
+ installer.sendBroadcastUninstall(packageName, Installer.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) {
+ installer.sendBroadcastInstall(installUri, installOriginatingUri,
+ Installer.ACTION_INSTALL_COMPLETE);
+ break;
+ }
+
+ // Fallback on N for https://gitlab.com/fdroid/fdroidclient/issues/631
+ if ("N".equals(Build.VERSION.CODENAME)) {
+ installer.sendBroadcastInstall(installUri, installOriginatingUri,
+ Installer.ACTION_INSTALL_COMPLETE);
+ break;
+ }
+
+ switch (resultCode) {
+ case Activity.RESULT_OK:
+ installer.sendBroadcastInstall(installUri, installOriginatingUri,
+ Installer.ACTION_INSTALL_COMPLETE);
+ break;
+ case Activity.RESULT_CANCELED:
+ installer.sendBroadcastInstall(installUri, installOriginatingUri,
+ Installer.ACTION_INSTALL_INTERRUPTED);
+ break;
+ case Activity.RESULT_FIRST_USER:
+ default:
+ // AOSP returns Activity.RESULT_FIRST_USER on error
+ installer.sendBroadcastInstall(installUri, installOriginatingUri,
+ Installer.ACTION_INSTALL_INTERRUPTED,
+ getString(R.string.install_error_unknown));
+ 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) {
+ installer.sendBroadcastUninstall(uninstallPackageName,
+ Installer.ACTION_UNINSTALL_COMPLETE);
+ break;
+ }
+
+ switch (resultCode) {
+ case Activity.RESULT_OK:
+ installer.sendBroadcastUninstall(uninstallPackageName,
+ Installer.ACTION_UNINSTALL_COMPLETE);
+ break;
+ case Activity.RESULT_CANCELED:
+ installer.sendBroadcastUninstall(uninstallPackageName,
+ Installer.ACTION_UNINSTALL_INTERRUPTED);
+ break;
+ case Activity.RESULT_FIRST_USER:
+ default:
+ // AOSP UninstallAppProgress returns RESULT_FIRST_USER on error
+ installer.sendBroadcastUninstall(uninstallPackageName,
+ Installer.ACTION_UNINSTALL_INTERRUPTED,
+ getString(R.string.uninstall_error_unknown));
+ break;
+ }
+
+ break;
+ default:
+ throw new RuntimeException("Invalid request code!");
+ }
+
+ // after doing the broadcasts, finish this transparent wrapper activity
+ finish();
+ }
+
+}
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/ErrorDialogActivity.java b/app/src/main/java/org/fdroid/fdroid/installer/ErrorDialogActivity.java
new file mode 100644
index 000000000..9e38794e1
--- /dev/null
+++ b/app/src/main/java/org/fdroid/fdroid/installer/ErrorDialogActivity.java
@@ -0,0 +1,70 @@
+/*
+ * 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.Activity;
+import android.content.DialogInterface;
+import android.content.Intent;
+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;
+
+public class ErrorDialogActivity extends FragmentActivity {
+
+ public static final String EXTRA_TITLE = "title";
+ public static final String EXTRA_MESSAGE = "message";
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ final Intent intent = getIntent();
+ final String title = intent.getStringExtra(EXTRA_TITLE);
+ final String message = intent.getStringExtra(EXTRA_MESSAGE);
+
+ // 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(title);
+ builder.setNeutralButton(android.R.string.ok,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ setResult(Activity.RESULT_OK);
+ finish();
+ }
+ });
+ builder.setOnCancelListener(
+ new DialogInterface.OnCancelListener() {
+ @Override
+ public void onCancel(DialogInterface dialog) {
+ setResult(Activity.RESULT_CANCELED);
+ finish();
+ }
+ });
+ builder.setMessage(message);
+ builder.create().show();
+ }
+}
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..970b8f99f
--- /dev/null
+++ b/app/src/main/java/org/fdroid/fdroid/installer/ExtensionInstaller.java
@@ -0,0 +1,108 @@
+/*
+ * 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
+ * as a privileged app into the system partition of Android.
+ *
+ * This is installer requires user interaction and thus install/uninstall directly
+ * return PendingIntents.
+ */
+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) {
+ Uri sanitizedUri;
+ try {
+ sanitizedUri = Installer.prepareApkFile(context, uri, packageName);
+ } catch (InstallFailedException e) {
+ Log.e(TAG, "prepareApkFile failed", e);
+ sendBroadcastInstall(uri, originatingUri, Installer.ACTION_INSTALL_INTERRUPTED,
+ e.getMessage());
+ 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(context);
+ if (!BuildConfig.DEBUG && !signatureVerifier.hasFDroidSignature(new File(sanitizedUri.getPath()))) {
+ sendBroadcastInstall(uri, originatingUri, Installer.ACTION_INSTALL_INTERRUPTED,
+ "APK signature of extension not correct!");
+ }
+ Intent installIntent = new Intent(context, InstallExtensionDialogActivity.class);
+ installIntent.setAction(InstallExtensionDialogActivity.ACTION_INSTALL);
+ installIntent.setData(sanitizedUri);
+
+ PendingIntent installPendingIntent = PendingIntent.getActivity(
+ context.getApplicationContext(),
+ uri.hashCode(),
+ installIntent,
+ PendingIntent.FLAG_UPDATE_CURRENT);
+
+ 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 = new Intent(context, InstallExtensionDialogActivity.class);
+ uninstallIntent.setAction(InstallExtensionDialogActivity.ACTION_UNINSTALL);
+
+ PendingIntent uninstallPendingIntent = PendingIntent.getActivity(
+ context.getApplicationContext(),
+ packageName.hashCode(),
+ uninstallIntent,
+ PendingIntent.FLAG_UPDATE_CURRENT);
+
+ 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);
+ }
+
+ @Override
+ protected boolean isUnattended() {
+ return false;
+ }
+}
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..9903ad188 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;
@@ -85,18 +86,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;
@@ -180,7 +169,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);
}
@@ -234,22 +223,26 @@ 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);
+ 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);
}
@@ -265,6 +258,70 @@ 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) {
+ Uri originatingUri = intent.getParcelableExtra(Installer.EXTRA_ORIGINATING_URI);
+
+ switch (intent.getAction()) {
+ case Installer.ACTION_INSTALL_STARTED:
+ // nothing to do
+ break;
+ case Installer.ACTION_INSTALL_COMPLETE:
+ Apk apkComplete = removeFromActive(originatingUri.toString());
+
+ PackageManagerCompat.setInstaller(getPackageManager(), apkComplete.packageName);
+
+ localBroadcastManager.unregisterReceiver(this);
+ break;
+ case Installer.ACTION_INSTALL_INTERRUPTED:
+ String errorMessage =
+ intent.getStringExtra(Installer.EXTRA_ERROR_MESSAGE);
+
+ // show notification if app details is not visible
+ if (!TextUtils.isEmpty(errorMessage)) {
+ App app = getAppFromActive(originatingUri.toString());
+ String title = String.format(
+ getString(R.string.install_error_notify_title),
+ app.name);
+
+ // show notification if app details is not visible
+ if (AppDetails.isAppVisible(app.packageName)) {
+ cancelNotification(originatingUri.toString());
+ } else {
+ notifyError(originatingUri.toString(), title, errorMessage);
+ }
+ }
+
+ localBroadcastManager.unregisterReceiver(this);
+ break;
+ case Installer.ACTION_INSTALL_USER_INTERACTION:
+ PendingIntent installPendingIntent =
+ intent.getParcelableExtra(Installer.EXTRA_USER_INTERACTION_PI);
+
+ Apk apkUserInteraction = getApkFromActive(originatingUri.toString());
+ // show notification if app details is not visible
+ if (AppDetails.isAppVisible(apkUserInteraction.packageName)) {
+ cancelNotification(originatingUri.toString());
+ } else {
+ notifyDownloadComplete(apkUserInteraction, originatingUri.toString(), installPendingIntent);
+ }
+
+ break;
+ default:
+ throw new RuntimeException("intent action not handled!");
+ }
+ }
+ };
+
+ localBroadcastManager.registerReceiver(installReceiver,
+ Installer.getInstallIntentFilter(uri));
}
private NotificationCompat.Builder createNotificationBuilder(String urlString, Apk apk) {
@@ -273,7 +330,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)
@@ -281,18 +338,8 @@ public class InstallManagerService extends Service {
.setProgress(100, 0, true);
}
- 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;
- }
+ private String getAppName(Apk apk) {
+ return ACTIVE_APPS.get(apk.packageName).name;
}
/**
@@ -319,14 +366,14 @@ 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();
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();
@@ -335,13 +382,38 @@ 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();
notificationManager.notify(downloadUrlId, notification);
}
+ private void notifyError(String urlString, String title, String text) {
+ int downloadUrlId = urlString.hashCode();
+
+ Intent errorDialogIntent = new Intent(this, ErrorDialogActivity.class);
+ errorDialogIntent.putExtra(
+ ErrorDialogActivity.EXTRA_TITLE, title);
+ errorDialogIntent.putExtra(
+ ErrorDialogActivity.EXTRA_MESSAGE, text);
+ PendingIntent errorDialogPendingIntent = PendingIntent.getActivity(
+ getApplicationContext(),
+ downloadUrlId,
+ errorDialogIntent,
+ PendingIntent.FLAG_UPDATE_CURRENT);
+
+ NotificationCompat.Builder builder =
+ new NotificationCompat.Builder(this)
+ .setAutoCancel(true)
+ .setContentTitle(title)
+ .setContentIntent(errorDialogPendingIntent)
+ .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()}
@@ -354,7 +426,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 getApkFromActive(String urlString) {
+ return ACTIVE_APKS.get(urlString);
}
/**
@@ -364,6 +439,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 f9800f74f..b1ed0ff13 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,24 +19,26 @@
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 org.fdroid.fdroid.privileged.views.UninstallDialogActivity;
import java.io.File;
import java.io.IOException;
@@ -44,22 +46,32 @@ 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;
+ final Context context;
+ final PackageManager pm;
+ final 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.Installer.extra.ORIGINATING_URI";
+ public static final String EXTRA_PACKAGE_NAME = "org.fdroid.fdroid.installer.Installer.extra.PACKAGE_NAME";
+ public static final String EXTRA_USER_INTERACTION_PI = "org.fdroid.fdroid.installer.Installer.extra.USER_INTERACTION_PI";
+ public static final String EXTRA_ERROR_MESSAGE = "org.fdroid.fdroid.net.installer.Installer.extra.ERROR_MESSAGE";
+
public static class InstallFailedException extends Exception {
private static final long serialVersionUID = -8343133906463328027L;
@@ -73,116 +85,31 @@ 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.context = context;
+ this.pm = context.getPackageManager();
+ localBroadcastManager = LocalBroadcastManager.getInstance(context);
}
- Installer(Context context, PackageManager pm, InstallerCallback callback)
+ public static Uri prepareApkFile(Context context, Uri uri, String packageName)
throws InstallFailedException {
- this.mContext = context;
- this.mPm = pm;
- this.mCallback = callback;
- }
- public static Installer getActivityInstaller(Activity activity, InstallerCallback callback) {
- return getActivityInstaller(activity, activity.getPackageManager(), callback);
- }
+ File apkFile = new File(uri.getPath());
- /**
- * 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)
- throws InstallFailedException {
- SanitizedFile apkToInstall = null;
+ 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(mContext, packageName, versionCode, new String[]{
+ Apk apk = ApkProvider.Helper.find(context, packageName, versionCode, new String[]{
ApkProvider.DataColumns.HASH,
ApkProvider.DataColumns.HASH_TYPE,
});
@@ -190,50 +117,29 @@ 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()));
- FileUtils.copyFile(apkFile, apkToInstall);
- if (!verifyApkFile(apkToInstall, apk.hash, apk.hashType)) {
+ sanitizedApkFile = SanitizedFile.knownSanitized(File.createTempFile("install-", ".apk",
+ context.getFilesDir()));
+ FileUtils.copyFile(apkFile, sanitizedApkFile);
+ if (!verifyApkFile(sanitizedApkFile, apk.hash, apk.hashType)) {
FileUtils.deleteQuietly(apkFile);
throw new 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(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);
+ sanitizedApkFile.setReadable(true, false);
- 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!");
} 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() {
@@ -248,41 +154,168 @@ public abstract class Installer {
}
}.start();
}
+
+ return Uri.fromFile(sanitizedApkFile);
}
- 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;
+ /**
+ * Returns permission screen for given apk.
+ *
+ * @param apk instance of Apk
+ * @return Intent with Activity to show required permissions.
+ * Returns null if Installer handles that on itself, e.g., with DefaultInstaller,
+ * or if no new permissions have been introduced during an update
+ */
+ public Intent getPermissionScreen(Apk apk) {
+ if (!isUnattended()) {
+ return null;
}
- // 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;
- }
+ int count = newPermissionCount(apk);
+ if (count > 0) {
+ Uri uri = ApkProvider.getContentUri(apk);
+ Intent intent = new Intent(context, InstallConfirmActivity.class);
+ intent.setData(uri);
- Intent uninstallIntent = new Intent(activity, InstallExtensionDialogActivity.class);
- uninstallIntent.setAction(InstallExtensionDialogActivity.ACTION_UNINSTALL);
- activity.startActivity(uninstallIntent);
- return;
+ return intent;
+ } else {
+ // no permission screen needed!
+ return null;
}
-
- deletePackageInternal(packageName);
}
- protected abstract void installPackageInternal(File apkFile)
- throws InstallFailedException;
+ 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;
+ //}
- protected abstract void deletePackageInternal(String packageName)
- throws InstallFailedException;
+ AppDiff appDiff = new AppDiff(context.getPackageManager(), apk);
+ if (appDiff.mPkgInfo == null) {
+ // could not get diff because we couldn't parse the package
+ throw new RuntimeException("cannot parse!");
+ }
+ AppSecurityPermissions perms = new AppSecurityPermissions(context, appDiff.mPkgInfo);
+ if (appDiff.mInstalledAppInfo != null) {
+ // update to an existing app
+ return perms.getPermissionCount(AppSecurityPermissions.WHICH_NEW);
+ }
+ // new app install
+ return perms.getPermissionCount(AppSecurityPermissions.WHICH_ALL);
+ }
+
+ /**
+ * Returns an Intent to start a dialog wrapped in an activity
+ * for uninstall confirmation.
+ *
+ * @param packageName packageName of app to uninstall
+ * @return Intent with activity for uninstall confirmation
+ * Returns null if Installer handles that on itself, e.g.,
+ * with DefaultInstaller.
+ */
+ public Intent getUninstallScreen(String packageName) {
+ if (!isUnattended()) {
+ return null;
+ }
+
+ Intent intent = new Intent(context, UninstallDialogActivity.class);
+ intent.putExtra(Installer.EXTRA_PACKAGE_NAME, packageName);
+
+ return intent;
+ }
+
+ /**
+ * 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);
+
+ protected abstract boolean isUnattended();
- 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..f2ecd7b6a
--- /dev/null
+++ b/app/src/main/java/org/fdroid/fdroid/installer/InstallerFactory.java
@@ -0,0 +1,77 @@
+/*
+ * 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";
+
+ /**
+ * Returns an instance of an appropriate installer.
+ * Either DefaultInstaller, PrivilegedInstaller, or in the special
+ * case to install the "F-Droid Privileged Extension" ExtensionInstaller.
+ *
+ * @param context current {@link Context}
+ * @param packageName package name of apk to be installed. Required to select
+ * the ExtensionInstaller.
+ * If this is null, the ExtensionInstaller will never be returned.
+ * @return instance of an Installer
+ */
+ public static Installer create(Context context, String packageName) {
+ Installer installer;
+
+ if (packageName != null
+ && packageName.equals(PrivilegedInstaller.PRIVILEGED_EXTENSION_PACKAGE_NAME)) {
+ // special case for "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");
+
+ 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);
+ }
+ } 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
new file mode 100644
index 000000000..1af566fa3
--- /dev/null
+++ b/app/src/main/java/org/fdroid/fdroid/installer/InstallerService.java
@@ -0,0 +1,91 @@
+/*
+ * 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.IntentService;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+
+/**
+ * This service handles the install process of apk files and
+ * uninstall process of apps.
+ *
+ * This service is based on an IntentService because:
+ * - no parallel installs/uninstalls should be allowed,
+ * i.e., runs sequentially
+ * - no cancel operation is needed. Cancelling an installation
+ * would be the same as starting uninstall afterwards
+ */
+public class InstallerService extends IntentService {
+
+ 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";
+
+ public InstallerService() {
+ super("InstallerService");
+ }
+
+ @Override
+ protected void onHandleIntent(Intent intent) {
+ String packageName = intent.getStringExtra(Installer.EXTRA_PACKAGE_NAME);
+ Installer installer = InstallerFactory.create(this, packageName);
+
+ if (ACTION_INSTALL.equals(intent.getAction())) {
+ Uri uri = intent.getData();
+ Uri originatingUri = intent.getParcelableExtra(Installer.EXTRA_ORIGINATING_URI);
+
+ installer.installPackage(uri, originatingUri, packageName);
+ } else if (ACTION_UNINSTALL.equals(intent.getAction())) {
+ installer.uninstallPackage(packageName);
+ }
+ }
+
+ /**
+ * Install an apk from {@link Uri}
+ *
+ * @param context this app's {@link Context}
+ * @param uri {@link Uri} pointing to (downloaded) local apk file
+ * @param originatingUri {@link Uri} where the apk has been downloaded from
+ * @param packageName package name of the app that should be installed
+ */
+ 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(Installer.EXTRA_ORIGINATING_URI, originatingUri);
+ intent.putExtra(Installer.EXTRA_PACKAGE_NAME, packageName);
+ context.startService(intent);
+ }
+
+ /**
+ * Uninstall an app
+ *
+ * @param context this app's {@link Context}
+ * @param packageName package name of the app that will be uninstalled
+ */
+ public static void uninstall(Context context, String packageName) {
+ Intent intent = new Intent(context, InstallerService.class);
+ intent.setAction(ACTION_UNINSTALL);
+ intent.putExtra(Installer.EXTRA_PACKAGE_NAME, packageName);
+ context.startService(intent);
+ }
+
+}
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..24a4001ff 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
@@ -20,49 +20,39 @@
package org.fdroid.fdroid.installer;
-import android.app.Activity;
import android.content.ComponentName;
import android.content.Context;
-import android.content.DialogInterface;
import android.content.Intent;
import android.content.ServiceConnection;
-import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.net.Uri;
-import android.os.Bundle;
import android.os.IBinder;
import android.os.RemoteException;
-import android.support.v7.app.AlertDialog;
import android.util.Log;
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;
+import java.util.HashMap;
/**
- * Installer based on using internal hidden APIs of the Android OS, which are
- * protected by the permissions
- *
- *
android.permission.INSTALL_PACKAGES
- *
android.permission.DELETE_PACKAGES
- *
- *
+ * Installer that only works if the "F-Droid Privileged
+ * Extension" is installed as a privileged app.
+ *
+ * "F-Droid Privileged Extension" provides a service that exposes
+ * internal Android APIs for install/uninstall which are protected
+ * by INSTALL_PACKAGES, DELETE_PACKAGES permissions.
* 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) and cannot be used directly by F-Droid.
+ *
+ * Instead, this installer binds to the service of
+ * "F-Droid Privileged Extension" and then executes the appropriate methods
+ * inside the privileged context of the privileged extension.
+ *
+ * This installer makes unattended installs/uninstalls possible.
+ * Thus no PendingIntents are returned.
+ *
* Sources for Android 4.4 change:
* https://groups.google.com/forum/#!msg/android-
* security-discuss/r7uL_OEMU5c/LijNHvxeV80J
@@ -76,19 +66,195 @@ 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;
+ // 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 INSTALL_RETURN_CODES;
+
+ static {
+ // Descriptions extracted from the source code comments in AOSP
+ INSTALL_RETURN_CODES = new HashMap<>();
+ INSTALL_RETURN_CODES.put(INSTALL_SUCCEEDED,
+ "Success");
+ INSTALL_RETURN_CODES.put(INSTALL_FAILED_ALREADY_EXISTS,
+ "Package is already installed.");
+ INSTALL_RETURN_CODES.put(INSTALL_FAILED_INVALID_APK,
+ "The package archive file is invalid.");
+ INSTALL_RETURN_CODES.put(INSTALL_FAILED_INVALID_URI,
+ "The URI passed in is invalid.");
+ INSTALL_RETURN_CODES.put(INSTALL_FAILED_INSUFFICIENT_STORAGE,
+ "The package manager service found that the device didn't have enough " +
+ "storage space to install the app.");
+ INSTALL_RETURN_CODES.put(INSTALL_FAILED_DUPLICATE_PACKAGE,
+ "A package is already installed with the same name.");
+ INSTALL_RETURN_CODES.put(INSTALL_FAILED_NO_SHARED_USER,
+ "The requested shared user does not exist.");
+ INSTALL_RETURN_CODES.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).");
+ INSTALL_RETURN_CODES.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.");
+ INSTALL_RETURN_CODES.put(INSTALL_FAILED_MISSING_SHARED_LIBRARY,
+ "The new package uses a shared library that is not available.");
+ INSTALL_RETURN_CODES.put(INSTALL_FAILED_REPLACE_COULDNT_DELETE,
+ "Unknown"); // wrong comment in source
+ INSTALL_RETURN_CODES.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.");
+ INSTALL_RETURN_CODES.put(INSTALL_FAILED_OLDER_SDK,
+ "The new package failed because the current SDK version is older than that " +
+ "required by the package.");
+ INSTALL_RETURN_CODES.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.");
+ INSTALL_RETURN_CODES.put(INSTALL_FAILED_NEWER_SDK,
+ "The new package failed because the current SDK version is newer than that " +
+ "required by the package.");
+ INSTALL_RETURN_CODES.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.");
+ INSTALL_RETURN_CODES.put(INSTALL_FAILED_CPU_ABI_INCOMPATIBLE,
+ "The package being installed contains native code, but none that is compatible " +
+ "with the device's CPU_ABI.");
+ INSTALL_RETURN_CODES.put(INSTALL_FAILED_MISSING_FEATURE,
+ "The new package uses a feature that is not available.");
+ INSTALL_RETURN_CODES.put(INSTALL_FAILED_CONTAINER_ERROR,
+ "A secure container mount point couldn't be accessed on external media.");
+ INSTALL_RETURN_CODES.put(INSTALL_FAILED_INVALID_INSTALL_LOCATION,
+ "The new package couldn't be installed in the specified install location.");
+ INSTALL_RETURN_CODES.put(INSTALL_FAILED_MEDIA_UNAVAILABLE,
+ "The new package couldn't be installed in the specified install location " +
+ "because the media is not available.");
+ INSTALL_RETURN_CODES.put(INSTALL_FAILED_VERIFICATION_TIMEOUT,
+ "The new package couldn't be installed because the verification timed out.");
+ INSTALL_RETURN_CODES.put(INSTALL_FAILED_VERIFICATION_FAILURE,
+ "The new package couldn't be installed because the verification did not succeed.");
+ INSTALL_RETURN_CODES.put(INSTALL_FAILED_PACKAGE_CHANGED,
+ "The package changed from what the calling program expected.");
+ INSTALL_RETURN_CODES.put(INSTALL_FAILED_UID_CHANGED,
+ "The new package is assigned a different UID than it previously held.");
+ INSTALL_RETURN_CODES.put(INSTALL_FAILED_VERSION_DOWNGRADE,
+ "The new package has an older version code than the currently installed package.");
+ INSTALL_RETURN_CODES.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.");
+ INSTALL_RETURN_CODES.put(INSTALL_PARSE_FAILED_BAD_MANIFEST,
+ "the parser was unable to retrieve the AndroidManifest.xml file.");
+ INSTALL_RETURN_CODES.put(INSTALL_PARSE_FAILED_UNEXPECTED_EXCEPTION,
+ "The parser encountered an unexpected exception.");
+ INSTALL_RETURN_CODES.put(INSTALL_PARSE_FAILED_NO_CERTIFICATES,
+ "The parser did not find any certificates in the .apk.");
+ INSTALL_RETURN_CODES.put(INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES,
+ "The parser found inconsistent certificates on the files in the .apk.");
+ INSTALL_RETURN_CODES.put(INSTALL_PARSE_FAILED_CERTIFICATE_ENCODING,
+ "The parser encountered a CertificateEncodingException in one of the files in " +
+ "the .apk.");
+ INSTALL_RETURN_CODES.put(INSTALL_PARSE_FAILED_BAD_PACKAGE_NAME,
+ "The parser encountered a bad or missing package name in the manifest.");
+ INSTALL_RETURN_CODES.put(INSTALL_PARSE_FAILED_BAD_SHARED_USER_ID,
+ "The parser encountered a bad shared user id name in the manifest.");
+ INSTALL_RETURN_CODES.put(INSTALL_PARSE_FAILED_MANIFEST_MALFORMED,
+ "The parser encountered some structural problem in the manifest.");
+ INSTALL_RETURN_CODES.put(INSTALL_PARSE_FAILED_MANIFEST_EMPTY,
+ "The parser did not find any actionable tags (instrumentation or application) " +
+ "in the manifest.");
+ INSTALL_RETURN_CODES.put(INSTALL_FAILED_INTERNAL_ERROR,
+ "The system failed to install the package because of system issues.");
+ INSTALL_RETURN_CODES.put(INSTALL_FAILED_USER_RESTRICTED,
+ "The system failed to install the package because the user is restricted from " +
+ "installing apps.");
+ INSTALL_RETURN_CODES.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.");
+ INSTALL_RETURN_CODES.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 UNINSTALL_RETURN_CODES;
+
+ static {
+ // Descriptions extracted from the source code comments in AOSP
+ UNINSTALL_RETURN_CODES = new HashMap<>();
+ UNINSTALL_RETURN_CODES.put(DELETE_SUCCEEDED,
+ "Success");
+ UNINSTALL_RETURN_CODES.put(DELETE_FAILED_INTERNAL_ERROR,
+ " the system failed to delete the package for an unspecified reason.");
+ UNINSTALL_RETURN_CODES.put(DELETE_FAILED_DEVICE_POLICY_MANAGER,
+ "the system failed to delete the package because it is the active " +
+ "DevicePolicy manager.");
+ UNINSTALL_RETURN_CODES.put(DELETE_FAILED_USER_RESTRICTED,
+ "the system failed to delete the package since the user is restricted.");
+ UNINSTALL_RETURN_CODES.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);
}
public static boolean isExtensionInstalled(Context context) {
@@ -102,29 +268,14 @@ public class PrivilegedInstaller extends Installer {
}
public static int isExtensionInstalledCorrectly(Context context) {
-
// check if installed
if (!isExtensionInstalled(context)) {
+ Log.e(TAG, "IS_EXTENSION_INSTALLED_NO");
return IS_EXTENSION_INSTALLED_NO;
}
- // check if it has the privileged permissions granted
- final Object mutex = new Object();
- final Bundle returnBundle = new Bundle();
- ServiceConnection mServiceConnection = new ServiceConnection() {
+ ServiceConnection serviceConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName name, IBinder service) {
- IPrivilegedService privService = IPrivilegedService.Stub.asInterface(service);
-
- try {
- boolean hasPermissions = privService.hasPrivilegedPermissions();
- returnBundle.putBoolean("has_permissions", hasPermissions);
- } catch (RemoteException e) {
- Log.e(TAG, "RemoteException", e);
- }
-
- synchronized (mutex) {
- mutex.notify();
- }
}
public void onServiceDisconnected(ComponentName name) {
@@ -133,68 +284,32 @@ public class PrivilegedInstaller extends Installer {
Intent serviceIntent = new Intent(PRIVILEGED_EXTENSION_SERVICE_INTENT);
serviceIntent.setPackage(PRIVILEGED_EXTENSION_PACKAGE_NAME);
+ // try to connect to check for signature
try {
- context.getApplicationContext().bindService(serviceIntent, mServiceConnection,
+ context.getApplicationContext().bindService(serviceIntent, serviceConnection,
Context.BIND_AUTO_CREATE);
} catch (SecurityException e) {
+ Log.e(TAG, "IS_EXTENSION_INSTALLED_SIGNATURE_PROBLEM", e);
return IS_EXTENSION_INSTALLED_SIGNATURE_PROBLEM;
}
- synchronized (mutex) {
- try {
- mutex.wait(3000);
- } catch (InterruptedException e) {
- // don't care
- }
- }
-
- boolean hasPermissions = returnBundle.getBoolean("has_permissions", false);
- if (!hasPermissions) {
- return IS_EXTENSION_INSTALLED_PERMISSIONS_PROBLEM;
- }
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);
+ protected void installPackage(final Uri uri, final Uri originatingUri, String packageName) {
+ sendBroadcastInstall(uri, originatingUri, Installer.ACTION_INSTALL_STARTED);
+
+ final Uri sanitizedUri;
+ try {
+ sanitizedUri = Installer.prepareApkFile(context, uri, packageName);
+ } catch (Installer.InstallFailedException e) {
+ Log.e(TAG, "prepareApkFile failed", e);
+ sendBroadcastInstall(uri, originatingUri, Installer.ACTION_INSTALL_INTERRUPTED,
+ e.getMessage());
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);
- }
- }
- }
- 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);
@@ -202,22 +317,30 @@ 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");
- 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,
+ "Error " + returnCode + ": "
+ + INSTALL_RETURN_CODES.get(returnCode));
}
}
};
try {
- privService.installPackage(packageURI, INSTALL_REPLACE_EXISTING, null, callback);
+ boolean hasPermissions = privService.hasPrivilegedPermissions();
+ if (!hasPermissions) {
+ sendBroadcastInstall(uri, originatingUri, ACTION_INSTALL_INTERRUPTED,
+ context.getString(R.string.system_install_denied_permissions));
+ return;
+ }
+
+ 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");
}
}
@@ -227,69 +350,14 @@ public class PrivilegedInstaller extends Installer {
Intent serviceIntent = new Intent(PRIVILEGED_EXTENSION_SERVICE_INTENT);
serviceIntent.setPackage(PRIVILEGED_EXTENSION_PACKAGE_NAME);
- mContext.getApplicationContext().bindService(serviceIntent, mServiceConnection,
+ context.getApplicationContext().bindService(serviceIntent, mServiceConnection,
Context.BIND_AUTO_CREATE);
}
@Override
- protected void deletePackageInternal(final String packageName)
- throws InstallFailedException {
- ApplicationInfo appInfo;
- try {
- //noinspection WrongConstant (lint is actually wrong here!)
- appInfo = mPm.getApplicationInfo(packageName, PackageManager.GET_UNINSTALLED_PACKAGES);
- } catch (PackageManager.NameNotFoundException e) {
- Log.w(TAG, "Failed to get ApplicationInfo for uninstalling");
- return;
- }
+ protected void uninstallPackage(final String packageName) {
+ sendBroadcastUninstall(packageName, Installer.ACTION_UNINSTALL_STARTED);
- 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
- mCallback.onError(InstallerCallback.OPERATION_DELETE,
- InstallerCallback.ERROR_CODE_OTHER);
- return;
- }
-
- int messageId;
- if (isUpdate) {
- messageId = R.string.uninstall_update_confirm;
- } else {
- messageId = R.string.uninstall_confirm;
- }
-
- final AlertDialog.Builder builder = new AlertDialog.Builder(mContext);
- builder.setTitle(appInfo.loadLabel(mPm));
- builder.setIcon(appInfo.loadIcon(mPm));
- builder.setPositiveButton(android.R.string.ok,
- new DialogInterface.OnClickListener() {
- @Override
- public void onClick(DialogInterface dialog, int which) {
- try {
- doDeletePackageInternal(packageName);
- } catch (InstallFailedException e) {
- mCallback.onError(InstallerCallback.OPERATION_DELETE,
- InstallerCallback.ERROR_CODE_OTHER);
- }
- }
- });
- builder.setNegativeButton(android.R.string.cancel,
- new DialogInterface.OnClickListener() {
- @Override
- public void onClick(DialogInterface dialog, int which) {
- dialog.cancel();
- mCallback.onError(InstallerCallback.OPERATION_DELETE,
- InstallerCallback.ERROR_CODE_CANCELED);
- }
- });
- builder.setMessage(messageId);
- builder.create().show();
- }
-
- private void doDeletePackageInternal(final String packageName)
- throws InstallFailedException {
ServiceConnection mServiceConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName name, IBinder service) {
IPrivilegedService privService = IPrivilegedService.Stub.asInterface(service);
@@ -297,23 +365,29 @@ 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");
-
- 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,
+ "Error " + returnCode + ": "
+ + UNINSTALL_RETURN_CODES.get(returnCode));
}
}
};
try {
+ boolean hasPermissions = privService.hasPrivilegedPermissions();
+ if (!hasPermissions) {
+ sendBroadcastUninstall(packageName, ACTION_UNINSTALL_INTERRUPTED,
+ context.getString(R.string.system_install_denied_permissions));
+ return;
+ }
+
privService.deletePackage(packageName, 0, callback);
} catch (RemoteException e) {
Log.e(TAG, "RemoteException", e);
+ sendBroadcastUninstall(packageName, ACTION_UNINSTALL_INTERRUPTED,
+ "connecting to privileged service failed");
}
}
@@ -323,389 +397,13 @@ public class PrivilegedInstaller extends Installer {
Intent serviceIntent = new Intent(PRIVILEGED_EXTENSION_SERVICE_INTENT);
serviceIntent.setPackage(PRIVILEGED_EXTENSION_PACKAGE_NAME);
- mContext.getApplicationContext().bindService(serviceIntent, mServiceConnection,
+ context.getApplicationContext().bindService(serviceIntent, mServiceConnection,
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;
- }
+ protected boolean isUnattended() {
+ return true;
}
- 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;
-
}
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..02d4cf6be 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
@@ -27,6 +27,7 @@ import android.app.ProgressDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
+import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.support.v4.app.FragmentActivity;
@@ -43,6 +44,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,13 +56,12 @@ 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";
public static final String ACTION_FIRST_TIME = "first_time";
- private String apkFile;
+ private String apkPath;
@Override
protected void onCreate(Bundle savedInstanceState) {
@@ -73,7 +75,11 @@ public class InstallExtensionDialogActivity extends FragmentActivity {
return;
}
- apkFile = getIntent().getStringExtra(EXTRA_INSTALL_APK);
+ Uri dataUri = getIntent().getData();
+ if (dataUri != null) {
+ File apkFile = new File(dataUri.getPath());
+ apkPath = apkFile.getAbsolutePath();
+ }
switch (getIntent().getAction()) {
case ACTION_UNINSTALL:
@@ -105,7 +111,6 @@ public class InstallExtensionDialogActivity extends FragmentActivity {
runFirstTime(context);
break;
- case PrivilegedInstaller.IS_EXTENSION_INSTALLED_PERMISSIONS_PROBLEM:
case PrivilegedInstaller.IS_EXTENSION_INSTALLED_SIGNATURE_PROBLEM:
default:
// do nothing
@@ -334,7 +339,7 @@ public class InstallExtensionDialogActivity extends FragmentActivity {
@Override
protected Void doInBackground(Void... voids) {
- InstallExtension.create(getApplicationContext()).runInstall(apkFile);
+ InstallExtension.create(getApplicationContext()).runInstall(apkPath);
return null;
}
};
@@ -369,12 +374,6 @@ public class InstallExtensionDialogActivity extends FragmentActivity {
"\n\n" + getString(R.string.system_install_denied_signature);
result = Activity.RESULT_CANCELED;
break;
- case PrivilegedInstaller.IS_EXTENSION_INSTALLED_PERMISSIONS_PROBLEM:
- title = getString(R.string.system_install_post_fail);
- message = getString(R.string.system_install_post_fail_message) +
- "\n\n" + getString(R.string.system_install_denied_permissions);
- result = Activity.RESULT_CANCELED;
- break;
default:
throw new RuntimeException("unhandled return");
}
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..03bb77592 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,30 @@ public class AppDiff {
public ApplicationInfo mInstalledAppInfo;
+ /**
+ * Constructor based on F-Droids Apk object
+ */
+ public AppDiff(PackageManager mPm, Apk apk) {
+ this.mPm = mPm;
+
+ mPkgInfo = new PackageInfo();
+ mPkgInfo.packageName = apk.packageName;
+ mPkgInfo.applicationInfo = new ApplicationInfo();
+
+ if (apk.permissions == null) {
+ mPkgInfo.requestedPermissions = null;
+ } else {
+ // 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 +86,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..e6a45238b 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,15 @@
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,45 +37,62 @@ 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;
private Intent intent;
- private PackageManager mPm;
-
- private AppDiff mAppDiff;
+ private AppDiff appDiff;
// View for install progress
- private View mInstallConfirm;
+ private View installConfirm;
// Buttons to indicate user acceptance
- private Button mOk;
- private Button mCancel;
- private CaffeinatedScrollView mScrollView;
- private boolean mOkCanInstall;
+ private Button okButton;
+ private Button cancelButton;
+ private CaffeinatedScrollView scrollView;
+ private boolean okCanInstall;
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);
@@ -87,27 +103,27 @@ public class InstallConfirmActivity extends Activity implements OnCancelListener
});
boolean permVisible = false;
- mScrollView = null;
- mOkCanInstall = false;
+ scrollView = null;
+ okCanInstall = false;
int msg = 0;
- AppSecurityPermissions perms = new AppSecurityPermissions(this, mAppDiff.mPkgInfo);
- if (mAppDiff.mInstalledAppInfo != null) {
- msg = (mAppDiff.mInstalledAppInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0
+ AppSecurityPermissions perms = new AppSecurityPermissions(this, appDiff.mPkgInfo);
+ if (appDiff.mInstalledAppInfo != null) {
+ msg = (appDiff.mInstalledAppInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0
? R.string.install_confirm_update_system
: R.string.install_confirm_update;
- mScrollView = new CaffeinatedScrollView(this);
- mScrollView.setFillViewport(true);
+ scrollView = new CaffeinatedScrollView(this);
+ scrollView.setFillViewport(true);
final boolean newPermissionsFound =
perms.getPermissionCount(AppSecurityPermissions.WHICH_NEW) > 0;
if (newPermissionsFound) {
permVisible = true;
- mScrollView.addView(perms.getPermissionsView(
+ scrollView.addView(perms.getPermissionsView(
AppSecurityPermissions.WHICH_NEW));
} else {
throw new RuntimeException("This should not happen. No new permissions were found but InstallConfirmActivity has been started!");
}
adapter.addTab(tabHost.newTabSpec(TAB_ID_NEW).setIndicator(
- getText(R.string.newPerms)), mScrollView);
+ getText(R.string.newPerms)), scrollView);
} else {
findViewById(R.id.tabscontainer).setVisibility(View.GONE);
findViewById(R.id.divider).setVisibility(View.VISIBLE);
@@ -118,8 +134,8 @@ public class InstallConfirmActivity extends Activity implements OnCancelListener
LayoutInflater inflater = (LayoutInflater) getSystemService(
Context.LAYOUT_INFLATER_SERVICE);
View root = inflater.inflate(R.layout.permissions_list, null);
- if (mScrollView == null) {
- mScrollView = (CaffeinatedScrollView) root.findViewById(R.id.scrollview);
+ if (scrollView == null) {
+ scrollView = (CaffeinatedScrollView) root.findViewById(R.id.scrollview);
}
final ViewGroup permList = (ViewGroup) root.findViewById(R.id.permission_list);
permList.addView(perms.getPermissionsView(AppSecurityPermissions.WHICH_ALL));
@@ -128,40 +144,40 @@ public class InstallConfirmActivity extends Activity implements OnCancelListener
}
if (!permVisible) {
- if (mAppDiff.mInstalledAppInfo != null) {
+ if (appDiff.mInstalledAppInfo != null) {
// This is an update to an application, but there are no
// permissions at all.
- msg = (mAppDiff.mInstalledAppInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0
+ msg = (appDiff.mInstalledAppInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0
? R.string.install_confirm_update_system_no_perms
: 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);
findViewById(R.id.divider).setVisibility(View.GONE);
- mScrollView = null;
+ scrollView = null;
}
if (msg != 0) {
((TextView) findViewById(R.id.install_confirm)).setText(msg);
}
- mInstallConfirm.setVisibility(View.VISIBLE);
- mOk = (Button) findViewById(R.id.ok_button);
- mCancel = (Button) findViewById(R.id.cancel_button);
- mOk.setOnClickListener(this);
- mCancel.setOnClickListener(this);
- if (mScrollView == null) {
+ installConfirm.setVisibility(View.VISIBLE);
+ okButton = (Button) findViewById(R.id.ok_button);
+ cancelButton = (Button) findViewById(R.id.cancel_button);
+ okButton.setOnClickListener(this);
+ cancelButton.setOnClickListener(this);
+ if (scrollView == null) {
// There is nothing to scroll view, so the ok button is immediately
// set to install.
- mOk.setText(R.string.menu_install);
- mOkCanInstall = true;
+ okButton.setText(R.string.menu_install);
+ okCanInstall = true;
} else {
- mScrollView.setFullScrollAction(new Runnable() {
+ scrollView.setFullScrollAction(new Runnable() {
@Override
public void run() {
- mOk.setText(R.string.menu_install);
- mOkCanInstall = true;
+ okButton.setText(R.string.menu_install);
+ okCanInstall = true;
}
});
}
@@ -171,22 +187,28 @@ public class InstallConfirmActivity extends Activity implements OnCancelListener
protected void onCreate(Bundle icicle) {
super.onCreate(icicle);
- ((FDroidApp) getApplication()).applyTheme(this);
-
- mPm = getPackageManager();
+ ((FDroidApp) getApplication()).applyDialogTheme(this);
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);
- if (mAppDiff.mPkgInfo == null) {
+ appDiff = new AppDiff(getPackageManager(), apk);
+ if (appDiff.mPkgInfo == null) {
setResult(RESULT_CANNOT_PARSE, intent);
finish();
}
setContentView(R.layout.install_start);
- mInstallConfirm = findViewById(R.id.install_confirm_panel);
- mInstallConfirm.setVisibility(View.INVISIBLE);
+
+ // 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);
+
+ installConfirm = findViewById(R.id.install_confirm_panel);
+ installConfirm.setVisibility(View.INVISIBLE);
startInstallConfirm();
}
@@ -197,14 +219,14 @@ public class InstallConfirmActivity extends Activity implements OnCancelListener
}
public void onClick(View v) {
- if (v == mOk) {
- if (mOkCanInstall || mScrollView == null) {
+ if (v == okButton) {
+ if (okCanInstall || scrollView == null) {
setResult(RESULT_OK, intent);
finish();
} else {
- mScrollView.pageScroll(View.FOCUS_DOWN);
+ scrollView.pageScroll(View.FOCUS_DOWN);
}
- } else if (v == mCancel) {
+ } else if (v == cancelButton) {
setResult(RESULT_CANCELED, intent);
finish();
}
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/java/org/fdroid/fdroid/views/fragments/PreferencesFragment.java b/app/src/main/java/org/fdroid/fdroid/views/fragments/PreferencesFragment.java
index 18baad9e1..ff29171f6 100644
--- a/app/src/main/java/org/fdroid/fdroid/views/fragments/PreferencesFragment.java
+++ b/app/src/main/java/org/fdroid/fdroid/views/fragments/PreferencesFragment.java
@@ -225,9 +225,6 @@ public class PreferencesFragment extends PreferenceFragment
case PrivilegedInstaller.IS_EXTENSION_INSTALLED_SIGNATURE_PROBLEM:
message = getActivity().getString(R.string.system_install_denied_signature);
break;
- case PrivilegedInstaller.IS_EXTENSION_INSTALLED_PERMISSIONS_PROBLEM:
- message = getActivity().getString(R.string.system_install_denied_permissions);
- break;
default:
throw new RuntimeException("unhandled return");
}
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..f6a81b9c8 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;
@@ -43,6 +44,7 @@ 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;
@@ -119,7 +121,6 @@ public class SwapWorkflowActivity extends AppCompatActivity {
private PrepareSwapRepo updateSwappableAppsTask;
private NewRepoConfig confirmSwapConfig;
private LocalBroadcastManager localBroadcastManager;
- private BroadcastReceiver downloadCompleteReceiver;
@NonNull
private final ServiceConnection serviceConnection = new ServiceConnection() {
@@ -773,7 +774,7 @@ public class SwapWorkflowActivity extends AppCompatActivity {
public void install(@NonNull final App app) {
final Apk apk = ApkProvider.Helper.find(this, app.packageName, app.suggestedVersionCode);
String urlString = apk.getUrl();
- downloadCompleteReceiver = new BroadcastReceiver() {
+ BroadcastReceiver downloadCompleteReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String path = intent.getStringExtra(Downloader.EXTRA_DOWNLOAD_PATH);
@@ -786,25 +787,44 @@ 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!");
+ }
+ }
+ };
+
}
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" />
-
+
-
+
-
-
-
+
diff --git a/app/src/main/res/layout/install_app_details.xml b/app/src/main/res/layout/install_app_details.xml
index e129dbb36..5fa67e8a9 100644
--- a/app/src/main/res/layout/install_app_details.xml
+++ b/app/src/main/res/layout/install_app_details.xml
@@ -1,5 +1,4 @@
-
-
-
-
+
-
+
+
-
+
+
-
-
-
+ android:textAppearance="?android:attr/textAppearanceLarge"
+ android:textColor="?android:attr/textColorPrimary"
+ tools:text="App Name" />
-
+
diff --git a/app/src/main/res/layout/install_confirm.xml b/app/src/main/res/layout/install_confirm.xml
index fd2a97271..b7e81e429 100644
--- a/app/src/main/res/layout/install_confirm.xml
+++ b/app/src/main/res/layout/install_confirm.xml
@@ -1,5 +1,4 @@
-
-
-
+
+ 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:visibility="gone" />
-
+ android:visibility="gone">
+ android:layout_height="match_parent"
+ android:orientation="vertical">
-
-
+
+
+
+ android:layout_gravity="center"
+ android:orientation="horizontal" />
@@ -84,63 +85,65 @@
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" />
-
+
-
+
-
-
-
+
diff --git a/app/src/main/res/layout/install_start.xml b/app/src/main/res/layout/install_start.xml
index 0fb199f8a..189936bab 100644
--- a/app/src/main/res/layout/install_start.xml
+++ b/app/src/main/res/layout/install_start.xml
@@ -1,5 +1,4 @@
-
-
-
+ android:layout_height="wrap_content">
+ android:layout_height="wrap_content" />
+ android:layout_below="@id/app_snippet" />
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 3dade6788..26f985b7c 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -267,10 +267,7 @@
Requesting root access…Root access deniedEither your Android device is not rooted or you have denied root access for F-Droid.
- Install errorFailed to install due to an unknown error
- An error occurred while parsing the package
- Uninstall errorFailed to uninstall due to an unknown errorF-Droid Privileged Extension is not availableThis option is only available when F-Droid Privileged Extension is installed.
@@ -341,10 +338,7 @@
Tap to install %sTap to update %s
- Do you want to install this application?
- It will get access to:
- Do you want to install this application?
- It does not require any special access.
+ needs access toDo you want to install an update
to this existing application? Your existing data will not
be lost. The updated application will get access to:
@@ -365,11 +359,16 @@
Download completed, tap to installDownload unsuccessfulWaiting to start download…
+ Error installing %s
+ Error uninstalling %s
+
New: Provided by %1$s.Downloading…Downloading %1$s
+ Installing…
+ Uninstalling…NeverHourly
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
index 254dbe092..5a9c2e85c 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -48,6 +48,20 @@
@color/fdroid_green
+
+
+
+