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:
Hans-Christoph Steiner 2016-06-01 15:06:38 +00:00
commit 9c1b917604
29 changed files with 1899 additions and 1331 deletions

View File

@ -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" />

View File

@ -22,6 +22,7 @@
package org.fdroid.fdroid;
import android.app.Activity;
import android.app.PendingIntent;
import android.bluetooth.BluetoothAdapter;
import android.content.ActivityNotFoundException;
import android.content.BroadcastReceiver;
@ -78,17 +79,16 @@ import com.nostra13.universalimageloader.core.ImageLoader;
import com.nostra13.universalimageloader.core.assist.ImageScaleType;
import org.fdroid.fdroid.Utils.CommaSeparatedList;
import org.fdroid.fdroid.compat.PackageManagerCompat;
import org.fdroid.fdroid.data.Apk;
import org.fdroid.fdroid.data.ApkProvider;
import org.fdroid.fdroid.data.App;
import org.fdroid.fdroid.data.AppProvider;
import org.fdroid.fdroid.data.InstalledAppProvider;
import org.fdroid.fdroid.data.RepoProvider;
import org.fdroid.fdroid.installer.InstallManagerService;
import org.fdroid.fdroid.installer.Installer;
import org.fdroid.fdroid.installer.Installer.InstallFailedException;
import org.fdroid.fdroid.installer.Installer.InstallerCallback;
import org.fdroid.fdroid.installer.InstallManagerService;
import org.fdroid.fdroid.installer.InstallerFactory;
import org.fdroid.fdroid.installer.InstallerService;
import org.fdroid.fdroid.net.Downloader;
import org.fdroid.fdroid.net.DownloaderService;
@ -101,6 +101,8 @@ public class AppDetails extends AppCompatActivity {
private static final String TAG = "AppDetails";
private static final int REQUEST_ENABLE_BLUETOOTH = 2;
private static final int REQUEST_PERMISSION_DIALOG = 3;
private static final int REQUEST_UNINSTALL_DIALOG = 4;
public static final String EXTRA_APPID = "appid";
public static final String EXTRA_FROM = "from";
@ -319,7 +321,6 @@ public class AppDetails extends AppCompatActivity {
private int startingIgnoreThis;
private final Context context = this;
private Installer installer;
private AppDetailsHeaderFragment headerFragment;
@ -375,8 +376,6 @@ public class AppDetails extends AppCompatActivity {
packageManager = getPackageManager();
installer = Installer.getActivityInstaller(this, packageManager, myInstallerCallback);
// Get the preferences we're going to use in this Activity...
ConfigurationChangeHelper previousData = (ConfigurationChangeHelper) getLastCustomNonConfigurationInstance();
if (previousData != null) {
@ -530,13 +529,12 @@ public class AppDetails extends AppCompatActivity {
private final BroadcastReceiver completeReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
File localFile = new File(intent.getStringExtra(Downloader.EXTRA_DOWNLOAD_PATH));
try {
installer.installPackage(localFile, app.packageName, intent.getDataString());
} catch (InstallFailedException e) {
Log.e(TAG, "Android not compatible with this Installer!", e);
}
cleanUpFinishedDownload();
Uri localUri =
Uri.fromFile(new File(intent.getStringExtra(Downloader.EXTRA_DOWNLOAD_PATH)));
localBroadcastManager.registerReceiver(installReceiver,
Installer.getInstallIntentFilter(localUri));
}
};
@ -555,6 +553,108 @@ public class AppDetails extends AppCompatActivity {
}
};
private final BroadcastReceiver installReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
switch (intent.getAction()) {
case Installer.ACTION_INSTALL_STARTED:
headerFragment.startProgress();
headerFragment.showIndeterminateProgress(getString(R.string.installing));
break;
case Installer.ACTION_INSTALL_COMPLETE:
headerFragment.removeProgress();
localBroadcastManager.unregisterReceiver(this);
break;
case Installer.ACTION_INSTALL_INTERRUPTED:
headerFragment.removeProgress();
onAppChanged();
String errorMessage =
intent.getStringExtra(Installer.EXTRA_ERROR_MESSAGE);
if (!TextUtils.isEmpty(errorMessage)) {
Log.e(TAG, "install aborted with errorMessage: " + errorMessage);
String title = String.format(
getString(R.string.install_error_notify_title),
app.name);
AlertDialog.Builder alertBuilder = new AlertDialog.Builder(AppDetails.this);
alertBuilder.setTitle(title);
alertBuilder.setMessage(errorMessage);
alertBuilder.setNeutralButton(android.R.string.ok, null);
alertBuilder.create().show();
}
localBroadcastManager.unregisterReceiver(this);
break;
case Installer.ACTION_INSTALL_USER_INTERACTION:
PendingIntent installPendingIntent =
intent.getParcelableExtra(Installer.EXTRA_USER_INTERACTION_PI);
try {
installPendingIntent.send();
} catch (PendingIntent.CanceledException e) {
Log.e(TAG, "PI canceled", e);
}
break;
default:
throw new RuntimeException("intent action not handled!");
}
}
};
private final BroadcastReceiver uninstallReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
switch (intent.getAction()) {
case Installer.ACTION_UNINSTALL_STARTED:
headerFragment.startProgress();
headerFragment.showIndeterminateProgress(getString(R.string.uninstalling));
break;
case Installer.ACTION_UNINSTALL_COMPLETE:
headerFragment.removeProgress();
onAppChanged();
localBroadcastManager.unregisterReceiver(this);
break;
case Installer.ACTION_UNINSTALL_INTERRUPTED:
headerFragment.removeProgress();
String errorMessage =
intent.getStringExtra(Installer.EXTRA_ERROR_MESSAGE);
if (!TextUtils.isEmpty(errorMessage)) {
Log.e(TAG, "uninstall aborted with errorMessage: " + errorMessage);
AlertDialog.Builder alertBuilder = new AlertDialog.Builder(AppDetails.this);
alertBuilder.setTitle(R.string.uninstall_error_notify_title);
alertBuilder.setMessage(errorMessage);
alertBuilder.setNeutralButton(android.R.string.ok, null);
alertBuilder.create().show();
}
localBroadcastManager.unregisterReceiver(this);
break;
case Installer.ACTION_UNINSTALL_USER_INTERACTION:
PendingIntent uninstallPendingIntent =
intent.getParcelableExtra(Installer.EXTRA_USER_INTERACTION_PI);
try {
uninstallPendingIntent.send();
} catch (PendingIntent.CanceledException e) {
Log.e(TAG, "PI canceled", e);
}
break;
default:
throw new RuntimeException("intent action not handled!");
}
}
};
private void onAppChanged() {
if (!reset(app.packageName)) {
this.finish();
@ -796,7 +896,7 @@ public class AppDetails extends AppCompatActivity {
return true;
case UNINSTALL:
removeApk(app.packageName);
uninstallApk(app.packageName);
return true;
case IGNOREALL:
@ -875,76 +975,43 @@ public class AppDetails extends AppCompatActivity {
}
private void initiateInstall(Apk apk) {
Installer installer = InstallerFactory.create(this, apk.packageName);
Intent intent = installer.getPermissionScreen(apk);
if (intent != null) {
// permission screen required
Utils.debugLog(TAG, "permission screen required");
startActivityForResult(intent, REQUEST_PERMISSION_DIALOG);
return;
}
startInstall(apk);
}
private void startInstall(Apk apk) {
activeDownloadUrlString = apk.getUrl();
registerDownloaderReceivers();
headerFragment.startProgress();
InstallManagerService.queue(this, app, apk);
}
private void removeApk(String packageName) {
try {
installer.deletePackage(packageName);
} catch (InstallFailedException e) {
Log.e(TAG, "Android not compatible with this Installer!", e);
private void uninstallApk(String packageName) {
Installer installer = InstallerFactory.create(this, packageName);
Intent intent = installer.getUninstallScreen(packageName);
if (intent != null) {
// uninstall screen required
Utils.debugLog(TAG, "screen screen required");
startActivityForResult(intent, REQUEST_UNINSTALL_DIALOG);
return;
}
startUninstall();
}
private final Installer.InstallerCallback myInstallerCallback = new Installer.InstallerCallback() {
@Override
public void onSuccess(final int operation) {
runOnUiThread(new Runnable() {
@Override
public void run() {
if (operation == Installer.InstallerCallback.OPERATION_INSTALL) {
PackageManagerCompat.setInstaller(packageManager, app.packageName);
}
onAppChanged();
}
});
}
@Override
public void onError(int operation, final int errorCode) {
if (errorCode == InstallerCallback.ERROR_CODE_CANCELED) {
return;
}
final int title, body;
if (operation == InstallerCallback.OPERATION_INSTALL) {
title = R.string.install_error_title;
switch (errorCode) {
case ERROR_CODE_CANNOT_PARSE:
body = R.string.install_error_cannot_parse;
break;
default: // ERROR_CODE_OTHER
body = R.string.install_error_unknown;
break;
}
} else { // InstallerCallback.OPERATION_DELETE
title = R.string.uninstall_error_title;
switch (errorCode) {
default: // ERROR_CODE_OTHER
body = R.string.uninstall_error_unknown;
break;
}
}
runOnUiThread(new Runnable() {
@Override
public void run() {
onAppChanged();
Log.e(TAG, "Installer aborted with errorCode: " + errorCode);
AlertDialog.Builder alertBuilder = new AlertDialog.Builder(AppDetails.this);
alertBuilder.setTitle(title);
alertBuilder.setMessage(body);
alertBuilder.setNeutralButton(android.R.string.ok, null);
alertBuilder.create().show();
}
});
}
};
private void startUninstall() {
localBroadcastManager.registerReceiver(uninstallReceiver,
Installer.getUninstallIntentFilter(app.packageName));
InstallerService.uninstall(context, app.packageName);
}
private void launchApk(String packageName) {
Intent intent = packageManager.getLaunchIntentForPackage(packageName);
@ -963,15 +1030,22 @@ public class AppDetails extends AppCompatActivity {
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
// handle cases for install manager first
if (installer.handleOnActivityResult(requestCode, resultCode, data)) {
return;
}
switch (requestCode) {
case REQUEST_ENABLE_BLUETOOTH:
fdroidApp.sendViaBluetooth(this, resultCode, app.packageName);
break;
case REQUEST_PERMISSION_DIALOG:
if (resultCode == Activity.RESULT_OK) {
Uri uri = data.getData();
Apk apk = ApkProvider.Helper.find(this, uri, ApkProvider.DataColumns.ALL);
startInstall(apk);
}
break;
case REQUEST_UNINSTALL_DIALOG:
if (resultCode == Activity.RESULT_OK) {
startUninstall();
}
break;
}
}
@ -1606,7 +1680,7 @@ public class AppDetails extends AppCompatActivity {
// If "launchable", launch
activity.launchApk(app.packageName);
} else {
activity.removeApk(app.packageName);
activity.uninstallApk(app.packageName);
}
} else if (app.suggestedVersionCode > 0) {
// If not installed, install
@ -1635,7 +1709,7 @@ public class AppDetails extends AppCompatActivity {
}
void remove() {
appDetails.removeApk(appDetails.getApp().packageName);
appDetails.uninstallApk(appDetails.getApp().packageName);
}
@Override

View File

@ -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);
}

View File

@ -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)) {

View File

@ -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) {

View File

@ -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) {

View File

@ -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";
DefaultInstaller(Context context) {
super(context);
}
private static final int REQUEST_CODE_INSTALL = 0;
private static final int REQUEST_CODE_DELETE = 1;
@Override
protected void installPackageInternal(File apkFile) throws InstallFailedException {
Intent intent = new Intent();
intent.setAction(Intent.ACTION_VIEW);
intent.setDataAndType(Uri.fromFile(apkFile),
"application/vnd.android.package-archive");
protected void installPackage(Uri uri, Uri originatingUri, String packageName) {
sendBroadcastInstall(uri, originatingUri, Installer.ACTION_INSTALL_STARTED);
Utils.debugLog(TAG, "DefaultInstaller uri: " + uri + " file: " + new File(uri.getPath()));
Uri sanitizedUri;
try {
mActivity.startActivityForResult(intent, REQUEST_CODE_INSTALL);
} catch (ActivityNotFoundException e) {
throw new InstallFailedException(e);
sanitizedUri = Installer.prepareApkFile(context, uri, packageName);
} catch (Installer.InstallFailedException e) {
Log.e(TAG, "prepareApkFile failed", e);
sendBroadcastInstall(uri, originatingUri, Installer.ACTION_INSTALL_INTERRUPTED,
e.getMessage());
return;
}
Intent installIntent = new Intent(context, DefaultInstallerActivity.class);
installIntent.setAction(DefaultInstallerActivity.ACTION_INSTALL_PACKAGE);
installIntent.putExtra(DefaultInstallerActivity.EXTRA_ORIGINATING_URI, originatingUri);
installIntent.setData(sanitizedUri);
PendingIntent installPendingIntent = PendingIntent.getActivity(
context.getApplicationContext(),
uri.hashCode(),
installIntent,
PendingIntent.FLAG_UPDATE_CURRENT);
sendBroadcastInstall(uri, originatingUri,
Installer.ACTION_INSTALL_USER_INTERACTION, installPendingIntent);
}
@Override
protected void deletePackageInternal(String packageName) throws InstallFailedException {
try {
PackageInfo pkgInfo = mPm.getPackageInfo(packageName, 0);
protected void uninstallPackage(String packageName) {
sendBroadcastUninstall(packageName, Installer.ACTION_UNINSTALL_STARTED);
Uri uri = Uri.fromParts("package", pkgInfo.packageName, null);
Intent intent = new Intent(Intent.ACTION_DELETE, uri);
try {
mActivity.startActivityForResult(intent, REQUEST_CODE_DELETE);
} catch (ActivityNotFoundException e) {
throw new InstallFailedException(e);
}
} catch (PackageManager.NameNotFoundException e) {
// already checked in super class
}
Intent uninstallIntent = new Intent(context, DefaultInstallerActivity.class);
uninstallIntent.setAction(DefaultInstallerActivity.ACTION_UNINSTALL_PACKAGE);
uninstallIntent.putExtra(
DefaultInstallerActivity.EXTRA_UNINSTALL_PACKAGE_NAME, packageName);
PendingIntent uninstallPendingIntent = PendingIntent.getActivity(
context.getApplicationContext(),
packageName.hashCode(),
uninstallIntent,
PendingIntent.FLAG_UPDATE_CURRENT);
sendBroadcastUninstall(packageName,
Installer.ACTION_UNINSTALL_USER_INTERACTION, uninstallPendingIntent);
}
@Override
public boolean handleOnActivityResult(int requestCode, int resultCode, Intent data) {
/**
* resultCode is always 0 on Android < 4.0. See
* com.android.packageinstaller.PackageInstallerActivity: setResult is
* never executed on Androids before 4.0
*/
switch (requestCode) {
case REQUEST_CODE_INSTALL:
mCallback.onSuccess(InstallerCallback.OPERATION_INSTALL);
return true;
case REQUEST_CODE_DELETE:
mCallback.onSuccess(InstallerCallback.OPERATION_DELETE);
return true;
default:
return false;
}
protected boolean isUnattended() {
return false;
}
}

View File

@ -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();
}
}

View File

@ -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;
}
}
}

View File

@ -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();
}
}

View File

@ -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;
}
}

View File

@ -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;
@ -180,7 +169,7 @@ public class InstallManagerService extends Service {
sendBroadcast(intent.getData(), Downloader.ACTION_STARTED, apkFilePath);
sendBroadcast(intent.getData(), Downloader.ACTION_COMPLETE, apkFilePath);
} else {
Utils.debugLog(TAG, " delete and download again " + urlString + " " + apkFilePath);
Utils.debugLog(TAG, "delete and download again " + urlString + " " + apkFilePath);
apkFilePath.delete();
DownloaderService.queue(this, urlString);
}
@ -234,22 +223,26 @@ public class InstallManagerService extends Service {
BroadcastReceiver completeReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String urlString = intent.getDataString();
// TODO these need to be removed based on whether they are fed to InstallerService or not
Apk apk = removeFromActive(urlString);
if (AppDetails.isAppVisible(apk.packageName)) {
cancelNotification(urlString);
} else {
notifyDownloadComplete(urlString, apk);
}
unregisterDownloaderReceivers(urlString);
// elsewhere called urlString
Uri originatingUri = intent.getData();
File localFile = new File(intent.getStringExtra(Downloader.EXTRA_DOWNLOAD_PATH));
Uri localUri = Uri.fromFile(localFile);
Utils.debugLog(TAG, "download completed of " + originatingUri
+ " to " + localUri);
unregisterDownloaderReceivers(intent.getDataString());
registerInstallerReceivers(localUri);
Apk apk = ACTIVE_APKS.get(originatingUri.toString());
InstallerService.install(context, localUri, originatingUri, apk.packageName);
}
};
BroadcastReceiver interruptedReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String urlString = intent.getDataString();
Apk apk = removeFromActive(urlString);
removeFromActive(urlString);
unregisterDownloaderReceivers(urlString);
cancelNotification(urlString);
}
@ -265,6 +258,70 @@ public class InstallManagerService extends Service {
receivers.put(urlString, new BroadcastReceiver[]{
startedReceiver, progressReceiver, completeReceiver, interruptedReceiver,
});
}
private void registerInstallerReceivers(Uri uri) {
BroadcastReceiver installReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
Uri originatingUri = intent.getParcelableExtra(Installer.EXTRA_ORIGINATING_URI);
switch (intent.getAction()) {
case Installer.ACTION_INSTALL_STARTED:
// nothing to do
break;
case Installer.ACTION_INSTALL_COMPLETE:
Apk apkComplete = removeFromActive(originatingUri.toString());
PackageManagerCompat.setInstaller(getPackageManager(), apkComplete.packageName);
localBroadcastManager.unregisterReceiver(this);
break;
case Installer.ACTION_INSTALL_INTERRUPTED:
String errorMessage =
intent.getStringExtra(Installer.EXTRA_ERROR_MESSAGE);
// show notification if app details is not visible
if (!TextUtils.isEmpty(errorMessage)) {
App app = getAppFromActive(originatingUri.toString());
String title = String.format(
getString(R.string.install_error_notify_title),
app.name);
// show notification if app details is not visible
if (AppDetails.isAppVisible(app.packageName)) {
cancelNotification(originatingUri.toString());
} else {
notifyError(originatingUri.toString(), title, errorMessage);
}
}
localBroadcastManager.unregisterReceiver(this);
break;
case Installer.ACTION_INSTALL_USER_INTERACTION:
PendingIntent installPendingIntent =
intent.getParcelableExtra(Installer.EXTRA_USER_INTERACTION_PI);
Apk apkUserInteraction = getApkFromActive(originatingUri.toString());
// show notification if app details is not visible
if (AppDetails.isAppVisible(apkUserInteraction.packageName)) {
cancelNotification(originatingUri.toString());
} else {
notifyDownloadComplete(apkUserInteraction, originatingUri.toString(), installPendingIntent);
}
break;
default:
throw new RuntimeException("intent action not handled!");
}
}
};
localBroadcastManager.registerReceiver(installReceiver,
Installer.getInstallIntentFilter(uri));
}
private NotificationCompat.Builder createNotificationBuilder(String urlString, Apk apk) {
@ -273,7 +330,7 @@ public class InstallManagerService extends Service {
.setAutoCancel(false)
.setOngoing(true)
.setContentIntent(getAppDetailsIntent(downloadUrlId, apk))
.setContentTitle(getString(R.string.downloading_apk, getAppName(urlString, apk)))
.setContentTitle(getString(R.string.downloading_apk, getAppName(apk)))
.addAction(R.drawable.ic_cancel_black_24dp, getString(R.string.cancel),
DownloaderService.getCancelPendingIntent(this, urlString))
.setSmallIcon(android.R.drawable.stat_sys_download)
@ -281,18 +338,8 @@ public class InstallManagerService extends Service {
.setProgress(100, 0, true);
}
private String getAppName(String urlString, Apk apk) {
App app = ACTIVE_APPS.get(apk.packageName);
if (app == null || TextUtils.isEmpty(app.name)) {
if (TEMP_HACK_APP_NAMES.containsKey(urlString)) {
return TEMP_HACK_APP_NAMES.get(urlString);
} else {
// this is ugly, but its better than nothing as a failsafe
return urlString;
}
} else {
return app.name;
}
private String getAppName(Apk apk) {
return ACTIVE_APPS.get(apk.packageName).name;
}
/**
@ -319,14 +366,14 @@ public class InstallManagerService extends Service {
* Removing the progress bar from a notification should cause the notification's content
* text to return to normal size</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) {

View File

@ -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);
Intent uninstallIntent = new Intent(activity, InstallExtensionDialogActivity.class);
uninstallIntent.setAction(InstallExtensionDialogActivity.ACTION_UNINSTALL);
activity.startActivity(uninstallIntent);
return;
return intent;
} else {
// no permission screen needed!
return null;
}
deletePackageInternal(packageName);
}
protected abstract void installPackageInternal(File apkFile)
throws InstallFailedException;
private int newPermissionCount(Apk apk) {
// TODO: requires targetSdk in Apk class/database
//boolean supportsRuntimePermissions = mPkgInfo.applicationInfo.targetSdkVersion
// >= Build.VERSION_CODES.M;
//if (supportsRuntimePermissions) {
// return 0;
//}
protected abstract void deletePackageInternal(String packageName)
throws InstallFailedException;
AppDiff appDiff = new AppDiff(context.getPackageManager(), apk);
if (appDiff.mPkgInfo == null) {
// could not get diff because we couldn't parse the package
throw new RuntimeException("cannot parse!");
}
AppSecurityPermissions perms = new AppSecurityPermissions(context, appDiff.mPkgInfo);
if (appDiff.mInstalledAppInfo != null) {
// update to an existing app
return perms.getPermissionCount(AppSecurityPermissions.WHICH_NEW);
}
// new app install
return perms.getPermissionCount(AppSecurityPermissions.WHICH_ALL);
}
/**
* Returns an Intent to start a dialog wrapped in an activity
* for uninstall confirmation.
*
* @param packageName packageName of app to uninstall
* @return Intent with activity for uninstall confirmation
* Returns null if Installer handles that on itself, e.g.,
* with DefaultInstaller.
*/
public Intent getUninstallScreen(String packageName) {
if (!isUnattended()) {
return null;
}
Intent intent = new Intent(context, UninstallDialogActivity.class);
intent.putExtra(Installer.EXTRA_PACKAGE_NAME, packageName);
return intent;
}
/**
* Checks the APK file against the provided hash, returning whether it is a match.
*/
public static boolean verifyApkFile(File apkFile, String hash, String hashType)
throws NoSuchAlgorithmException {
if (!apkFile.exists()) {
return false;
}
Hasher hasher = new Hasher(hashType, apkFile);
return hasher.match(hash);
}
public void sendBroadcastInstall(Uri uri, Uri originatingUri, String action,
PendingIntent pendingIntent) {
sendBroadcastInstall(uri, originatingUri, action, pendingIntent, null);
}
public void sendBroadcastInstall(Uri uri, Uri originatingUri, String action) {
sendBroadcastInstall(uri, originatingUri, action, null, null);
}
public void sendBroadcastInstall(Uri uri, Uri originatingUri, String action, String errorMessage) {
sendBroadcastInstall(uri, originatingUri, action, null, errorMessage);
}
public void sendBroadcastInstall(Uri uri, Uri originatingUri, String action,
PendingIntent pendingIntent, String errorMessage) {
Intent intent = new Intent(action);
intent.setData(uri);
intent.putExtra(Installer.EXTRA_ORIGINATING_URI, originatingUri);
intent.putExtra(Installer.EXTRA_USER_INTERACTION_PI, pendingIntent);
if (!TextUtils.isEmpty(errorMessage)) {
intent.putExtra(Installer.EXTRA_ERROR_MESSAGE, errorMessage);
}
localBroadcastManager.sendBroadcast(intent);
}
public void sendBroadcastUninstall(String packageName, String action, String errorMessage) {
sendBroadcastUninstall(packageName, action, null, errorMessage);
}
public void sendBroadcastUninstall(String packageName, String action) {
sendBroadcastUninstall(packageName, action, null, null);
}
public void sendBroadcastUninstall(String packageName, String action,
PendingIntent pendingIntent) {
sendBroadcastUninstall(packageName, action, pendingIntent, null);
}
public void sendBroadcastUninstall(String packageName, String action,
PendingIntent pendingIntent, String errorMessage) {
Uri uri = Uri.fromParts("package", packageName, null);
Intent intent = new Intent(action);
intent.setData(uri); // for broadcast filtering
intent.putExtra(Installer.EXTRA_PACKAGE_NAME, packageName);
intent.putExtra(Installer.EXTRA_USER_INTERACTION_PI, pendingIntent);
if (!TextUtils.isEmpty(errorMessage)) {
intent.putExtra(Installer.EXTRA_ERROR_MESSAGE, errorMessage);
}
localBroadcastManager.sendBroadcast(intent);
}
public static IntentFilter getInstallIntentFilter(Uri uri) {
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(Installer.ACTION_INSTALL_STARTED);
intentFilter.addAction(Installer.ACTION_INSTALL_COMPLETE);
intentFilter.addAction(Installer.ACTION_INSTALL_INTERRUPTED);
intentFilter.addAction(Installer.ACTION_INSTALL_USER_INTERACTION);
intentFilter.addDataScheme(uri.getScheme());
intentFilter.addDataPath(uri.getPath(), PatternMatcher.PATTERN_LITERAL);
return intentFilter;
}
public static IntentFilter getUninstallIntentFilter(String packageName) {
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(Installer.ACTION_UNINSTALL_STARTED);
intentFilter.addAction(Installer.ACTION_UNINSTALL_COMPLETE);
intentFilter.addAction(Installer.ACTION_UNINSTALL_INTERRUPTED);
intentFilter.addAction(Installer.ACTION_UNINSTALL_USER_INTERACTION);
intentFilter.addDataScheme("package");
intentFilter.addDataPath(packageName, PatternMatcher.PATTERN_LITERAL);
return intentFilter;
}
protected abstract void installPackage(Uri uri, Uri originatingUri, String packageName);
protected abstract void uninstallPackage(String packageName);
protected abstract boolean isUnattended();
public abstract boolean handleOnActivityResult(int requestCode, int resultCode, Intent data);
}

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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);
}
return true;
default:
return false;
}
protected boolean isUnattended() {
return true;
}
public static final int INSTALL_REPLACE_EXISTING = 2;
/**
* Following return codes are copied from Android 5.1 source code
*/
/**
* Installation return code: this is passed to the {@link IPackageInstallObserver} by
* {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} on success.
*/
public static final int INSTALL_SUCCEEDED = 1;
/**
* Installation return code: this is passed to the {@link IPackageInstallObserver} by
* {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if the package is
* already installed.
*/
public static final int INSTALL_FAILED_ALREADY_EXISTS = -1;
/**
* Installation return code: this is passed to the {@link IPackageInstallObserver} by
* {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if the package archive
* file is invalid.
*/
public static final int INSTALL_FAILED_INVALID_APK = -2;
/**
* Installation return code: this is passed to the {@link IPackageInstallObserver} by
* {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if the URI passed in
* is invalid.
*/
public static final int INSTALL_FAILED_INVALID_URI = -3;
/**
* Installation return code: this is passed to the {@link IPackageInstallObserver} by
* {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if the package manager
* service found that the device didn't have enough storage space to install the app.
*/
public static final int INSTALL_FAILED_INSUFFICIENT_STORAGE = -4;
/**
* Installation return code: this is passed to the {@link IPackageInstallObserver} by
* {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if a
* package is already installed with the same name.
*/
public static final int INSTALL_FAILED_DUPLICATE_PACKAGE = -5;
/**
* Installation return code: this is passed to the {@link IPackageInstallObserver} by
* {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if
* the requested shared user does not exist.
*/
public static final int INSTALL_FAILED_NO_SHARED_USER = -6;
/**
* Installation return code: this is passed to the {@link IPackageInstallObserver} by
* {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if
* a previously installed package of the same name has a different signature
* than the new package (and the old package's data was not removed).
*/
public static final int INSTALL_FAILED_UPDATE_INCOMPATIBLE = -7;
/**
* Installation return code: this is passed to the {@link IPackageInstallObserver} by
* {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if
* the new package is requested a shared user which is already installed on the
* device and does not have matching signature.
*/
public static final int INSTALL_FAILED_SHARED_USER_INCOMPATIBLE = -8;
/**
* Installation return code: this is passed to the {@link IPackageInstallObserver} by
* {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if
* the new package uses a shared library that is not available.
*/
public static final int INSTALL_FAILED_MISSING_SHARED_LIBRARY = -9;
/**
* Installation return code: this is passed to the {@link IPackageInstallObserver} by
* {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if
* the new package uses a shared library that is not available.
*/
public static final int INSTALL_FAILED_REPLACE_COULDNT_DELETE = -10;
/**
* Installation return code: this is passed to the {@link IPackageInstallObserver} by
* {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if
* the new package failed while optimizing and validating its dex files,
* either because there was not enough storage or the validation failed.
*/
public static final int INSTALL_FAILED_DEXOPT = -11;
/**
* Installation return code: this is passed to the {@link IPackageInstallObserver} by
* {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if
* the new package failed because the current SDK version is older than
* that required by the package.
*/
public static final int INSTALL_FAILED_OLDER_SDK = -12;
/**
* Installation return code: this is passed to the {@link IPackageInstallObserver} by
* {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if
* the new package failed because it contains a content provider with the
* same authority as a provider already installed in the system.
*/
public static final int INSTALL_FAILED_CONFLICTING_PROVIDER = -13;
/**
* Installation return code: this is passed to the {@link IPackageInstallObserver} by
* {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if
* the new package failed because the current SDK version is newer than
* that required by the package.
*/
public static final int INSTALL_FAILED_NEWER_SDK = -14;
/**
* Installation return code: this is passed to the {@link IPackageInstallObserver} by
* {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if
* the new package failed because it has specified that it is a test-only
* package and the caller has not supplied the {@link #INSTALL_ALLOW_TEST}
* flag.
*/
public static final int INSTALL_FAILED_TEST_ONLY = -15;
/**
* Installation return code: this is passed to the {@link IPackageInstallObserver} by
* {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if
* the package being installed contains native code, but none that is
* compatible with the device's CPU_ABI.
*/
public static final int INSTALL_FAILED_CPU_ABI_INCOMPATIBLE = -16;
/**
* Installation return code: this is passed to the {@link IPackageInstallObserver} by
* {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if
* the new package uses a feature that is not available.
*/
public static final int INSTALL_FAILED_MISSING_FEATURE = -17;
// ------ Errors related to sdcard
/**
* Installation return code: this is passed to the {@link IPackageInstallObserver} by
* {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if
* a secure container mount point couldn't be accessed on external media.
*/
public static final int INSTALL_FAILED_CONTAINER_ERROR = -18;
/**
* Installation return code: this is passed to the {@link IPackageInstallObserver} by
* {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if
* the new package couldn't be installed in the specified install
* location.
*/
public static final int INSTALL_FAILED_INVALID_INSTALL_LOCATION = -19;
/**
* Installation return code: this is passed to the {@link IPackageInstallObserver} by
* {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if
* the new package couldn't be installed in the specified install
* location because the media is not available.
*/
public static final int INSTALL_FAILED_MEDIA_UNAVAILABLE = -20;
/**
* Installation return code: this is passed to the {@link IPackageInstallObserver} by
* {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if
* the new package couldn't be installed because the verification timed out.
*/
public static final int INSTALL_FAILED_VERIFICATION_TIMEOUT = -21;
/**
* Installation return code: this is passed to the {@link IPackageInstallObserver} by
* {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if
* the new package couldn't be installed because the verification did not succeed.
*/
public static final int INSTALL_FAILED_VERIFICATION_FAILURE = -22;
/**
* Installation return code: this is passed to the {@link IPackageInstallObserver} by
* {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if
* the package changed from what the calling program expected.
*/
public static final int INSTALL_FAILED_PACKAGE_CHANGED = -23;
/**
* Installation return code: this is passed to the {@link IPackageInstallObserver} by
* {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if
* the new package is assigned a different UID than it previously held.
*/
public static final int INSTALL_FAILED_UID_CHANGED = -24;
/**
* Installation return code: this is passed to the {@link IPackageInstallObserver} by
* {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if
* the new package has an older version code than the currently installed package.
*/
public static final int INSTALL_FAILED_VERSION_DOWNGRADE = -25;
/**
* Installation parse return code: this is passed to the {@link IPackageInstallObserver} by
* {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)}
* if the parser was given a path that is not a file, or does not end with the expected
* '.apk' extension.
*/
public static final int INSTALL_PARSE_FAILED_NOT_APK = -100;
/**
* Installation parse return code: this is passed to the {@link IPackageInstallObserver} by
* {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)}
* if the parser was unable to retrieve the AndroidManifest.xml file.
*/
public static final int INSTALL_PARSE_FAILED_BAD_MANIFEST = -101;
/**
* Installation parse return code: this is passed to the {@link IPackageInstallObserver} by
* {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)}
* if the parser encountered an unexpected exception.
*/
public static final int INSTALL_PARSE_FAILED_UNEXPECTED_EXCEPTION = -102;
/**
* Installation parse return code: this is passed to the {@link IPackageInstallObserver} by
* {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)}
* if the parser did not find any certificates in the .apk.
*/
public static final int INSTALL_PARSE_FAILED_NO_CERTIFICATES = -103;
/**
* Installation parse return code: this is passed to the {@link IPackageInstallObserver} by
* {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)}
* if the parser found inconsistent certificates on the files in the .apk.
*/
public static final int INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES = -104;
/**
* Installation parse return code: this is passed to the {@link IPackageInstallObserver} by
* {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)}
* if the parser encountered a CertificateEncodingException in one of the
* files in the .apk.
*/
public static final int INSTALL_PARSE_FAILED_CERTIFICATE_ENCODING = -105;
/**
* Installation parse return code: this is passed to the {@link IPackageInstallObserver} by
* {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)}
* if the parser encountered a bad or missing package name in the manifest.
*/
public static final int INSTALL_PARSE_FAILED_BAD_PACKAGE_NAME = -106;
/**
* Installation parse return code: this is passed to the {@link IPackageInstallObserver} by
* {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)}
* if the parser encountered a bad shared user id name in the manifest.
*/
public static final int INSTALL_PARSE_FAILED_BAD_SHARED_USER_ID = -107;
/**
* Installation parse return code: this is passed to the {@link IPackageInstallObserver} by
* {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)}
* if the parser encountered some structural problem in the manifest.
*/
public static final int INSTALL_PARSE_FAILED_MANIFEST_MALFORMED = -108;
/**
* Installation parse return code: this is passed to the {@link IPackageInstallObserver} by
* {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)}
* if the parser did not find any actionable tags (instrumentation or application)
* in the manifest.
*/
public static final int INSTALL_PARSE_FAILED_MANIFEST_EMPTY = -109;
/**
* Installation failed return code: this is passed to the {@link IPackageInstallObserver} by
* {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)}
* if the system failed to install the package because of system issues.
*/
public static final int INSTALL_FAILED_INTERNAL_ERROR = -110;
/**
* Installation failed return code: this is passed to the {@link IPackageInstallObserver} by
* {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)}
* if the system failed to install the package because the user is restricted from installing
* apps.
*/
public static final int INSTALL_FAILED_USER_RESTRICTED = -111;
/**
* Installation failed return code: this is passed to the {@link IPackageInstallObserver} by
* {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)}
* if the system failed to install the package because it is attempting to define a
* permission that is already defined by some existing package.
*
* The package name of the app which has already defined the permission is passed to
* a {@link PackageInstallObserver}, if any, as the {@link #EXTRA_EXISTING_PACKAGE}
* string extra; and the name of the permission being redefined is passed in the
* {@link #EXTRA_EXISTING_PERMISSION} string extra.
*/
public static final int INSTALL_FAILED_DUPLICATE_PERMISSION = -112;
/**
* Installation failed return code: this is passed to the {@link IPackageInstallObserver} by
* {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)}
* if the system failed to install the package because its packaged native code did not
* match any of the ABIs supported by the system.
*/
public static final int INSTALL_FAILED_NO_MATCHING_ABIS = -113;
/**
* Internal return code for NativeLibraryHelper methods to indicate that the package
* being processed did not contain any native code. This is placed here only so that
* it can belong to the same value space as the other install failure codes.
*/
public static final int NO_NATIVE_LIBRARIES = -114;
public static final int INSTALL_FAILED_ABORTED = -115;
/**
* Return code for when package deletion succeeds. This is passed to the
* {@link IPackageDeleteObserver} by {@link #deletePackage()} if the system
* succeeded in deleting the package.
*/
public static final int DELETE_SUCCEEDED = 1;
/**
* Deletion failed return code: this is passed to the
* {@link IPackageDeleteObserver} by {@link #deletePackage()} if the system
* failed to delete the package for an unspecified reason.
*/
public static final int DELETE_FAILED_INTERNAL_ERROR = -1;
/**
* Deletion failed return code: this is passed to the
* {@link IPackageDeleteObserver} by {@link #deletePackage()} if the system
* failed to delete the package because it is the active DevicePolicy
* manager.
*/
public static final int DELETE_FAILED_DEVICE_POLICY_MANAGER = -2;
/**
* Deletion failed return code: this is passed to the
* {@link IPackageDeleteObserver} by {@link #deletePackage()} if the system
* failed to delete the package since the user is restricted.
*/
public static final int DELETE_FAILED_USER_RESTRICTED = -3;
/**
* Deletion failed return code: this is passed to the
* {@link IPackageDeleteObserver} by {@link #deletePackage()} if the system
* failed to delete the package because a profile
* or device owner has marked the package as uninstallable.
*/
public static final int DELETE_FAILED_OWNER_BLOCKED = -4;
public static final int DELETE_FAILED_ABORTED = -5;
}

View File

@ -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");
}

View File

@ -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;
@ -55,7 +86,7 @@ public class AppDiff {
String pkgName = mPkgInfo.packageName;
// Check if there is already a package on the device with this name
// but it has been renamed to something else.
final String[] oldName = mPm.canonicalToCurrentPackageNames(new String[] {pkgName});
final String[] oldName = mPm.canonicalToCurrentPackageNames(new String[]{pkgName});
if (oldName != null && oldName.length > 0 && oldName[0] != null) {
pkgName = oldName[0];
mPkgInfo.packageName = pkgName;

View File

@ -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);
}

View File

@ -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();
}

View File

@ -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();
}
}

View File

@ -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");
}

View File

@ -1,6 +1,7 @@
package org.fdroid.fdroid.views.swap;
import android.app.Activity;
import android.app.PendingIntent;
import android.bluetooth.BluetoothAdapter;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
@ -43,6 +44,7 @@ import org.fdroid.fdroid.data.App;
import org.fdroid.fdroid.data.NewRepoConfig;
import org.fdroid.fdroid.installer.InstallManagerService;
import org.fdroid.fdroid.installer.Installer;
import org.fdroid.fdroid.installer.InstallerService;
import org.fdroid.fdroid.localrepo.LocalRepoManager;
import org.fdroid.fdroid.localrepo.SwapService;
import org.fdroid.fdroid.localrepo.peers.Peer;
@ -119,7 +121,6 @@ public class SwapWorkflowActivity extends AppCompatActivity {
private PrepareSwapRepo updateSwappableAppsTask;
private NewRepoConfig confirmSwapConfig;
private LocalBroadcastManager localBroadcastManager;
private BroadcastReceiver downloadCompleteReceiver;
@NonNull
private final ServiceConnection serviceConnection = new ServiceConnection() {
@ -773,7 +774,7 @@ public class SwapWorkflowActivity extends AppCompatActivity {
public void install(@NonNull final App app) {
final Apk apk = ApkProvider.Helper.find(this, app.packageName, app.suggestedVersionCode);
String urlString = apk.getUrl();
downloadCompleteReceiver = new BroadcastReceiver() {
BroadcastReceiver downloadCompleteReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String path = intent.getStringExtra(Downloader.EXTRA_DOWNLOAD_PATH);
@ -786,25 +787,44 @@ public class SwapWorkflowActivity extends AppCompatActivity {
}
private void handleDownloadComplete(File apkFile, String packageName, String urlString) {
Uri originatingUri = Uri.parse(urlString);
Uri localUri = Uri.fromFile(apkFile);
try {
Installer.getActivityInstaller(this, new Installer.InstallerCallback() {
@Override
public void onSuccess(int operation) {
// TODO: Don't reload the view weely-neely, but rather get the view to listen
// for broadcasts that say the install was complete.
showRelevantView(true);
}
@Override
public void onError(int operation, int errorCode) {
// TODO: Boo!
}
}).installPackage(apkFile, packageName, urlString);
localBroadcastManager.unregisterReceiver(downloadCompleteReceiver);
} catch (Installer.InstallFailedException e) {
// TODO: Handle exception properly
}
localBroadcastManager.registerReceiver(installReceiver,
Installer.getInstallIntentFilter(Uri.fromFile(apkFile)));
InstallerService.install(this, localUri, originatingUri, packageName);
}
private final BroadcastReceiver installReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
switch (intent.getAction()) {
case Installer.ACTION_INSTALL_STARTED:
break;
case Installer.ACTION_INSTALL_COMPLETE:
localBroadcastManager.unregisterReceiver(this);
showRelevantView(true);
break;
case Installer.ACTION_INSTALL_INTERRUPTED:
localBroadcastManager.unregisterReceiver(this);
// TODO: handle errors!
break;
case Installer.ACTION_INSTALL_USER_INTERACTION:
PendingIntent installPendingIntent =
intent.getParcelableExtra(Installer.EXTRA_USER_INTERACTION_PI);
try {
installPendingIntent.send();
} catch (PendingIntent.CanceledException e) {
Log.e(TAG, "PI canceled", e);
}
break;
default:
throw new RuntimeException("intent action not handled!");
}
}
};
}

View File

@ -21,37 +21,35 @@
user before it is installed.
-->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="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:id="@+id/install_confirm"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:paddingTop="4dip"
android:text="@string/install_confirm"
android:textAppearance="?android:attr/textAppearanceMedium" />
<ImageView
android:id="@+id/divider"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:background="?android:attr/dividerHorizontal"
android:visibility="gone" />
android:id="@+id/divider"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:background="?android:attr/dividerHorizontal"
android:visibility="gone" />
<FrameLayout
android:id="@+id/filler"
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"
android:layout_height="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>
@ -85,64 +87,68 @@
android:id="@android:id/tabcontent"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_weight="0"/>
android:layout_weight="0" />
<android.support.v4.view.ViewPager
android:id="@+id/pager"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"/>
android:layout_weight="1" />
</LinearLayout>
</TabHost>
<!-- OK confirm and cancel buttons. -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:divider="?android:attr/dividerHorizontal"
android:showDividers="beginning">
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:divider="?android:attr/dividerHorizontal"
android:orientation="vertical"
android:showDividers="beginning">
<LinearLayout
style="?android:attr/buttonBarStyle"
android:layout_width="match_parent"
style="?android:attr/buttonBarStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:measureWithLargestChild="true"
android:orientation="horizontal">
<LinearLayout
android:id="@+id/leftSpacer"
android:layout_width="0dip"
android:layout_height="wrap_content"
android:layout_weight="0.25"
android:orientation="horizontal"
android:measureWithLargestChild="true">
android:visibility="gone" />
<LinearLayout android:id="@+id/leftSpacer"
android:layout_weight="0.25"
android:layout_width="0dip"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:visibility="gone" />
<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:maxLines="2"
android:text="@string/cancel" />
<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"
style="?android:attr/buttonBarButtonStyle" />
<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:filterTouchesWhenObscured="true"
android:maxLines="2"
android:text="@string/next" />
<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"
style="?android:attr/buttonBarButtonStyle" />
<LinearLayout android:id="@+id/rightSpacer"
android:layout_width="0dip"
android:layout_weight="0.25"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:visibility="gone" />
<LinearLayout
android:id="@+id/rightSpacer"
android:layout_width="0dip"
android:layout_height="wrap_content"
android:layout_weight="0.25"
android:orientation="horizontal"
android:visibility="gone" />
</LinearLayout>
</LinearLayout>

View File

@ -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>

View File

@ -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,36 +20,34 @@
user before it is installed.
-->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="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:id="@+id/install_confirm"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:paddingTop="4dip"
android:text="@string/install_confirm"
android:textAppearance="?android:attr/textAppearanceMedium" />
<ImageView
android:id="@+id/divider"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:visibility="gone" />
android:id="@+id/divider"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:visibility="gone" />
<FrameLayout
android:id="@+id/filler"
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"
android:layout_height="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>
@ -84,63 +85,65 @@
android:id="@android:id/tabcontent"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_weight="0"/>
android:layout_weight="0" />
<android.support.v4.view.ViewPager
android:id="@+id/pager"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"/>
android:layout_weight="1" />
</LinearLayout>
</TabHost>
<!-- OK confirm and cancel buttons. -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:divider="?android:attr/dividerHorizontal"
android:showDividers="beginning">
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:divider="?android:attr/dividerHorizontal"
android:orientation="vertical"
android:showDividers="beginning">
<LinearLayout
android:layout_width="match_parent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:measureWithLargestChild="true"
android:orientation="horizontal">
<LinearLayout
android:id="@+id/leftSpacer"
android:layout_width="0dip"
android:layout_height="wrap_content"
android:layout_weight="0.25"
android:orientation="horizontal"
android:measureWithLargestChild="true">
android:visibility="gone" />
<LinearLayout android:id="@+id/leftSpacer"
android:layout_weight="0.25"
android:layout_width="0dip"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:visibility="gone" />
<Button
android:id="@+id/cancel_button"
android:layout_width="0dip"
android:layout_height="wrap_content"
android:layout_gravity="start"
android:layout_weight="1"
android:maxLines="2"
android:text="@string/cancel" />
<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"
/>
<Button
android:id="@+id/ok_button"
android:layout_width="0dip"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:layout_weight="1"
android:filterTouchesWhenObscured="true"
android:maxLines="2"
android:text="@string/next" />
<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"
/>
<LinearLayout android:id="@+id/rightSpacer"
android:layout_width="0dip"
android:layout_weight="0.25"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:visibility="gone" />
<LinearLayout
android:id="@+id/rightSpacer"
android:layout_width="0dip"
android:layout_height="wrap_content"
android:layout_weight="0.25"
android:orientation="horizontal"
android:visibility="gone" />
</LinearLayout>
</LinearLayout>

View File

@ -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>

View File

@ -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>

View File

@ -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>