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 <activity
android:name=".privileged.views.InstallConfirmActivity" android:name=".privileged.views.InstallConfirmActivity"
android:label="@string/menu_install" android:label="@string/menu_install"
android:theme="@style/MinWithDialogBaseThemeLight"
android:excludeFromRecents="true"
android:parentActivityName=".FDroid" android:parentActivityName=".FDroid"
android:configChanges="layoutDirection|locale" > android:configChanges="layoutDirection|locale" >
<meta-data <meta-data
android:name="android.support.PARENT_ACTIVITY" android:name="android.support.PARENT_ACTIVITY"
android:value=".FDroid" /> android:value=".FDroid" />
</activity> </activity>
<activity
android:name=".privileged.views.UninstallDialogActivity"
android:excludeFromRecents="true"
android:theme="@style/AppThemeTransparent" />
<activity <activity
android:name=".views.ManageReposActivity" android:name=".views.ManageReposActivity"
android:label="@string/app_name" android:label="@string/app_name"
@ -401,6 +407,14 @@
<action android:name="android.intent.action.BOOT_COMPLETED" /> <action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter> </intent-filter>
</receiver> </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" > <receiver android:name=".receiver.StartupReceiver" >
<intent-filter> <intent-filter>
@ -440,6 +454,9 @@
<service <service
android:name=".net.DownloaderService" android:name=".net.DownloaderService"
android:exported="false" /> android:exported="false" />
<service
android:name=".installer.InstallerService"
android:exported="false" />
<service <service
android:name=".CleanCacheService" android:name=".CleanCacheService"
android:exported="false" /> android:exported="false" />

View File

@ -22,6 +22,7 @@
package org.fdroid.fdroid; package org.fdroid.fdroid;
import android.app.Activity; import android.app.Activity;
import android.app.PendingIntent;
import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothAdapter;
import android.content.ActivityNotFoundException; import android.content.ActivityNotFoundException;
import android.content.BroadcastReceiver; import android.content.BroadcastReceiver;
@ -78,17 +79,16 @@ import com.nostra13.universalimageloader.core.ImageLoader;
import com.nostra13.universalimageloader.core.assist.ImageScaleType; import com.nostra13.universalimageloader.core.assist.ImageScaleType;
import org.fdroid.fdroid.Utils.CommaSeparatedList; import org.fdroid.fdroid.Utils.CommaSeparatedList;
import org.fdroid.fdroid.compat.PackageManagerCompat;
import org.fdroid.fdroid.data.Apk; import org.fdroid.fdroid.data.Apk;
import org.fdroid.fdroid.data.ApkProvider; import org.fdroid.fdroid.data.ApkProvider;
import org.fdroid.fdroid.data.App; import org.fdroid.fdroid.data.App;
import org.fdroid.fdroid.data.AppProvider; import org.fdroid.fdroid.data.AppProvider;
import org.fdroid.fdroid.data.InstalledAppProvider; import org.fdroid.fdroid.data.InstalledAppProvider;
import org.fdroid.fdroid.data.RepoProvider; import org.fdroid.fdroid.data.RepoProvider;
import org.fdroid.fdroid.installer.InstallManagerService;
import org.fdroid.fdroid.installer.Installer; import org.fdroid.fdroid.installer.Installer;
import org.fdroid.fdroid.installer.Installer.InstallFailedException; import org.fdroid.fdroid.installer.InstallManagerService;
import org.fdroid.fdroid.installer.Installer.InstallerCallback; import org.fdroid.fdroid.installer.InstallerFactory;
import org.fdroid.fdroid.installer.InstallerService;
import org.fdroid.fdroid.net.Downloader; import org.fdroid.fdroid.net.Downloader;
import org.fdroid.fdroid.net.DownloaderService; import org.fdroid.fdroid.net.DownloaderService;
@ -101,6 +101,8 @@ public class AppDetails extends AppCompatActivity {
private static final String TAG = "AppDetails"; private static final String TAG = "AppDetails";
private static final int REQUEST_ENABLE_BLUETOOTH = 2; 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_APPID = "appid";
public static final String EXTRA_FROM = "from"; public static final String EXTRA_FROM = "from";
@ -319,7 +321,6 @@ public class AppDetails extends AppCompatActivity {
private int startingIgnoreThis; private int startingIgnoreThis;
private final Context context = this; private final Context context = this;
private Installer installer;
private AppDetailsHeaderFragment headerFragment; private AppDetailsHeaderFragment headerFragment;
@ -375,8 +376,6 @@ public class AppDetails extends AppCompatActivity {
packageManager = getPackageManager(); packageManager = getPackageManager();
installer = Installer.getActivityInstaller(this, packageManager, myInstallerCallback);
// Get the preferences we're going to use in this Activity... // Get the preferences we're going to use in this Activity...
ConfigurationChangeHelper previousData = (ConfigurationChangeHelper) getLastCustomNonConfigurationInstance(); ConfigurationChangeHelper previousData = (ConfigurationChangeHelper) getLastCustomNonConfigurationInstance();
if (previousData != null) { if (previousData != null) {
@ -530,13 +529,12 @@ public class AppDetails extends AppCompatActivity {
private final BroadcastReceiver completeReceiver = new BroadcastReceiver() { private final BroadcastReceiver completeReceiver = new BroadcastReceiver() {
@Override @Override
public void onReceive(Context context, Intent intent) { 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(); 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() { private void onAppChanged() {
if (!reset(app.packageName)) { if (!reset(app.packageName)) {
this.finish(); this.finish();
@ -796,7 +896,7 @@ public class AppDetails extends AppCompatActivity {
return true; return true;
case UNINSTALL: case UNINSTALL:
removeApk(app.packageName); uninstallApk(app.packageName);
return true; return true;
case IGNOREALL: case IGNOREALL:
@ -875,76 +975,43 @@ public class AppDetails extends AppCompatActivity {
} }
private void initiateInstall(Apk apk) { 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(); activeDownloadUrlString = apk.getUrl();
registerDownloaderReceivers(); registerDownloaderReceivers();
headerFragment.startProgress(); headerFragment.startProgress();
InstallManagerService.queue(this, app, apk); InstallManagerService.queue(this, app, apk);
} }
private void removeApk(String packageName) { private void uninstallApk(String packageName) {
try { Installer installer = InstallerFactory.create(this, packageName);
installer.deletePackage(packageName); Intent intent = installer.getUninstallScreen(packageName);
} catch (InstallFailedException e) { if (intent != null) {
Log.e(TAG, "Android not compatible with this Installer!", e); // uninstall screen required
Utils.debugLog(TAG, "screen screen required");
startActivityForResult(intent, REQUEST_UNINSTALL_DIALOG);
return;
} }
startUninstall();
} }
private final Installer.InstallerCallback myInstallerCallback = new Installer.InstallerCallback() { private void startUninstall() {
localBroadcastManager.registerReceiver(uninstallReceiver,
@Override Installer.getUninstallIntentFilter(app.packageName));
public void onSuccess(final int operation) { InstallerService.uninstall(context, app.packageName);
runOnUiThread(new Runnable() { }
@Override
public void run() {
if (operation == Installer.InstallerCallback.OPERATION_INSTALL) {
PackageManagerCompat.setInstaller(packageManager, app.packageName);
}
onAppChanged();
}
});
}
@Override
public void onError(int operation, final int errorCode) {
if (errorCode == InstallerCallback.ERROR_CODE_CANCELED) {
return;
}
final int title, body;
if (operation == InstallerCallback.OPERATION_INSTALL) {
title = R.string.install_error_title;
switch (errorCode) {
case ERROR_CODE_CANNOT_PARSE:
body = R.string.install_error_cannot_parse;
break;
default: // ERROR_CODE_OTHER
body = R.string.install_error_unknown;
break;
}
} else { // InstallerCallback.OPERATION_DELETE
title = R.string.uninstall_error_title;
switch (errorCode) {
default: // ERROR_CODE_OTHER
body = R.string.uninstall_error_unknown;
break;
}
}
runOnUiThread(new Runnable() {
@Override
public void run() {
onAppChanged();
Log.e(TAG, "Installer aborted with errorCode: " + errorCode);
AlertDialog.Builder alertBuilder = new AlertDialog.Builder(AppDetails.this);
alertBuilder.setTitle(title);
alertBuilder.setMessage(body);
alertBuilder.setNeutralButton(android.R.string.ok, null);
alertBuilder.create().show();
}
});
}
};
private void launchApk(String packageName) { private void launchApk(String packageName) {
Intent intent = packageManager.getLaunchIntentForPackage(packageName); Intent intent = packageManager.getLaunchIntentForPackage(packageName);
@ -963,15 +1030,22 @@ public class AppDetails extends AppCompatActivity {
@Override @Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) { protected void onActivityResult(int requestCode, int resultCode, Intent data) {
// handle cases for install manager first
if (installer.handleOnActivityResult(requestCode, resultCode, data)) {
return;
}
switch (requestCode) { switch (requestCode) {
case REQUEST_ENABLE_BLUETOOTH: case REQUEST_ENABLE_BLUETOOTH:
fdroidApp.sendViaBluetooth(this, resultCode, app.packageName); fdroidApp.sendViaBluetooth(this, resultCode, app.packageName);
break; 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 // If "launchable", launch
activity.launchApk(app.packageName); activity.launchApk(app.packageName);
} else { } else {
activity.removeApk(app.packageName); activity.uninstallApk(app.packageName);
} }
} else if (app.suggestedVersionCode > 0) { } else if (app.suggestedVersionCode > 0) {
// If not installed, install // If not installed, install
@ -1635,7 +1709,7 @@ public class AppDetails extends AppCompatActivity {
} }
void remove() { void remove() {
appDetails.removeApk(appDetails.getApp().packageName); appDetails.uninstallApk(appDetails.getApp().packageName);
} }
@Override @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() { public static void enableSpongyCastle() {
Security.addProvider(SPONGYCASTLE_PROVIDER); Security.addProvider(SPONGYCASTLE_PROVIDER);
} }

View File

@ -57,6 +57,7 @@ import java.security.cert.CertificateEncodingException;
import java.text.DateFormat; import java.text.DateFormat;
import java.text.ParseException; import java.text.ParseException;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date; import java.util.Date;
import java.util.Formatter; import java.util.Formatter;
import java.util.Iterator; 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 * 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 * 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. * 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. * is fine since that does the right thing.
*/ */
public static File getApkCacheDir(Context context) { public static File getApkCacheDir(Context context) {
@ -457,6 +458,19 @@ public final class Utils {
return splitter.iterator(); 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) { public boolean contains(String v) {
for (final String s : this) { for (final String s : this) {
if (s.equals(v)) { 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) { public static Apk find(Context context, String packageName, int versionCode, String[] projection) {
ContentResolver resolver = context.getContentResolver();
final Uri uri = getContentUri(packageName, versionCode); 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); Cursor cursor = resolver.query(uri, projection, null, null, null);
Apk apk = null; Apk apk = null;
if (cursor != null) { if (cursor != null) {

View File

@ -42,12 +42,12 @@ public class ApkSignatureVerifier {
private static final String TAG = "ApkSignatureVerifier"; private static final String TAG = "ApkSignatureVerifier";
private final Context mContext; private final Context context;
private final PackageManager mPm; private final PackageManager pm;
ApkSignatureVerifier(Context context) { ApkSignatureVerifier(Context context) {
mContext = context; this.context = context;
mPm = context.getPackageManager(); pm = context.getPackageManager();
} }
public boolean hasFDroidSignature(File apkFile) { public boolean hasFDroidSignature(File apkFile) {
@ -66,7 +66,7 @@ public class ApkSignatureVerifier {
private byte[] getApkSignature(File apkFile) { private byte[] getApkSignature(File apkFile) {
final String pkgPath = apkFile.getAbsolutePath(); 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); return signatureToBytes(pkgInfo.signatures);
} }
@ -74,7 +74,7 @@ public class ApkSignatureVerifier {
try { try {
// we do check the byte array of *all* signatures // we do check the byte array of *all* signatures
@SuppressLint("PackageManagerGetSignatures") @SuppressLint("PackageManagerGetSignatures")
PackageInfo pkgInfo = mPm.getPackageInfo(mContext.getPackageName(), PackageInfo pkgInfo = pm.getPackageInfo(context.getPackageName(),
PackageManager.GET_SIGNATURES); PackageManager.GET_SIGNATURES);
return signatureToBytes(pkgInfo.signatures); return signatureToBytes(pkgInfo.signatures);
} catch (PackageManager.NameNotFoundException e) { } 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 * This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License * modify it under the terms of the GNU General Public License
@ -19,82 +19,82 @@
package org.fdroid.fdroid.installer; package org.fdroid.fdroid.installer;
import android.app.Activity; import android.app.PendingIntent;
import android.content.ActivityNotFoundException; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.net.Uri; import android.net.Uri;
import android.util.Log;
import org.fdroid.fdroid.Utils;
import java.io.File; import java.io.File;
/** /**
* For Android < 4: Default Installer using the public PackageManager API of * The default installer of F-Droid. It uses the normal Intents APIs of Android
* Android to install/delete packages. This starts a Activity from the Android * to install apks. Its main inner workings are encapsulated in DefaultInstallerActivity.
* OS showing all permissions/changed permissions. The the user needs to * <p/>
* manually press an install button, this Installer cannot be used for * This is installer requires user interaction and thus install/uninstall directly
* unattended installations. * return PendingIntents.
*/ */
public class DefaultInstaller extends Installer { public class DefaultInstaller extends Installer {
private final Activity mActivity;
public DefaultInstaller(Activity activity, PackageManager pm, InstallerCallback callback) private static final String TAG = "DefaultInstaller";
throws InstallFailedException {
super(activity, pm, callback); DefaultInstaller(Context context) {
this.mActivity = activity; super(context);
} }
private static final int REQUEST_CODE_INSTALL = 0;
private static final int REQUEST_CODE_DELETE = 1;
@Override @Override
protected void installPackageInternal(File apkFile) throws InstallFailedException { protected void installPackage(Uri uri, Uri originatingUri, String packageName) {
Intent intent = new Intent(); sendBroadcastInstall(uri, originatingUri, Installer.ACTION_INSTALL_STARTED);
intent.setAction(Intent.ACTION_VIEW);
intent.setDataAndType(Uri.fromFile(apkFile), Utils.debugLog(TAG, "DefaultInstaller uri: " + uri + " file: " + new File(uri.getPath()));
"application/vnd.android.package-archive");
Uri sanitizedUri;
try { try {
mActivity.startActivityForResult(intent, REQUEST_CODE_INSTALL); sanitizedUri = Installer.prepareApkFile(context, uri, packageName);
} catch (ActivityNotFoundException e) { } catch (Installer.InstallFailedException e) {
throw new 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 @Override
protected void deletePackageInternal(String packageName) throws InstallFailedException { protected void uninstallPackage(String packageName) {
try { sendBroadcastUninstall(packageName, Installer.ACTION_UNINSTALL_STARTED);
PackageInfo pkgInfo = mPm.getPackageInfo(packageName, 0);
Uri uri = Uri.fromParts("package", pkgInfo.packageName, null); Intent uninstallIntent = new Intent(context, DefaultInstallerActivity.class);
Intent intent = new Intent(Intent.ACTION_DELETE, uri); uninstallIntent.setAction(DefaultInstallerActivity.ACTION_UNINSTALL_PACKAGE);
try { uninstallIntent.putExtra(
mActivity.startActivityForResult(intent, REQUEST_CODE_DELETE); DefaultInstallerActivity.EXTRA_UNINSTALL_PACKAGE_NAME, packageName);
} catch (ActivityNotFoundException e) { PendingIntent uninstallPendingIntent = PendingIntent.getActivity(
throw new InstallFailedException(e); context.getApplicationContext(),
} packageName.hashCode(),
} catch (PackageManager.NameNotFoundException e) { uninstallIntent,
// already checked in super class PendingIntent.FLAG_UPDATE_CURRENT);
}
sendBroadcastUninstall(packageName,
Installer.ACTION_UNINSTALL_USER_INTERACTION, uninstallPendingIntent);
} }
@Override @Override
public boolean handleOnActivityResult(int requestCode, int resultCode, Intent data) { protected boolean isUnattended() {
/** return false;
* 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;
}
} }
} }

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.AppDetails;
import org.fdroid.fdroid.R; import org.fdroid.fdroid.R;
import org.fdroid.fdroid.Utils; import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.compat.PackageManagerCompat;
import org.fdroid.fdroid.data.Apk; import org.fdroid.fdroid.data.Apk;
import org.fdroid.fdroid.data.App; import org.fdroid.fdroid.data.App;
import org.fdroid.fdroid.net.Downloader; import org.fdroid.fdroid.net.Downloader;
@ -85,18 +86,6 @@ public class InstallManagerService extends Service {
*/ */
private final HashMap<String, BroadcastReceiver[]> receivers = new HashMap<>(3); 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 LocalBroadcastManager localBroadcastManager;
private NotificationManager notificationManager; private NotificationManager notificationManager;
@ -180,7 +169,7 @@ public class InstallManagerService extends Service {
sendBroadcast(intent.getData(), Downloader.ACTION_STARTED, apkFilePath); sendBroadcast(intent.getData(), Downloader.ACTION_STARTED, apkFilePath);
sendBroadcast(intent.getData(), Downloader.ACTION_COMPLETE, apkFilePath); sendBroadcast(intent.getData(), Downloader.ACTION_COMPLETE, apkFilePath);
} else { } else {
Utils.debugLog(TAG, " delete and download again " + urlString + " " + apkFilePath); Utils.debugLog(TAG, "delete and download again " + urlString + " " + apkFilePath);
apkFilePath.delete(); apkFilePath.delete();
DownloaderService.queue(this, urlString); DownloaderService.queue(this, urlString);
} }
@ -234,22 +223,26 @@ public class InstallManagerService extends Service {
BroadcastReceiver completeReceiver = new BroadcastReceiver() { BroadcastReceiver completeReceiver = new BroadcastReceiver() {
@Override @Override
public void onReceive(Context context, Intent intent) { public void onReceive(Context context, Intent intent) {
String urlString = intent.getDataString(); // elsewhere called urlString
// TODO these need to be removed based on whether they are fed to InstallerService or not Uri originatingUri = intent.getData();
Apk apk = removeFromActive(urlString); File localFile = new File(intent.getStringExtra(Downloader.EXTRA_DOWNLOAD_PATH));
if (AppDetails.isAppVisible(apk.packageName)) { Uri localUri = Uri.fromFile(localFile);
cancelNotification(urlString);
} else { Utils.debugLog(TAG, "download completed of " + originatingUri
notifyDownloadComplete(urlString, apk); + " to " + localUri);
}
unregisterDownloaderReceivers(urlString); unregisterDownloaderReceivers(intent.getDataString());
registerInstallerReceivers(localUri);
Apk apk = ACTIVE_APKS.get(originatingUri.toString());
InstallerService.install(context, localUri, originatingUri, apk.packageName);
} }
}; };
BroadcastReceiver interruptedReceiver = new BroadcastReceiver() { BroadcastReceiver interruptedReceiver = new BroadcastReceiver() {
@Override @Override
public void onReceive(Context context, Intent intent) { public void onReceive(Context context, Intent intent) {
String urlString = intent.getDataString(); String urlString = intent.getDataString();
Apk apk = removeFromActive(urlString); removeFromActive(urlString);
unregisterDownloaderReceivers(urlString); unregisterDownloaderReceivers(urlString);
cancelNotification(urlString); cancelNotification(urlString);
} }
@ -265,6 +258,70 @@ public class InstallManagerService extends Service {
receivers.put(urlString, new BroadcastReceiver[]{ receivers.put(urlString, new BroadcastReceiver[]{
startedReceiver, progressReceiver, completeReceiver, interruptedReceiver, 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) { private NotificationCompat.Builder createNotificationBuilder(String urlString, Apk apk) {
@ -273,7 +330,7 @@ public class InstallManagerService extends Service {
.setAutoCancel(false) .setAutoCancel(false)
.setOngoing(true) .setOngoing(true)
.setContentIntent(getAppDetailsIntent(downloadUrlId, apk)) .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), .addAction(R.drawable.ic_cancel_black_24dp, getString(R.string.cancel),
DownloaderService.getCancelPendingIntent(this, urlString)) DownloaderService.getCancelPendingIntent(this, urlString))
.setSmallIcon(android.R.drawable.stat_sys_download) .setSmallIcon(android.R.drawable.stat_sys_download)
@ -281,18 +338,8 @@ public class InstallManagerService extends Service {
.setProgress(100, 0, true); .setProgress(100, 0, true);
} }
private String getAppName(String urlString, Apk apk) { private String getAppName(Apk apk) {
App app = ACTIVE_APPS.get(apk.packageName); return ACTIVE_APPS.get(apk.packageName).name;
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;
}
} }
/** /**
@ -319,14 +366,14 @@ public class InstallManagerService extends Service {
* Removing the progress bar from a notification should cause the notification's content * Removing the progress bar from a notification should cause the notification's content
* text to return to normal size</a> * 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; String title;
try { try {
PackageManager pm = getPackageManager(); PackageManager pm = getPackageManager();
title = String.format(getString(R.string.tap_to_update_format), title = String.format(getString(R.string.tap_to_update_format),
pm.getApplicationLabel(pm.getApplicationInfo(apk.packageName, 0))); pm.getApplicationLabel(pm.getApplicationInfo(apk.packageName, 0)));
} catch (PackageManager.NameNotFoundException e) { } 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(); int downloadUrlId = urlString.hashCode();
@ -335,13 +382,38 @@ public class InstallManagerService extends Service {
.setAutoCancel(true) .setAutoCancel(true)
.setOngoing(false) .setOngoing(false)
.setContentTitle(title) .setContentTitle(title)
.setContentIntent(getAppDetailsIntent(downloadUrlId, apk)) .setContentIntent(installPendingIntent)
.setSmallIcon(android.R.drawable.stat_sys_download_done) .setSmallIcon(android.R.drawable.stat_sys_download_done)
.setContentText(getString(R.string.tap_to_install)) .setContentText(getString(R.string.tap_to_install))
.build(); .build();
notificationManager.notify(downloadUrlId, notification); 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 * Cancel the {@link Notification} tied to {@code urlString}, which is the
* unique ID used to represent a given APK file. {@link String#hashCode()} * 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) { private static void addToActive(String urlString, App app, Apk apk) {
ACTIVE_APKS.put(urlString, apk); ACTIVE_APKS.put(urlString, apk);
ACTIVE_APPS.put(app.packageName, app); 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 * {@link BroadcastReceiver}s, in which case {@code urlString} would not
* find anything in the active maps. * 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) { private static Apk removeFromActive(String urlString) {
Apk apk = ACTIVE_APKS.remove(urlString); Apk apk = ACTIVE_APKS.remove(urlString);
if (apk != null) { 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 * This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License * modify it under the terms of the GNU General Public License
@ -19,24 +19,26 @@
package org.fdroid.fdroid.installer; package org.fdroid.fdroid.installer;
import android.app.Activity; import android.app.PendingIntent;
import android.app.NotificationManager;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager; 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.text.TextUtils;
import android.util.Log;
import org.apache.commons.io.FileUtils; import org.apache.commons.io.FileUtils;
import org.fdroid.fdroid.AndroidXMLDecompress; import org.fdroid.fdroid.AndroidXMLDecompress;
import org.fdroid.fdroid.BuildConfig;
import org.fdroid.fdroid.Hasher; 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.Apk;
import org.fdroid.fdroid.data.ApkProvider; import org.fdroid.fdroid.data.ApkProvider;
import org.fdroid.fdroid.data.SanitizedFile; 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.File;
import java.io.IOException; import java.io.IOException;
@ -44,22 +46,32 @@ import java.security.NoSuchAlgorithmException;
import java.util.Map; 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 { public abstract class Installer {
final Context mContext; final Context context;
final PackageManager mPm; final PackageManager pm;
final InstallerCallback mCallback; 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 * Same as http://developer.android.com/reference/android/content/Intent.html#EXTRA_ORIGINATING_URI
* is running on. This could be due to a broken superuser in case of * In InstallManagerService often called urlString
* RootInstaller or due to an incompatible Android version in case of
* SystemPermissionInstaller
*/ */
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 { public static class InstallFailedException extends Exception {
private static final long serialVersionUID = -8343133906463328027L; private static final long serialVersionUID = -8343133906463328027L;
@ -73,116 +85,31 @@ public abstract class Installer {
} }
} }
/** Installer(Context context) {
* Callback from Installer. NOTE: This callback can be in a different thread this.context = context;
* than the UI thread this.pm = context.getPackageManager();
*/ localBroadcastManager = LocalBroadcastManager.getInstance(context);
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, PackageManager pm, InstallerCallback callback) public static Uri prepareApkFile(Context context, Uri uri, String packageName)
throws InstallFailedException { throws InstallFailedException {
this.mContext = context;
this.mPm = pm;
this.mCallback = callback;
}
public static Installer getActivityInstaller(Activity activity, InstallerCallback callback) { File apkFile = new File(uri.getPath());
return getActivityInstaller(activity, activity.getPackageManager(), callback);
}
/** SanitizedFile sanitizedApkFile = null;
* 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;
try { try {
Map<String, Object> attributes = AndroidXMLDecompress.getManifestHeaderAttributes(apkFile.getAbsolutePath()); Map<String, Object> attributes = AndroidXMLDecompress.getManifestHeaderAttributes(apkFile.getAbsolutePath());
/* This isn't really needed, but might as well since we have the data already */ /* 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"))) { 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")) { if (!attributes.containsKey("versionCode")) {
throw new InstallFailedException(apkFile + " is missing versionCode!"); throw new InstallFailedException(uri + " is missing versionCode!");
} }
int versionCode = (Integer) attributes.get("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,
ApkProvider.DataColumns.HASH_TYPE, 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 * of the app to prevent attacks based on other apps swapping the file
* out during the install process. Most likely, apkFile was just downloaded, * out during the install process. Most likely, apkFile was just downloaded,
* so it should still be in the RAM disk cache */ * so it should still be in the RAM disk cache */
apkToInstall = SanitizedFile.knownSanitized(File.createTempFile("install-", ".apk", mContext.getFilesDir())); sanitizedApkFile = SanitizedFile.knownSanitized(File.createTempFile("install-", ".apk",
FileUtils.copyFile(apkFile, apkToInstall); context.getFilesDir()));
if (!verifyApkFile(apkToInstall, apk.hash, apk.hashType)) { FileUtils.copyFile(apkFile, sanitizedApkFile);
if (!verifyApkFile(sanitizedApkFile, apk.hash, apk.hashType)) {
FileUtils.deleteQuietly(apkFile); FileUtils.deleteQuietly(apkFile);
throw new InstallFailedException(apkFile + " failed to verify!"); throw new InstallFailedException(apkFile + " failed to verify!");
} }
apkFile = null; // ensure this is not used now that its copied to apkToInstall 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. // 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 // 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 // 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 // storage can overwrite the app between F-Droid asking for it to be installed and
// the installer actually installing it. // the installer actually installing it.
apkToInstall.setReadable(true, false); sanitizedApkFile.setReadable(true, false);
installPackageInternal(apkToInstall);
NotificationManager nm = (NotificationManager) } catch (NumberFormatException | IOException | NoSuchAlgorithmException e) {
mContext.getSystemService(Context.NOTIFICATION_SERVICE);
nm.cancel(urlString.hashCode());
} catch (NumberFormatException | NoSuchAlgorithmException | IOException e) {
throw new InstallFailedException(e); throw new InstallFailedException(e);
} catch (ClassCastException e) { } catch (ClassCastException e) {
throw new InstallFailedException("F-Droid Privileged can only be updated using an activity!"); throw new InstallFailedException("F-Droid Privileged can only be updated using an activity!");
} finally { } finally {
// 20 minutes the start of the install process, delete the file // 20 minutes the start of the install process, delete the file
final File apkToDelete = apkToInstall; final File apkToDelete = sanitizedApkFile;
new Thread() { new Thread() {
@Override @Override
public void run() { public void run() {
@ -248,41 +154,168 @@ public abstract class Installer {
} }
}.start(); }.start();
} }
return Uri.fromFile(sanitizedApkFile);
} }
public void deletePackage(String packageName) throws InstallFailedException { /**
// check if package exists before proceeding... * Returns permission screen for given apk.
try { *
mPm.getPackageInfo(packageName, 0); * @param apk instance of Apk
} catch (PackageManager.NameNotFoundException e) { * @return Intent with Activity to show required permissions.
Log.e(TAG, "Couldn't find package " + packageName + " to delete."); * Returns null if Installer handles that on itself, e.g., with DefaultInstaller,
return; * 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 int count = newPermissionCount(apk);
if (packageName != null && packageName.equals(PrivilegedInstaller.PRIVILEGED_EXTENSION_PACKAGE_NAME)) { if (count > 0) {
Activity activity; Uri uri = ApkProvider.getContentUri(apk);
try { Intent intent = new Intent(context, InstallConfirmActivity.class);
activity = (Activity) mContext; intent.setData(uri);
} catch (ClassCastException e) {
Utils.debugLog(TAG, "F-Droid Privileged can only be uninstalled using an activity!");
return;
}
Intent uninstallIntent = new Intent(activity, InstallExtensionDialogActivity.class); return intent;
uninstallIntent.setAction(InstallExtensionDialogActivity.ACTION_UNINSTALL); } else {
activity.startActivity(uninstallIntent); // no permission screen needed!
return; return null;
} }
deletePackageInternal(packageName);
} }
protected abstract void installPackageInternal(File apkFile) private int newPermissionCount(Apk apk) {
throws InstallFailedException; // 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) AppDiff appDiff = new AppDiff(context.getPackageManager(), apk);
throws InstallFailedException; 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> * Copyright (C) 2015 Daniel Martí <mvdan@mvdan.cc>
* *
* This program is free software; you can redistribute it and/or * This program is free software; you can redistribute it and/or
@ -20,49 +20,39 @@
package org.fdroid.fdroid.installer; package org.fdroid.fdroid.installer;
import android.app.Activity;
import android.content.ComponentName; import android.content.ComponentName;
import android.content.Context; import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent; import android.content.Intent;
import android.content.ServiceConnection; import android.content.ServiceConnection;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle;
import android.os.IBinder; import android.os.IBinder;
import android.os.RemoteException; import android.os.RemoteException;
import android.support.v7.app.AlertDialog;
import android.util.Log; import android.util.Log;
import org.fdroid.fdroid.R; import org.fdroid.fdroid.R;
import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.privileged.IPrivilegedCallback; import org.fdroid.fdroid.privileged.IPrivilegedCallback;
import org.fdroid.fdroid.privileged.IPrivilegedService; 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 * Installer that only works if the "F-Droid Privileged
* protected by the permissions * Extension" is installed as a privileged app.
* <ul> * <p/>
* <li>android.permission.INSTALL_PACKAGES</li> * "F-Droid Privileged Extension" provides a service that exposes
* <li>android.permission.DELETE_PACKAGES</li> * internal Android APIs for install/uninstall which are protected
* </ul> * by INSTALL_PACKAGES, DELETE_PACKAGES permissions.
*
* Both permissions are protected by systemOrSignature (in newer versions: * Both permissions are protected by systemOrSignature (in newer versions:
* system|signature) and only granted on F-Droid's install in the following * system|signature) and cannot be used directly by F-Droid.
* cases: * <p/>
* <ul> * Instead, this installer binds to the service of
* <li>On all Android versions if F-Droid is pre-deployed as a * "F-Droid Privileged Extension" and then executes the appropriate methods
* system-application with the Rom</li> * inside the privileged context of the privileged extension.
* <li>On Android < 4.4 also when moved into /system/app/</li> * <p/>
* <li>On Android >= 4.4 also when moved into /system/priv-app/</li> * This installer makes unattended installs/uninstalls possible.
* </ul> * Thus no PendingIntents are returned.
* * <p/>
* Sources for Android 4.4 change: * Sources for Android 4.4 change:
* https://groups.google.com/forum/#!msg/android- * https://groups.google.com/forum/#!msg/android-
* security-discuss/r7uL_OEMU5c/LijNHvxeV80J * 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"; 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"; 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_NO = 0;
public static final int IS_EXTENSION_INSTALLED_YES = 1; 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_SIGNATURE_PROBLEM = 2;
public static final int IS_EXTENSION_INSTALLED_PERMISSIONS_PROBLEM = 3;
public PrivilegedInstaller(Activity activity, PackageManager pm, // From AOSP source code
InstallerCallback callback) throws InstallFailedException { public static final int ACTION_INSTALL_REPLACE_EXISTING = 2;
super(activity, pm, callback);
this.mActivity = activity; /**
* 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) { public static boolean isExtensionInstalled(Context context) {
@ -102,29 +268,14 @@ public class PrivilegedInstaller extends Installer {
} }
public static int isExtensionInstalledCorrectly(Context context) { public static int isExtensionInstalledCorrectly(Context context) {
// check if installed // check if installed
if (!isExtensionInstalled(context)) { if (!isExtensionInstalled(context)) {
Log.e(TAG, "IS_EXTENSION_INSTALLED_NO");
return IS_EXTENSION_INSTALLED_NO; return IS_EXTENSION_INSTALLED_NO;
} }
// check if it has the privileged permissions granted ServiceConnection serviceConnection = new ServiceConnection() {
final Object mutex = new Object();
final Bundle returnBundle = new Bundle();
ServiceConnection mServiceConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName name, IBinder service) { 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) { public void onServiceDisconnected(ComponentName name) {
@ -133,68 +284,32 @@ public class PrivilegedInstaller extends Installer {
Intent serviceIntent = new Intent(PRIVILEGED_EXTENSION_SERVICE_INTENT); Intent serviceIntent = new Intent(PRIVILEGED_EXTENSION_SERVICE_INTENT);
serviceIntent.setPackage(PRIVILEGED_EXTENSION_PACKAGE_NAME); serviceIntent.setPackage(PRIVILEGED_EXTENSION_PACKAGE_NAME);
// try to connect to check for signature
try { try {
context.getApplicationContext().bindService(serviceIntent, mServiceConnection, context.getApplicationContext().bindService(serviceIntent, serviceConnection,
Context.BIND_AUTO_CREATE); Context.BIND_AUTO_CREATE);
} catch (SecurityException e) { } catch (SecurityException e) {
Log.e(TAG, "IS_EXTENSION_INSTALLED_SIGNATURE_PROBLEM", e);
return IS_EXTENSION_INSTALLED_SIGNATURE_PROBLEM; 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; return IS_EXTENSION_INSTALLED_YES;
} }
@Override @Override
protected void installPackageInternal(File apkFile) throws InstallFailedException { protected void installPackage(final Uri uri, final Uri originatingUri, String packageName) {
Uri packageUri = Uri.fromFile(apkFile); sendBroadcastInstall(uri, originatingUri, Installer.ACTION_INSTALL_STARTED);
int count = newPermissionCount(packageUri);
if (count < 0) { final Uri sanitizedUri;
mCallback.onError(InstallerCallback.OPERATION_INSTALL, try {
InstallerCallback.ERROR_CODE_CANNOT_PARSE); 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; 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() { ServiceConnection mServiceConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName name, IBinder service) { public void onServiceConnected(ComponentName name, IBinder service) {
IPrivilegedService privService = IPrivilegedService.Stub.asInterface(service); IPrivilegedService privService = IPrivilegedService.Stub.asInterface(service);
@ -202,22 +317,30 @@ public class PrivilegedInstaller extends Installer {
IPrivilegedCallback callback = new IPrivilegedCallback.Stub() { IPrivilegedCallback callback = new IPrivilegedCallback.Stub() {
@Override @Override
public void handleResult(String packageName, int returnCode) throws RemoteException { public void handleResult(String packageName, int returnCode) throws RemoteException {
// TODO: propagate other return codes?
if (returnCode == INSTALL_SUCCEEDED) { if (returnCode == INSTALL_SUCCEEDED) {
Utils.debugLog(TAG, "Install succeeded"); sendBroadcastInstall(uri, originatingUri, ACTION_INSTALL_COMPLETE);
mCallback.onSuccess(InstallerCallback.OPERATION_INSTALL);
} else { } else {
Log.e(TAG, "Install failed with returnCode " + returnCode); sendBroadcastInstall(uri, originatingUri, ACTION_INSTALL_INTERRUPTED,
mCallback.onError(InstallerCallback.OPERATION_INSTALL, "Error " + returnCode + ": "
InstallerCallback.ERROR_CODE_OTHER); + INSTALL_RETURN_CODES.get(returnCode));
} }
} }
}; };
try { 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) { } catch (RemoteException e) {
Log.e(TAG, "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); Intent serviceIntent = new Intent(PRIVILEGED_EXTENSION_SERVICE_INTENT);
serviceIntent.setPackage(PRIVILEGED_EXTENSION_PACKAGE_NAME); serviceIntent.setPackage(PRIVILEGED_EXTENSION_PACKAGE_NAME);
mContext.getApplicationContext().bindService(serviceIntent, mServiceConnection, context.getApplicationContext().bindService(serviceIntent, mServiceConnection,
Context.BIND_AUTO_CREATE); Context.BIND_AUTO_CREATE);
} }
@Override @Override
protected void deletePackageInternal(final String packageName) protected void uninstallPackage(final String packageName) {
throws InstallFailedException { sendBroadcastUninstall(packageName, Installer.ACTION_UNINSTALL_STARTED);
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;
}
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() { ServiceConnection mServiceConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName name, IBinder service) { public void onServiceConnected(ComponentName name, IBinder service) {
IPrivilegedService privService = IPrivilegedService.Stub.asInterface(service); IPrivilegedService privService = IPrivilegedService.Stub.asInterface(service);
@ -297,23 +365,29 @@ public class PrivilegedInstaller extends Installer {
IPrivilegedCallback callback = new IPrivilegedCallback.Stub() { IPrivilegedCallback callback = new IPrivilegedCallback.Stub() {
@Override @Override
public void handleResult(String packageName, int returnCode) throws RemoteException { public void handleResult(String packageName, int returnCode) throws RemoteException {
// TODO: propagate other return codes?
if (returnCode == DELETE_SUCCEEDED) { if (returnCode == DELETE_SUCCEEDED) {
Utils.debugLog(TAG, "Delete succeeded"); sendBroadcastUninstall(packageName, ACTION_UNINSTALL_COMPLETE);
mCallback.onSuccess(InstallerCallback.OPERATION_DELETE);
} else { } else {
Log.e(TAG, "Delete failed with returnCode " + returnCode); sendBroadcastUninstall(packageName, ACTION_UNINSTALL_INTERRUPTED,
mCallback.onError(InstallerCallback.OPERATION_DELETE, "Error " + returnCode + ": "
InstallerCallback.ERROR_CODE_OTHER); + UNINSTALL_RETURN_CODES.get(returnCode));
} }
} }
}; };
try { 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); privService.deletePackage(packageName, 0, callback);
} catch (RemoteException e) { } catch (RemoteException e) {
Log.e(TAG, "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); Intent serviceIntent = new Intent(PRIVILEGED_EXTENSION_SERVICE_INTENT);
serviceIntent.setPackage(PRIVILEGED_EXTENSION_PACKAGE_NAME); serviceIntent.setPackage(PRIVILEGED_EXTENSION_PACKAGE_NAME);
mContext.getApplicationContext().bindService(serviceIntent, mServiceConnection, context.getApplicationContext().bindService(serviceIntent, mServiceConnection,
Context.BIND_AUTO_CREATE); Context.BIND_AUTO_CREATE);
} }
@Override @Override
public boolean handleOnActivityResult(int requestCode, int resultCode, Intent data) { protected boolean isUnattended() {
switch (requestCode) { return true;
case REQUEST_CONFIRM_PERMS:
if (resultCode == Activity.RESULT_OK) {
final Uri packageUri = data.getData();
try {
doInstallPackageInternal(packageUri);
} catch (InstallFailedException e) {
mCallback.onError(InstallerCallback.OPERATION_INSTALL,
InstallerCallback.ERROR_CODE_OTHER);
}
} else if (resultCode == InstallConfirmActivity.RESULT_CANNOT_PARSE) {
mCallback.onError(InstallerCallback.OPERATION_INSTALL,
InstallerCallback.ERROR_CODE_CANNOT_PARSE);
} else { // Activity.RESULT_CANCELED
mCallback.onError(InstallerCallback.OPERATION_INSTALL,
InstallerCallback.ERROR_CODE_CANCELED);
}
return true;
default:
return false;
}
} }
public static final int INSTALL_REPLACE_EXISTING = 2;
/**
* 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.Context;
import android.content.DialogInterface; import android.content.DialogInterface;
import android.content.Intent; import android.content.Intent;
import android.net.Uri;
import android.os.AsyncTask; import android.os.AsyncTask;
import android.os.Bundle; import android.os.Bundle;
import android.support.v4.app.FragmentActivity; import android.support.v4.app.FragmentActivity;
@ -43,6 +44,8 @@ import org.fdroid.fdroid.Preferences;
import org.fdroid.fdroid.R; import org.fdroid.fdroid.R;
import org.fdroid.fdroid.installer.PrivilegedInstaller; import org.fdroid.fdroid.installer.PrivilegedInstaller;
import java.io.File;
import eu.chainfire.libsuperuser.Shell; import eu.chainfire.libsuperuser.Shell;
/** /**
@ -53,13 +56,12 @@ public class InstallExtensionDialogActivity extends FragmentActivity {
private static final String TAG = "InstallIntoSystem"; private static final String TAG = "InstallIntoSystem";
public static final String ACTION_INSTALL = "install"; 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_UNINSTALL = "uninstall";
public static final String ACTION_POST_INSTALL = "post_install"; public static final String ACTION_POST_INSTALL = "post_install";
public static final String ACTION_FIRST_TIME = "first_time"; public static final String ACTION_FIRST_TIME = "first_time";
private String apkFile; private String apkPath;
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
@ -73,7 +75,11 @@ public class InstallExtensionDialogActivity extends FragmentActivity {
return; 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()) { switch (getIntent().getAction()) {
case ACTION_UNINSTALL: case ACTION_UNINSTALL:
@ -105,7 +111,6 @@ public class InstallExtensionDialogActivity extends FragmentActivity {
runFirstTime(context); runFirstTime(context);
break; break;
case PrivilegedInstaller.IS_EXTENSION_INSTALLED_PERMISSIONS_PROBLEM:
case PrivilegedInstaller.IS_EXTENSION_INSTALLED_SIGNATURE_PROBLEM: case PrivilegedInstaller.IS_EXTENSION_INSTALLED_SIGNATURE_PROBLEM:
default: default:
// do nothing // do nothing
@ -334,7 +339,7 @@ public class InstallExtensionDialogActivity extends FragmentActivity {
@Override @Override
protected Void doInBackground(Void... voids) { protected Void doInBackground(Void... voids) {
InstallExtension.create(getApplicationContext()).runInstall(apkFile); InstallExtension.create(getApplicationContext()).runInstall(apkPath);
return null; return null;
} }
}; };
@ -369,12 +374,6 @@ public class InstallExtensionDialogActivity extends FragmentActivity {
"\n\n" + getString(R.string.system_install_denied_signature); "\n\n" + getString(R.string.system_install_denied_signature);
result = Activity.RESULT_CANCELED; result = Activity.RESULT_CANCELED;
break; 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: default:
throw new RuntimeException("unhandled return"); throw new RuntimeException("unhandled return");
} }

View File

@ -18,11 +18,18 @@
package org.fdroid.fdroid.privileged.views; package org.fdroid.fdroid.privileged.views;
import android.annotation.TargetApi;
import android.content.pm.ApplicationInfo; import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo; import android.content.pm.PackageInfo;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.net.Uri; 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 { public class AppDiff {
private final PackageManager mPm; private final PackageManager mPm;
@ -30,6 +37,30 @@ public class AppDiff {
public ApplicationInfo mInstalledAppInfo; 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) { public AppDiff(PackageManager mPm, Uri mPackageURI) {
this.mPm = mPm; this.mPm = mPm;
@ -55,7 +86,7 @@ public class AppDiff {
String pkgName = mPkgInfo.packageName; String pkgName = mPkgInfo.packageName;
// Check if there is already a package on the device with this name // Check if there is already a package on the device with this name
// but it has been renamed to something else. // 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) { if (oldName != null && oldName.length > 0 && oldName[0] != null) {
pkgName = oldName[0]; pkgName = oldName[0];
mPkgInfo.packageName = pkgName; mPkgInfo.packageName = pkgName;

View File

@ -235,8 +235,7 @@ public class AppSecurityPermissions {
try { try {
installedPkgInfo = mPm.getPackageInfo(info.packageName, installedPkgInfo = mPm.getPackageInfo(info.packageName,
PackageManager.GET_PERMISSIONS); PackageManager.GET_PERMISSIONS);
} catch (NameNotFoundException e) { } catch (NameNotFoundException ignored) {
throw new RuntimeException("NameNotFoundException during GET_PERMISSIONS!");
} }
extractPerms(info, permSet, installedPkgInfo); extractPerms(info, permSet, installedPkgInfo);
} }

View File

@ -18,16 +18,15 @@
package org.fdroid.fdroid.privileged.views; package org.fdroid.fdroid.privileged.views;
import android.app.Activity;
import android.content.Context; import android.content.Context;
import android.content.DialogInterface; import android.content.DialogInterface;
import android.content.DialogInterface.OnCancelListener; import android.content.DialogInterface.OnCancelListener;
import android.content.Intent; import android.content.Intent;
import android.content.pm.ApplicationInfo; import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager; import android.graphics.Bitmap;
import android.graphics.drawable.Drawable;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.support.v4.app.FragmentActivity;
import android.support.v4.view.ViewPager; import android.support.v4.view.ViewPager;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
@ -38,45 +37,62 @@ import android.widget.ImageView;
import android.widget.TabHost; import android.widget.TabHost;
import android.widget.TextView; 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.FDroidApp;
import org.fdroid.fdroid.R; 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: * NOTES:
* Parts are based on AOSP src/com/android/packageinstaller/PackageInstallerActivity.java * Parts are based on AOSP src/com/android/packageinstaller/PackageInstallerActivity.java
* latest included commit: c23d802958158d522e7350321ad9ac6d43013883 * 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; public static final int RESULT_CANNOT_PARSE = RESULT_FIRST_USER + 1;
private Intent intent; private Intent intent;
private PackageManager mPm; private AppDiff appDiff;
private AppDiff mAppDiff;
// View for install progress // View for install progress
private View mInstallConfirm; private View installConfirm;
// Buttons to indicate user acceptance // Buttons to indicate user acceptance
private Button mOk; private Button okButton;
private Button mCancel; private Button cancelButton;
private CaffeinatedScrollView mScrollView; private CaffeinatedScrollView scrollView;
private boolean mOkCanInstall; private boolean okCanInstall;
private static final String TAB_ID_ALL = "all"; private static final String TAB_ID_ALL = "all";
private static final String TAB_ID_NEW = "new"; 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() { 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); View appSnippet = findViewById(R.id.app_snippet);
((ImageView) appSnippet.findViewById(R.id.app_icon)).setImageDrawable(appIcon); TextView appName = (TextView) appSnippet.findViewById(R.id.app_name);
((TextView) appSnippet.findViewById(R.id.app_name)).setText(appLabel); ImageView appIcon = (ImageView) appSnippet.findViewById(R.id.app_icon);
TabHost tabHost = (TabHost) findViewById(android.R.id.tabhost); TabHost tabHost = (TabHost) findViewById(android.R.id.tabhost);
appName.setText(mApp.name);
ImageLoader.getInstance().displayImage(mApp.iconUrlLarge, appIcon,
displayImageOptions);
tabHost.setup(); tabHost.setup();
ViewPager viewPager = (ViewPager) findViewById(R.id.pager); ViewPager viewPager = (ViewPager) findViewById(R.id.pager);
TabsAdapter adapter = new TabsAdapter(this, tabHost, viewPager); TabsAdapter adapter = new TabsAdapter(this, tabHost, viewPager);
@ -87,27 +103,27 @@ public class InstallConfirmActivity extends Activity implements OnCancelListener
}); });
boolean permVisible = false; boolean permVisible = false;
mScrollView = null; scrollView = null;
mOkCanInstall = false; okCanInstall = false;
int msg = 0; int msg = 0;
AppSecurityPermissions perms = new AppSecurityPermissions(this, mAppDiff.mPkgInfo); AppSecurityPermissions perms = new AppSecurityPermissions(this, appDiff.mPkgInfo);
if (mAppDiff.mInstalledAppInfo != null) { if (appDiff.mInstalledAppInfo != null) {
msg = (mAppDiff.mInstalledAppInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0 msg = (appDiff.mInstalledAppInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0
? R.string.install_confirm_update_system ? R.string.install_confirm_update_system
: R.string.install_confirm_update; : R.string.install_confirm_update;
mScrollView = new CaffeinatedScrollView(this); scrollView = new CaffeinatedScrollView(this);
mScrollView.setFillViewport(true); scrollView.setFillViewport(true);
final boolean newPermissionsFound = final boolean newPermissionsFound =
perms.getPermissionCount(AppSecurityPermissions.WHICH_NEW) > 0; perms.getPermissionCount(AppSecurityPermissions.WHICH_NEW) > 0;
if (newPermissionsFound) { if (newPermissionsFound) {
permVisible = true; permVisible = true;
mScrollView.addView(perms.getPermissionsView( scrollView.addView(perms.getPermissionsView(
AppSecurityPermissions.WHICH_NEW)); AppSecurityPermissions.WHICH_NEW));
} else { } else {
throw new RuntimeException("This should not happen. No new permissions were found but InstallConfirmActivity has been started!"); 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( adapter.addTab(tabHost.newTabSpec(TAB_ID_NEW).setIndicator(
getText(R.string.newPerms)), mScrollView); getText(R.string.newPerms)), scrollView);
} else { } else {
findViewById(R.id.tabscontainer).setVisibility(View.GONE); findViewById(R.id.tabscontainer).setVisibility(View.GONE);
findViewById(R.id.divider).setVisibility(View.VISIBLE); findViewById(R.id.divider).setVisibility(View.VISIBLE);
@ -118,8 +134,8 @@ public class InstallConfirmActivity extends Activity implements OnCancelListener
LayoutInflater inflater = (LayoutInflater) getSystemService( LayoutInflater inflater = (LayoutInflater) getSystemService(
Context.LAYOUT_INFLATER_SERVICE); Context.LAYOUT_INFLATER_SERVICE);
View root = inflater.inflate(R.layout.permissions_list, null); View root = inflater.inflate(R.layout.permissions_list, null);
if (mScrollView == null) { if (scrollView == null) {
mScrollView = (CaffeinatedScrollView) root.findViewById(R.id.scrollview); scrollView = (CaffeinatedScrollView) root.findViewById(R.id.scrollview);
} }
final ViewGroup permList = (ViewGroup) root.findViewById(R.id.permission_list); final ViewGroup permList = (ViewGroup) root.findViewById(R.id.permission_list);
permList.addView(perms.getPermissionsView(AppSecurityPermissions.WHICH_ALL)); permList.addView(perms.getPermissionsView(AppSecurityPermissions.WHICH_ALL));
@ -128,40 +144,40 @@ public class InstallConfirmActivity extends Activity implements OnCancelListener
} }
if (!permVisible) { if (!permVisible) {
if (mAppDiff.mInstalledAppInfo != null) { if (appDiff.mInstalledAppInfo != null) {
// This is an update to an application, but there are no // This is an update to an application, but there are no
// permissions at all. // 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_system_no_perms
: R.string.install_confirm_update_no_perms; : R.string.install_confirm_update_no_perms;
} else { } else {
// This is a new application with no permissions. // 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); tabHost.setVisibility(View.GONE);
findViewById(R.id.filler).setVisibility(View.VISIBLE); findViewById(R.id.filler).setVisibility(View.VISIBLE);
findViewById(R.id.divider).setVisibility(View.GONE); findViewById(R.id.divider).setVisibility(View.GONE);
mScrollView = null; scrollView = null;
} }
if (msg != 0) { if (msg != 0) {
((TextView) findViewById(R.id.install_confirm)).setText(msg); ((TextView) findViewById(R.id.install_confirm)).setText(msg);
} }
mInstallConfirm.setVisibility(View.VISIBLE); installConfirm.setVisibility(View.VISIBLE);
mOk = (Button) findViewById(R.id.ok_button); okButton = (Button) findViewById(R.id.ok_button);
mCancel = (Button) findViewById(R.id.cancel_button); cancelButton = (Button) findViewById(R.id.cancel_button);
mOk.setOnClickListener(this); okButton.setOnClickListener(this);
mCancel.setOnClickListener(this); cancelButton.setOnClickListener(this);
if (mScrollView == null) { if (scrollView == null) {
// There is nothing to scroll view, so the ok button is immediately // There is nothing to scroll view, so the ok button is immediately
// set to install. // set to install.
mOk.setText(R.string.menu_install); okButton.setText(R.string.menu_install);
mOkCanInstall = true; okCanInstall = true;
} else { } else {
mScrollView.setFullScrollAction(new Runnable() { scrollView.setFullScrollAction(new Runnable() {
@Override @Override
public void run() { public void run() {
mOk.setText(R.string.menu_install); okButton.setText(R.string.menu_install);
mOkCanInstall = true; okCanInstall = true;
} }
}); });
} }
@ -171,22 +187,28 @@ public class InstallConfirmActivity extends Activity implements OnCancelListener
protected void onCreate(Bundle icicle) { protected void onCreate(Bundle icicle) {
super.onCreate(icicle); super.onCreate(icicle);
((FDroidApp) getApplication()).applyTheme(this); ((FDroidApp) getApplication()).applyDialogTheme(this);
mPm = getPackageManager();
intent = getIntent(); 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); appDiff = new AppDiff(getPackageManager(), apk);
if (mAppDiff.mPkgInfo == null) { if (appDiff.mPkgInfo == null) {
setResult(RESULT_CANNOT_PARSE, intent); setResult(RESULT_CANNOT_PARSE, intent);
finish(); finish();
} }
setContentView(R.layout.install_start); 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(); startInstallConfirm();
} }
@ -197,14 +219,14 @@ public class InstallConfirmActivity extends Activity implements OnCancelListener
} }
public void onClick(View v) { public void onClick(View v) {
if (v == mOk) { if (v == okButton) {
if (mOkCanInstall || mScrollView == null) { if (okCanInstall || scrollView == null) {
setResult(RESULT_OK, intent); setResult(RESULT_OK, intent);
finish(); finish();
} else { } else {
mScrollView.pageScroll(View.FOCUS_DOWN); scrollView.pageScroll(View.FOCUS_DOWN);
} }
} else if (v == mCancel) { } else if (v == cancelButton) {
setResult(RESULT_CANCELED, intent); setResult(RESULT_CANCELED, intent);
finish(); 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: case PrivilegedInstaller.IS_EXTENSION_INSTALLED_SIGNATURE_PROBLEM:
message = getActivity().getString(R.string.system_install_denied_signature); message = getActivity().getString(R.string.system_install_denied_signature);
break; break;
case PrivilegedInstaller.IS_EXTENSION_INSTALLED_PERMISSIONS_PROBLEM:
message = getActivity().getString(R.string.system_install_denied_permissions);
break;
default: default:
throw new RuntimeException("unhandled return"); throw new RuntimeException("unhandled return");
} }

View File

@ -1,6 +1,7 @@
package org.fdroid.fdroid.views.swap; package org.fdroid.fdroid.views.swap;
import android.app.Activity; import android.app.Activity;
import android.app.PendingIntent;
import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothAdapter;
import android.content.BroadcastReceiver; import android.content.BroadcastReceiver;
import android.content.ComponentName; import android.content.ComponentName;
@ -43,6 +44,7 @@ import org.fdroid.fdroid.data.App;
import org.fdroid.fdroid.data.NewRepoConfig; import org.fdroid.fdroid.data.NewRepoConfig;
import org.fdroid.fdroid.installer.InstallManagerService; import org.fdroid.fdroid.installer.InstallManagerService;
import org.fdroid.fdroid.installer.Installer; import org.fdroid.fdroid.installer.Installer;
import org.fdroid.fdroid.installer.InstallerService;
import org.fdroid.fdroid.localrepo.LocalRepoManager; import org.fdroid.fdroid.localrepo.LocalRepoManager;
import org.fdroid.fdroid.localrepo.SwapService; import org.fdroid.fdroid.localrepo.SwapService;
import org.fdroid.fdroid.localrepo.peers.Peer; import org.fdroid.fdroid.localrepo.peers.Peer;
@ -119,7 +121,6 @@ public class SwapWorkflowActivity extends AppCompatActivity {
private PrepareSwapRepo updateSwappableAppsTask; private PrepareSwapRepo updateSwappableAppsTask;
private NewRepoConfig confirmSwapConfig; private NewRepoConfig confirmSwapConfig;
private LocalBroadcastManager localBroadcastManager; private LocalBroadcastManager localBroadcastManager;
private BroadcastReceiver downloadCompleteReceiver;
@NonNull @NonNull
private final ServiceConnection serviceConnection = new ServiceConnection() { private final ServiceConnection serviceConnection = new ServiceConnection() {
@ -773,7 +774,7 @@ public class SwapWorkflowActivity extends AppCompatActivity {
public void install(@NonNull final App app) { public void install(@NonNull final App app) {
final Apk apk = ApkProvider.Helper.find(this, app.packageName, app.suggestedVersionCode); final Apk apk = ApkProvider.Helper.find(this, app.packageName, app.suggestedVersionCode);
String urlString = apk.getUrl(); String urlString = apk.getUrl();
downloadCompleteReceiver = new BroadcastReceiver() { BroadcastReceiver downloadCompleteReceiver = new BroadcastReceiver() {
@Override @Override
public void onReceive(Context context, Intent intent) { public void onReceive(Context context, Intent intent) {
String path = intent.getStringExtra(Downloader.EXTRA_DOWNLOAD_PATH); 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) { private void handleDownloadComplete(File apkFile, String packageName, String urlString) {
Uri originatingUri = Uri.parse(urlString);
Uri localUri = Uri.fromFile(apkFile);
try { localBroadcastManager.registerReceiver(installReceiver,
Installer.getActivityInstaller(this, new Installer.InstallerCallback() { Installer.getInstallIntentFilter(Uri.fromFile(apkFile)));
@Override InstallerService.install(this, localUri, originatingUri, packageName);
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
}
} }
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. user before it is installed.
--> -->
<LinearLayout <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent"
android:orientation="vertical" android:layout_height="match_parent"
android:layout_width="match_parent" android:orientation="vertical">
android:layout_height="match_parent">
<TextView <TextView
android:id="@+id/install_confirm" android:id="@+id/install_confirm"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/install_confirm" android:paddingLeft="16dp"
android:textAppearance="?android:attr/textAppearanceMedium" android:paddingRight="16dp"
android:paddingLeft="16dp" android:paddingTop="4dip"
android:paddingRight="16dp" android:text="@string/install_confirm"
android:paddingTop="4dip" /> android:textAppearance="?android:attr/textAppearanceMedium" />
<ImageView <ImageView
android:id="@+id/divider" android:id="@+id/divider"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
android:background="?android:attr/dividerHorizontal" android:background="?android:attr/dividerHorizontal"
android:visibility="gone" /> android:visibility="gone" />
<FrameLayout <FrameLayout
android:id="@+id/filler" android:id="@+id/filler"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1" android:layout_weight="1"
android:visibility="gone"> android:visibility="gone"></FrameLayout>
</FrameLayout>
<TabHost <TabHost
android:id="@android:id/tabhost" android:id="@android:id/tabhost"
@ -60,24 +58,28 @@
android:layout_weight="1"> android:layout_weight="1">
<LinearLayout <LinearLayout
android:orientation="vertical"
android:layout_width="match_parent" 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_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="@drawable/tab_unselected_holo" android:background="@drawable/tab_unselected_holo"
android:fillViewport="true" android:fillViewport="true"
android:scrollbars="none"> 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 <TabWidget
android:id="@android:id/tabs" android:id="@android:id/tabs"
android:orientation="horizontal"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center" /> android:layout_gravity="center"
android:orientation="horizontal" />
</FrameLayout> </FrameLayout>
</HorizontalScrollView> </HorizontalScrollView>
@ -85,64 +87,68 @@
android:id="@android:id/tabcontent" android:id="@android:id/tabcontent"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="0dp"
android:layout_weight="0"/> android:layout_weight="0" />
<android.support.v4.view.ViewPager <android.support.v4.view.ViewPager
android:id="@+id/pager" android:id="@+id/pager"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="0dp"
android:layout_weight="1"/> android:layout_weight="1" />
</LinearLayout> </LinearLayout>
</TabHost> </TabHost>
<!-- OK confirm and cancel buttons. --> <!-- OK confirm and cancel buttons. -->
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" android:divider="?android:attr/dividerHorizontal"
android:divider="?android:attr/dividerHorizontal" android:orientation="vertical"
android:showDividers="beginning"> android:showDividers="beginning">
<LinearLayout <LinearLayout
style="?android:attr/buttonBarStyle" style="?android:attr/buttonBarStyle"
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_height="wrap_content"
android:layout_weight="0.25"
android:orientation="horizontal" android:orientation="horizontal"
android:measureWithLargestChild="true"> android:visibility="gone" />
<LinearLayout android:id="@+id/leftSpacer" <Button
android:layout_weight="0.25" android:id="@+id/cancel_button"
android:layout_width="0dip" style="?android:attr/buttonBarButtonStyle"
android:layout_height="wrap_content" android:layout_width="0dip"
android:orientation="horizontal" android:layout_height="wrap_content"
android:visibility="gone" /> android:layout_gravity="start"
android:layout_weight="1"
android:maxLines="2"
android:text="@string/cancel" />
<Button android:id="@+id/cancel_button" <Button
android:layout_width="0dip" android:id="@+id/ok_button"
android:layout_height="wrap_content" style="?android:attr/buttonBarButtonStyle"
android:layout_gravity="start" android:layout_width="0dip"
android:layout_weight="1" android:layout_height="wrap_content"
android:text="@string/cancel" android:layout_gravity="end"
android:maxLines="2" android:layout_weight="1"
style="?android:attr/buttonBarButtonStyle" /> android:filterTouchesWhenObscured="true"
android:maxLines="2"
android:text="@string/next" />
<Button android:id="@+id/ok_button" <LinearLayout
android:layout_width="0dip" android:id="@+id/rightSpacer"
android:layout_height="wrap_content" android:layout_width="0dip"
android:layout_gravity="end" android:layout_height="wrap_content"
android:layout_weight="1" android:layout_weight="0.25"
android:text="@string/next" android:orientation="horizontal"
android:maxLines="2" android:visibility="gone" />
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> </LinearLayout>
</LinearLayout> </LinearLayout>

View File

@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?><!-- Copyright (C) 2008 The Android Open Source Project
<!-- Copyright (C) 2008 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with 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 Defines the layout of the application snippet that appears on top of the
installation screens installation screens
--> --><!-- The snippet about the application - title, icon, description. -->
<!-- The snippet about the application - title, icon, description. --> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
<RelativeLayout xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/app_snippet" android:id="@+id/app_snippet"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingLeft="16dip"
android:paddingStart="16dip"
android:paddingRight="16dip"
android:paddingEnd="16dip" android:paddingEnd="16dip"
android:paddingTop="24dip" android:paddingLeft="16dip"
> android:paddingRight="16dip"
<ImageView android:id="@+id/app_icon" android:paddingStart="16dip"
android:layout_width="32dip" android:paddingTop="16dip">
android:layout_height="32dip"
<ImageView
android:id="@+id/app_icon"
android:layout_width="48dip"
android:layout_height="48dip"
android:layout_marginLeft="8dip" android:layout_marginLeft="8dip"
android:layout_marginStart="8dip" android:layout_marginStart="8dip"
android:background="@android:color/transparent" android:background="@android:color/transparent"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:gravity="start" android:gravity="start"
android:scaleType="centerCrop"/> android:scaleType="centerCrop"
<TextView android:id="@+id/app_name" tools:src="@drawable/ic_launcher" />
<TextView
android:id="@+id/app_name"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:ellipsize="end"
android:gravity="center" android:gravity="center"
android:textAppearance="?android:attr/textAppearanceLarge" android:paddingEnd="16dip"
android:textColor="?android:attr/textColorPrimary" android:paddingLeft="16dip"
android:paddingRight="16dip"
android:paddingStart="16dip"
android:shadowColor="@color/shadow" android:shadowColor="@color/shadow"
android:shadowRadius="2" android:shadowRadius="2"
android:layout_toRightOf="@id/app_icon"
android:layout_toEndOf="@id/app_icon"
android:singleLine="true" android:singleLine="true"
android:layout_centerInParent="true" android:textAppearance="?android:attr/textAppearanceLarge"
android:paddingRight="16dip" android:textColor="?android:attr/textColorPrimary"
android:paddingEnd="16dip" tools:text="App Name" />
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>
</RelativeLayout> </LinearLayout>

View File

@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?><!-- Copyright (C) 2008 The Android Open Source Project
<!-- Copyright (C) 2008 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -21,36 +20,34 @@
user before it is installed. user before it is installed.
--> -->
<LinearLayout <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent"
android:orientation="vertical" android:layout_height="match_parent"
android:layout_width="match_parent" android:orientation="vertical">
android:layout_height="match_parent">
<TextView <TextView
android:id="@+id/install_confirm" android:id="@+id/install_confirm"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/install_confirm" android:paddingLeft="16dp"
android:textAppearance="?android:attr/textAppearanceMedium" android:paddingRight="16dp"
android:paddingLeft="16dp" android:paddingTop="4dip"
android:paddingRight="16dp" android:text="@string/install_confirm"
android:paddingTop="4dip" /> android:textAppearance="?android:attr/textAppearanceMedium" />
<ImageView <ImageView
android:id="@+id/divider" android:id="@+id/divider"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
android:visibility="gone" /> android:visibility="gone" />
<FrameLayout <FrameLayout
android:id="@+id/filler" android:id="@+id/filler"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1" android:layout_weight="1"
android:visibility="gone"> android:visibility="gone"></FrameLayout>
</FrameLayout>
<TabHost <TabHost
android:id="@android:id/tabhost" android:id="@android:id/tabhost"
@ -59,24 +56,28 @@
android:layout_weight="1"> android:layout_weight="1">
<LinearLayout <LinearLayout
android:orientation="vertical"
android:layout_width="match_parent" 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_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="@drawable/tab_unselected_holo" android:background="@drawable/tab_unselected_holo"
android:fillViewport="true" android:fillViewport="true"
android:scrollbars="none"> 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 <TabWidget
android:id="@android:id/tabs" android:id="@android:id/tabs"
android:orientation="horizontal"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center" /> android:layout_gravity="center"
android:orientation="horizontal" />
</FrameLayout> </FrameLayout>
</HorizontalScrollView> </HorizontalScrollView>
@ -84,63 +85,65 @@
android:id="@android:id/tabcontent" android:id="@android:id/tabcontent"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="0dp"
android:layout_weight="0"/> android:layout_weight="0" />
<android.support.v4.view.ViewPager <android.support.v4.view.ViewPager
android:id="@+id/pager" android:id="@+id/pager"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="0dp"
android:layout_weight="1"/> android:layout_weight="1" />
</LinearLayout> </LinearLayout>
</TabHost> </TabHost>
<!-- OK confirm and cancel buttons. --> <!-- OK confirm and cancel buttons. -->
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" android:divider="?android:attr/dividerHorizontal"
android:divider="?android:attr/dividerHorizontal" android:orientation="vertical"
android:showDividers="beginning"> android:showDividers="beginning">
<LinearLayout <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_height="wrap_content"
android:layout_weight="0.25"
android:orientation="horizontal" android:orientation="horizontal"
android:measureWithLargestChild="true"> android:visibility="gone" />
<LinearLayout android:id="@+id/leftSpacer" <Button
android:layout_weight="0.25" android:id="@+id/cancel_button"
android:layout_width="0dip" android:layout_width="0dip"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="horizontal" android:layout_gravity="start"
android:visibility="gone" /> android:layout_weight="1"
android:maxLines="2"
android:text="@string/cancel" />
<Button android:id="@+id/cancel_button" <Button
android:layout_width="0dip" android:id="@+id/ok_button"
android:layout_height="wrap_content" android:layout_width="0dip"
android:layout_gravity="start" android:layout_height="wrap_content"
android:layout_weight="1" android:layout_gravity="end"
android:text="@string/cancel" android:layout_weight="1"
android:maxLines="2" android:filterTouchesWhenObscured="true"
/> android:maxLines="2"
android:text="@string/next" />
<Button android:id="@+id/ok_button" <LinearLayout
android:layout_width="0dip" android:id="@+id/rightSpacer"
android:layout_height="wrap_content" android:layout_width="0dip"
android:layout_gravity="end" android:layout_height="wrap_content"
android:layout_weight="1" android:layout_weight="0.25"
android:text="@string/next" android:orientation="horizontal"
android:maxLines="2" android:visibility="gone" />
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> </LinearLayout>
</LinearLayout> </LinearLayout>

View File

@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?><!-- Copyright (C) 2008 The Android Open Source Project
<!-- Copyright (C) 2008 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -14,24 +13,22 @@
limitations under the License. limitations under the License.
--> -->
<RelativeLayout <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="wrap_content">
<include <include
android:id="@+id/app_snippet"
layout="@layout/install_app_details" layout="@layout/install_app_details"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content" />
android:id="@+id/app_snippet"/>
<include <include
layout="@layout/install_confirm"
android:id="@+id/install_confirm_panel" android:id="@+id/install_confirm_panel"
layout="@layout/install_confirm"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_below="@id/app_snippet" android:layout_below="@id/app_snippet" />
android:layout_alignParentBottom="true"/>
</RelativeLayout> </RelativeLayout>

View File

@ -267,10 +267,7 @@
<string name="requesting_root_access_body">Requesting root access…</string> <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_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="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_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="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_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> <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_install_format">Tap to install %s</string>
<string name="tap_to_update_format">Tap to update %s</string> <string name="tap_to_update_format">Tap to update %s</string>
<string name="install_confirm">Do you want to install this application? <string name="install_confirm">needs access to</string>
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_update">Do you want to install an update <string name="install_confirm_update">Do you want to install an update
to this existing application? Your existing data will not to this existing application? Your existing data will not
be lost. The updated application will get access to:</string> 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="tap_to_install">Download completed, tap to install</string>
<string name="download_error">Download unsuccessful</string> <string name="download_error">Download unsuccessful</string>
<string name="download_pending">Waiting to start download…</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_new_perm_prefix">New: </string>
<string name="perms_description_app">Provided by %1$s.</string> <string name="perms_description_app">Provided by %1$s.</string>
<string name="downloading">Downloading…</string> <string name="downloading">Downloading…</string>
<string name="downloading_apk">Downloading %1$s</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_never">Never</string>
<string name="interval_1h">Hourly</string> <string name="interval_1h">Hourly</string>

View File

@ -48,6 +48,20 @@
<item name="colorAccent">@color/fdroid_green</item> <item name="colorAccent">@color/fdroid_green</item>
</style> </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"> <style name="TextViewStyle" parent="android:Widget.TextView">
<item name="android:textColor">?android:attr/textColorPrimary</item> <item name="android:textColor">?android:attr/textColorPrimary</item>
</style> </style>