Merge branch 'installerservice-wip' into 'master'
InstallerService This merge request mainly introduces the ``InstallerService``. Many files have been touched and reworked in this merge request, due to the following changes: * After download of an apk in ``InstallManagerService``, the ``InstallerService``is started an kicks off the installation process. For unattended installers this directly runs through without any user interaction, for the default installer a new PendingIntent containing ``DefaultActivityInstaller`` is returned that is either stuffed into the notification or directly started from ``AppDetails`` * Using local broadcasts, ``InstallManagerService`` and ``AppDetails`` are informed of state changes in the installation process * ``DefaultActivityInstaller`` is a wrapper around the default installation APIs of Android * If the unattended ``PrivilegedInstaller`` is available, a permission screen is shown before download * Actual error codes and messages are displayed in notification or dialog on fail, especially interesting when using the ``PrivilegedInstaller`` * The process for installing the Privileged Extension has been moved into an own installer for logic seperation, called ``ExtensionInstaller`` Some design considerations: * I try to use Uris where ever possible. At some points this clashes with the usage of ``urlString`` in ``InstallManagerService``. This could be fixed in a later merge request Some other TODOs are left, but I would like to do them after this merge request has been merged if it's okay, as this one is already too huge: * Check if apk permissions are the same as announced in the permission screen for ``PrivilegedInstaller`` * In ``Installer.newPermissionCount()``, I need the target SDK before download to check if it's targetting Android M, which does not require the permission screen * Introduce FileProvider for Android N * Redesign layout of ``InstallConfirmActivity`` * Remove "cancel" icon for installing progress in AppDetails See merge request !300
This commit is contained in:
commit
9c1b917604
@ -316,12 +316,18 @@
|
||||
<activity
|
||||
android:name=".privileged.views.InstallConfirmActivity"
|
||||
android:label="@string/menu_install"
|
||||
android:theme="@style/MinWithDialogBaseThemeLight"
|
||||
android:excludeFromRecents="true"
|
||||
android:parentActivityName=".FDroid"
|
||||
android:configChanges="layoutDirection|locale" >
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value=".FDroid" />
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".privileged.views.UninstallDialogActivity"
|
||||
android:excludeFromRecents="true"
|
||||
android:theme="@style/AppThemeTransparent" />
|
||||
<activity
|
||||
android:name=".views.ManageReposActivity"
|
||||
android:label="@string/app_name"
|
||||
@ -401,6 +407,14 @@
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<!-- Note: AppThemeTransparent, this activity shows dialogs only -->
|
||||
<activity
|
||||
android:name=".installer.DefaultInstallerActivity"
|
||||
android:theme="@style/AppThemeTransparent" />
|
||||
<!-- Note: AppThemeTransparent, this activity shows dialogs only -->
|
||||
<activity
|
||||
android:name=".installer.ErrorDialogActivity"
|
||||
android:theme="@style/AppThemeTransparent" />
|
||||
|
||||
<receiver android:name=".receiver.StartupReceiver" >
|
||||
<intent-filter>
|
||||
@ -440,6 +454,9 @@
|
||||
<service
|
||||
android:name=".net.DownloaderService"
|
||||
android:exported="false" />
|
||||
<service
|
||||
android:name=".installer.InstallerService"
|
||||
android:exported="false" />
|
||||
<service
|
||||
android:name=".CleanCacheService"
|
||||
android:exported="false" />
|
||||
|
@ -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 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) {
|
||||
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;
|
||||
}
|
||||
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);
|
||||
startUninstall();
|
||||
}
|
||||
|
||||
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
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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<String> toArrayList() {
|
||||
ArrayList<String> out = new ArrayList<>();
|
||||
for (String element : this) {
|
||||
out.add(element);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
public String[] toArray() {
|
||||
ArrayList<String> list = toArrayList();
|
||||
return list.toArray(new String[list.size()]);
|
||||
}
|
||||
|
||||
public boolean contains(String v) {
|
||||
for (final String s : this) {
|
||||
if (s.equals(v)) {
|
||||
|
@ -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) {
|
||||
|
@ -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) {
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
|
||||
* Copyright (C) 2016 Dominik Schürmann <dominik@dominikschuermann.de>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License
|
||||
@ -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.
|
||||
* <p/>
|
||||
* 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";
|
||||
|
||||
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");
|
||||
try {
|
||||
mActivity.startActivityForResult(intent, REQUEST_CODE_INSTALL);
|
||||
} catch (ActivityNotFoundException e) {
|
||||
throw new InstallFailedException(e);
|
||||
}
|
||||
DefaultInstaller(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void deletePackageInternal(String packageName) throws InstallFailedException {
|
||||
try {
|
||||
PackageInfo pkgInfo = mPm.getPackageInfo(packageName, 0);
|
||||
protected void installPackage(Uri uri, Uri originatingUri, String packageName) {
|
||||
sendBroadcastInstall(uri, originatingUri, Installer.ACTION_INSTALL_STARTED);
|
||||
|
||||
Uri uri = Uri.fromParts("package", pkgInfo.packageName, null);
|
||||
Intent intent = new Intent(Intent.ACTION_DELETE, uri);
|
||||
Utils.debugLog(TAG, "DefaultInstaller uri: " + uri + " file: " + new File(uri.getPath()));
|
||||
|
||||
Uri sanitizedUri;
|
||||
try {
|
||||
mActivity.startActivityForResult(intent, REQUEST_CODE_DELETE);
|
||||
} catch (ActivityNotFoundException e) {
|
||||
throw new InstallFailedException(e);
|
||||
}
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
// already checked in super class
|
||||
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
|
||||
public boolean handleOnActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
/**
|
||||
* resultCode is always 0 on Android < 4.0. See
|
||||
* com.android.packageinstaller.PackageInstallerActivity: setResult is
|
||||
* never executed on Androids before 4.0
|
||||
*/
|
||||
switch (requestCode) {
|
||||
case REQUEST_CODE_INSTALL:
|
||||
mCallback.onSuccess(InstallerCallback.OPERATION_INSTALL);
|
||||
protected void uninstallPackage(String packageName) {
|
||||
sendBroadcastUninstall(packageName, Installer.ACTION_UNINSTALL_STARTED);
|
||||
|
||||
return true;
|
||||
case REQUEST_CODE_DELETE:
|
||||
mCallback.onSuccess(InstallerCallback.OPERATION_DELETE);
|
||||
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);
|
||||
|
||||
return true;
|
||||
default:
|
||||
sendBroadcastUninstall(packageName,
|
||||
Installer.ACTION_UNINSTALL_USER_INTERACTION, uninstallPendingIntent);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isUnattended() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,241 @@
|
||||
/*
|
||||
* Copyright (C) 2014-2016 Dominik Schürmann <dominik@dominikschuermann.de>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation; either version 3
|
||||
* of the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
||||
* MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
package org.fdroid.fdroid.installer;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.FragmentActivity;
|
||||
import android.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();
|
||||
}
|
||||
|
||||
}
|
@ -1,134 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation; either version 3
|
||||
* of the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
||||
* MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
package org.fdroid.fdroid.installer;
|
||||
|
||||
import android.annotation.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;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,70 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Dominik Schürmann <dominik@dominikschuermann.de>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation; either version 3
|
||||
* of the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
||||
* MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
package org.fdroid.fdroid.installer;
|
||||
|
||||
import android.app.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();
|
||||
}
|
||||
}
|
@ -0,0 +1,108 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Dominik Schürmann <dominik@dominikschuermann.de>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation; either version 3
|
||||
* of the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
||||
* MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
package org.fdroid.fdroid.installer;
|
||||
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.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.
|
||||
* <p/>
|
||||
* 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;
|
||||
}
|
||||
}
|
@ -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<String, BroadcastReceiver[]> receivers = new HashMap<>(3);
|
||||
|
||||
/**
|
||||
* Get the app name based on a {@code urlString} key. The app name needs
|
||||
* to be kept around for the final notification update, but {@link App}
|
||||
* and {@link Apk} instances have already removed by the time that final
|
||||
* notification update comes around. Once there is a proper
|
||||
* {@code InstallerService} and its integrated here, this must go away,
|
||||
* since the {@link App} and {@link Apk} instances will be available.
|
||||
* <p>
|
||||
* TODO <b>delete me once InstallerService exists</b>
|
||||
*/
|
||||
private static final HashMap<String, String> TEMP_HACK_APP_NAMES = new HashMap<>(3);
|
||||
|
||||
private LocalBroadcastManager localBroadcastManager;
|
||||
private NotificationManager notificationManager;
|
||||
|
||||
@ -234,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</a>
|
||||
*/
|
||||
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) {
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
|
||||
* Copyright (C) 2016 Dominik Schürmann <dominik@dominikschuermann.de>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License
|
||||
@ -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<String, Object> attributes = AndroidXMLDecompress.getManifestHeaderAttributes(apkFile.getAbsolutePath());
|
||||
|
||||
/* This isn't really needed, but might as well since we have the data already */
|
||||
if (attributes.containsKey("packageName") && !TextUtils.equals(packageName, (String) attributes.get("packageName"))) {
|
||||
throw new 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);
|
||||
|
||||
return intent;
|
||||
} else {
|
||||
// no permission screen needed!
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Intent uninstallIntent = new Intent(activity, InstallExtensionDialogActivity.class);
|
||||
uninstallIntent.setAction(InstallExtensionDialogActivity.ACTION_UNINSTALL);
|
||||
activity.startActivity(uninstallIntent);
|
||||
return;
|
||||
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;
|
||||
//}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
deletePackageInternal(packageName);
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
protected abstract void installPackageInternal(File apkFile)
|
||||
throws InstallFailedException;
|
||||
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();
|
||||
|
||||
protected abstract void deletePackageInternal(String packageName)
|
||||
throws InstallFailedException;
|
||||
|
||||
public abstract boolean handleOnActivityResult(int requestCode, int resultCode, Intent data);
|
||||
}
|
||||
|
@ -0,0 +1,77 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Dominik Schürmann <dominik@dominikschuermann.de>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation; either version 3
|
||||
* of the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
||||
* MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
package org.fdroid.fdroid.installer;
|
||||
|
||||
import android.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();
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,91 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Dominik Schürmann <dominik@dominikschuermann.de>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation; either version 3
|
||||
* of the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
||||
* MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
package org.fdroid.fdroid.installer;
|
||||
|
||||
import android.app.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.
|
||||
* <p/>
|
||||
* 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);
|
||||
}
|
||||
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
|
||||
* Copyright (C) 2014-2016 Dominik Schürmann <dominik@dominikschuermann.de>
|
||||
* Copyright (C) 2015 Daniel Martí <mvdan@mvdan.cc>
|
||||
*
|
||||
* 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
|
||||
* <ul>
|
||||
* <li>android.permission.INSTALL_PACKAGES</li>
|
||||
* <li>android.permission.DELETE_PACKAGES</li>
|
||||
* </ul>
|
||||
*
|
||||
* Installer that only works if the "F-Droid Privileged
|
||||
* Extension" is installed as a privileged app.
|
||||
* <p/>
|
||||
* "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:
|
||||
* <ul>
|
||||
* <li>On all Android versions if F-Droid is pre-deployed as a
|
||||
* system-application with the Rom</li>
|
||||
* <li>On Android < 4.4 also when moved into /system/app/</li>
|
||||
* <li>On Android >= 4.4 also when moved into /system/priv-app/</li>
|
||||
* </ul>
|
||||
*
|
||||
* system|signature) and cannot be used directly by F-Droid.
|
||||
* <p/>
|
||||
* 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.
|
||||
* <p/>
|
||||
* This installer makes unattended installs/uninstalls possible.
|
||||
* Thus no PendingIntents are returned.
|
||||
* <p/>
|
||||
* 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<Integer, String> 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<Integer, String> 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);
|
||||
}
|
||||
protected boolean isUnattended() {
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
}
|
||||
|
@ -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");
|
||||
}
|
||||
|
@ -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<String> 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;
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -0,0 +1,106 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Dominik Schürmann <dominik@dominikschuermann.de>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation; either version 3
|
||||
* of the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
||||
* MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
package org.fdroid.fdroid.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();
|
||||
}
|
||||
}
|
@ -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");
|
||||
}
|
||||
|
@ -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);
|
||||
|
||||
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 {
|
||||
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);
|
||||
installPendingIntent.send();
|
||||
} catch (PendingIntent.CanceledException e) {
|
||||
Log.e(TAG, "PI canceled", e);
|
||||
}
|
||||
|
||||
@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
|
||||
break;
|
||||
default:
|
||||
throw new RuntimeException("intent action not handled!");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
@ -21,21 +21,20 @@
|
||||
user before it is installed.
|
||||
-->
|
||||
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:orientation="vertical"
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/install_confirm"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/install_confirm"
|
||||
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||
android:paddingLeft="16dp"
|
||||
android:paddingRight="16dp"
|
||||
android:paddingTop="4dip" />
|
||||
android:paddingTop="4dip"
|
||||
android:text="@string/install_confirm"
|
||||
android:textAppearance="?android:attr/textAppearanceMedium" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/divider"
|
||||
@ -50,8 +49,7 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:visibility="gone">
|
||||
</FrameLayout>
|
||||
android:visibility="gone"></FrameLayout>
|
||||
|
||||
<TabHost
|
||||
android:id="@android:id/tabhost"
|
||||
@ -60,24 +58,28 @@
|
||||
android:layout_weight="1">
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<HorizontalScrollView android:id="@+id/tabscontainer"
|
||||
<HorizontalScrollView
|
||||
android:id="@+id/tabscontainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/tab_unselected_holo"
|
||||
android:fillViewport="true"
|
||||
android:scrollbars="none">
|
||||
<FrameLayout android:layout_width="wrap_content"
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<TabWidget
|
||||
android:id="@android:id/tabs"
|
||||
android:orientation="horizontal"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center" />
|
||||
android:layout_gravity="center"
|
||||
android:orientation="horizontal" />
|
||||
</FrameLayout>
|
||||
</HorizontalScrollView>
|
||||
|
||||
@ -100,47 +102,51 @@
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:divider="?android:attr/dividerHorizontal"
|
||||
android:orientation="vertical"
|
||||
android:showDividers="beginning">
|
||||
|
||||
<LinearLayout
|
||||
style="?android:attr/buttonBarStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:measureWithLargestChild="true">
|
||||
android:measureWithLargestChild="true"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<LinearLayout android:id="@+id/leftSpacer"
|
||||
android:layout_weight="0.25"
|
||||
<LinearLayout
|
||||
android:id="@+id/leftSpacer"
|
||||
android:layout_width="0dip"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="0.25"
|
||||
android:orientation="horizontal"
|
||||
android:visibility="gone" />
|
||||
|
||||
<Button android:id="@+id/cancel_button"
|
||||
<Button
|
||||
android:id="@+id/cancel_button"
|
||||
style="?android:attr/buttonBarButtonStyle"
|
||||
android:layout_width="0dip"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="start"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/cancel"
|
||||
android:maxLines="2"
|
||||
style="?android:attr/buttonBarButtonStyle" />
|
||||
android:text="@string/cancel" />
|
||||
|
||||
<Button android:id="@+id/ok_button"
|
||||
<Button
|
||||
android:id="@+id/ok_button"
|
||||
style="?android:attr/buttonBarButtonStyle"
|
||||
android:layout_width="0dip"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="end"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/next"
|
||||
android:maxLines="2"
|
||||
android:filterTouchesWhenObscured="true"
|
||||
style="?android:attr/buttonBarButtonStyle" />
|
||||
android:maxLines="2"
|
||||
android:text="@string/next" />
|
||||
|
||||
<LinearLayout android:id="@+id/rightSpacer"
|
||||
<LinearLayout
|
||||
android:id="@+id/rightSpacer"
|
||||
android:layout_width="0dip"
|
||||
android:layout_weight="0.25"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="0.25"
|
||||
android:orientation="horizontal"
|
||||
android:visibility="gone" />
|
||||
|
||||
|
@ -1,5 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright (C) 2008 The Android Open Source Project
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Copyright (C) 2008 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@ -17,59 +16,46 @@
|
||||
<!--
|
||||
Defines the layout of the application snippet that appears on top of the
|
||||
installation screens
|
||||
-->
|
||||
<!-- The snippet about the application - title, icon, description. -->
|
||||
<RelativeLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
--><!-- The snippet about the application - title, icon, description. -->
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/app_snippet"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingLeft="16dip"
|
||||
android:paddingStart="16dip"
|
||||
android:paddingRight="16dip"
|
||||
android:paddingEnd="16dip"
|
||||
android:paddingTop="24dip"
|
||||
>
|
||||
<ImageView android:id="@+id/app_icon"
|
||||
android:layout_width="32dip"
|
||||
android:layout_height="32dip"
|
||||
android:paddingLeft="16dip"
|
||||
android:paddingRight="16dip"
|
||||
android:paddingStart="16dip"
|
||||
android:paddingTop="16dip">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/app_icon"
|
||||
android:layout_width="48dip"
|
||||
android:layout_height="48dip"
|
||||
android:layout_marginLeft="8dip"
|
||||
android:layout_marginStart="8dip"
|
||||
android:background="@android:color/transparent"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_alignParentStart="true"
|
||||
android:gravity="start"
|
||||
android:scaleType="centerCrop"/>
|
||||
<TextView android:id="@+id/app_name"
|
||||
android:scaleType="centerCrop"
|
||||
tools:src="@drawable/ic_launcher" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/app_name"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:ellipsize="end"
|
||||
android:gravity="center"
|
||||
android:textAppearance="?android:attr/textAppearanceLarge"
|
||||
android:textColor="?android:attr/textColorPrimary"
|
||||
android:paddingEnd="16dip"
|
||||
android:paddingLeft="16dip"
|
||||
android:paddingRight="16dip"
|
||||
android:paddingStart="16dip"
|
||||
android:shadowColor="@color/shadow"
|
||||
android:shadowRadius="2"
|
||||
android:layout_toRightOf="@id/app_icon"
|
||||
android:layout_toEndOf="@id/app_icon"
|
||||
android:singleLine="true"
|
||||
android:layout_centerInParent="true"
|
||||
android:paddingRight="16dip"
|
||||
android:paddingEnd="16dip"
|
||||
android:paddingTop="3dip"
|
||||
android:paddingLeft="16dip"
|
||||
android:paddingStart="16dip"
|
||||
android:ellipsize="end"/>
|
||||
<FrameLayout
|
||||
android:id="@+id/top_divider"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="4dip"
|
||||
android:layout_below="@id/app_name">
|
||||
<ProgressBar
|
||||
android:id="@+id/progress_bar"
|
||||
style="?android:attr/progressBarStyleHorizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
</FrameLayout>
|
||||
android:textAppearance="?android:attr/textAppearanceLarge"
|
||||
android:textColor="?android:attr/textColorPrimary"
|
||||
tools:text="App Name" />
|
||||
|
||||
</RelativeLayout>
|
||||
</LinearLayout>
|
||||
|
||||
|
@ -1,5 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright (C) 2008 The Android Open Source Project
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Copyright (C) 2008 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@ -21,21 +20,20 @@
|
||||
user before it is installed.
|
||||
-->
|
||||
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:orientation="vertical"
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/install_confirm"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/install_confirm"
|
||||
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||
android:paddingLeft="16dp"
|
||||
android:paddingRight="16dp"
|
||||
android:paddingTop="4dip" />
|
||||
android:paddingTop="4dip"
|
||||
android:text="@string/install_confirm"
|
||||
android:textAppearance="?android:attr/textAppearanceMedium" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/divider"
|
||||
@ -49,8 +47,7 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:visibility="gone">
|
||||
</FrameLayout>
|
||||
android:visibility="gone"></FrameLayout>
|
||||
|
||||
<TabHost
|
||||
android:id="@android:id/tabhost"
|
||||
@ -59,24 +56,28 @@
|
||||
android:layout_weight="1">
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<HorizontalScrollView android:id="@+id/tabscontainer"
|
||||
<HorizontalScrollView
|
||||
android:id="@+id/tabscontainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/tab_unselected_holo"
|
||||
android:fillViewport="true"
|
||||
android:scrollbars="none">
|
||||
<FrameLayout android:layout_width="wrap_content"
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<TabWidget
|
||||
android:id="@android:id/tabs"
|
||||
android:orientation="horizontal"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center" />
|
||||
android:layout_gravity="center"
|
||||
android:orientation="horizontal" />
|
||||
</FrameLayout>
|
||||
</HorizontalScrollView>
|
||||
|
||||
@ -99,46 +100,48 @@
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:divider="?android:attr/dividerHorizontal"
|
||||
android:orientation="vertical"
|
||||
android:showDividers="beginning">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:measureWithLargestChild="true">
|
||||
android:measureWithLargestChild="true"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<LinearLayout android:id="@+id/leftSpacer"
|
||||
android:layout_weight="0.25"
|
||||
<LinearLayout
|
||||
android:id="@+id/leftSpacer"
|
||||
android:layout_width="0dip"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="0.25"
|
||||
android:orientation="horizontal"
|
||||
android:visibility="gone" />
|
||||
|
||||
<Button android:id="@+id/cancel_button"
|
||||
<Button
|
||||
android:id="@+id/cancel_button"
|
||||
android:layout_width="0dip"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="start"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/cancel"
|
||||
android:maxLines="2"
|
||||
/>
|
||||
android:text="@string/cancel" />
|
||||
|
||||
<Button android:id="@+id/ok_button"
|
||||
<Button
|
||||
android:id="@+id/ok_button"
|
||||
android:layout_width="0dip"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="end"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/next"
|
||||
android:maxLines="2"
|
||||
android:filterTouchesWhenObscured="true"
|
||||
/>
|
||||
android:maxLines="2"
|
||||
android:text="@string/next" />
|
||||
|
||||
<LinearLayout android:id="@+id/rightSpacer"
|
||||
<LinearLayout
|
||||
android:id="@+id/rightSpacer"
|
||||
android:layout_width="0dip"
|
||||
android:layout_weight="0.25"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="0.25"
|
||||
android:orientation="horizontal"
|
||||
android:visibility="gone" />
|
||||
|
||||
|
@ -1,5 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright (C) 2008 The Android Open Source Project
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Copyright (C) 2008 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@ -14,24 +13,22 @@
|
||||
limitations under the License.
|
||||
-->
|
||||
|
||||
<RelativeLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<include
|
||||
android:id="@+id/app_snippet"
|
||||
layout="@layout/install_app_details"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/app_snippet"/>
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<include
|
||||
layout="@layout/install_confirm"
|
||||
android:id="@+id/install_confirm_panel"
|
||||
layout="@layout/install_confirm"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/app_snippet"
|
||||
android:layout_alignParentBottom="true"/>
|
||||
android:layout_below="@id/app_snippet" />
|
||||
</RelativeLayout>
|
||||
|
||||
|
||||
|
@ -267,10 +267,7 @@
|
||||
<string name="requesting_root_access_body">Requesting root access…</string>
|
||||
<string name="root_access_denied_title">Root access denied</string>
|
||||
<string name="root_access_denied_body">Either your Android device is not rooted or you have denied root access for F-Droid.</string>
|
||||
<string name="install_error_title">Install error</string>
|
||||
<string name="install_error_unknown">Failed to install due to an unknown error</string>
|
||||
<string name="install_error_cannot_parse">An error occurred while parsing the package</string>
|
||||
<string name="uninstall_error_title">Uninstall error</string>
|
||||
<string name="uninstall_error_unknown">Failed to uninstall due to an unknown error</string>
|
||||
<string name="system_install_denied_title">F-Droid Privileged Extension is not available</string>
|
||||
<string name="system_install_denied_body">This option is only available when F-Droid Privileged Extension is installed.</string>
|
||||
@ -341,10 +338,7 @@
|
||||
|
||||
<string name="tap_to_install_format">Tap to install %s</string>
|
||||
<string name="tap_to_update_format">Tap to update %s</string>
|
||||
<string name="install_confirm">Do you want to install this application?
|
||||
It will get access to:</string>
|
||||
<string name="install_confirm_no_perms">Do you want to install this application?
|
||||
It does not require any special access.</string>
|
||||
<string name="install_confirm">needs access to</string>
|
||||
<string name="install_confirm_update">Do you want to install an update
|
||||
to this existing application? Your existing data will not
|
||||
be lost. The updated application will get access to:</string>
|
||||
@ -365,11 +359,16 @@
|
||||
<string name="tap_to_install">Download completed, tap to install</string>
|
||||
<string name="download_error">Download unsuccessful</string>
|
||||
<string name="download_pending">Waiting to start download…</string>
|
||||
<string name="install_error_notify_title">Error installing %s</string>
|
||||
<string name="uninstall_error_notify_title">Error uninstalling %s</string>
|
||||
|
||||
|
||||
<string name="perms_new_perm_prefix">New: </string>
|
||||
<string name="perms_description_app">Provided by %1$s.</string>
|
||||
<string name="downloading">Downloading…</string>
|
||||
<string name="downloading_apk">Downloading %1$s</string>
|
||||
<string name="installing">Installing…</string>
|
||||
<string name="uninstalling">Uninstalling…</string>
|
||||
|
||||
<string name="interval_never">Never</string>
|
||||
<string name="interval_1h">Hourly</string>
|
||||
|
@ -48,6 +48,20 @@
|
||||
<item name="colorAccent">@color/fdroid_green</item>
|
||||
</style>
|
||||
|
||||
<style name="MinWithDialogBaseThemeDark" parent="Theme.AppCompat.Dialog.MinWidth">
|
||||
<item name="colorAccent">@color/fdroid_green</item>
|
||||
|
||||
<item name="windowActionBar">false</item>
|
||||
<item name="windowNoTitle">true</item>
|
||||
</style>
|
||||
|
||||
<style name="MinWithDialogBaseThemeLight" parent="Theme.AppCompat.Light.Dialog.MinWidth">
|
||||
<item name="colorAccent">@color/fdroid_green</item>
|
||||
|
||||
<item name="windowActionBar">false</item>
|
||||
<item name="windowNoTitle">true</item>
|
||||
</style>
|
||||
|
||||
<style name="TextViewStyle" parent="android:Widget.TextView">
|
||||
<item name="android:textColor">?android:attr/textColorPrimary</item>
|
||||
</style>
|
||||
|
Loading…
x
Reference in New Issue
Block a user