Merge branch 'installerservice-wip' into 'master'
InstallerService This merge request mainly introduces the ``InstallerService``. Many files have been touched and reworked in this merge request, due to the following changes: * After download of an apk in ``InstallManagerService``, the ``InstallerService``is started an kicks off the installation process. For unattended installers this directly runs through without any user interaction, for the default installer a new PendingIntent containing ``DefaultActivityInstaller`` is returned that is either stuffed into the notification or directly started from ``AppDetails`` * Using local broadcasts, ``InstallManagerService`` and ``AppDetails`` are informed of state changes in the installation process * ``DefaultActivityInstaller`` is a wrapper around the default installation APIs of Android * If the unattended ``PrivilegedInstaller`` is available, a permission screen is shown before download * Actual error codes and messages are displayed in notification or dialog on fail, especially interesting when using the ``PrivilegedInstaller`` * The process for installing the Privileged Extension has been moved into an own installer for logic seperation, called ``ExtensionInstaller`` Some design considerations: * I try to use Uris where ever possible. At some points this clashes with the usage of ``urlString`` in ``InstallManagerService``. This could be fixed in a later merge request Some other TODOs are left, but I would like to do them after this merge request has been merged if it's okay, as this one is already too huge: * Check if apk permissions are the same as announced in the permission screen for ``PrivilegedInstaller`` * In ``Installer.newPermissionCount()``, I need the target SDK before download to check if it's targetting Android M, which does not require the permission screen * Introduce FileProvider for Android N * Redesign layout of ``InstallConfirmActivity`` * Remove "cancel" icon for installing progress in AppDetails See merge request !300
This commit is contained in:
commit
9c1b917604
@ -316,12 +316,18 @@
|
|||||||
<activity
|
<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" />
|
||||||
|
@ -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
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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)) {
|
||||||
|
@ -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) {
|
||||||
|
@ -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) {
|
||||||
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,241 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2014-2016 Dominik Schürmann <dominik@dominikschuermann.de>
|
||||||
|
*
|
||||||
|
* This program is free software; you can redistribute it and/or
|
||||||
|
* modify it under the terms of the GNU General Public License
|
||||||
|
* as published by the Free Software Foundation; either version 3
|
||||||
|
* of the License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program; if not, write to the Free Software
|
||||||
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
||||||
|
* MA 02110-1301, USA.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.fdroid.fdroid.installer;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.content.ActivityNotFoundException;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.pm.PackageManager;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.support.v4.app.FragmentActivity;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import org.fdroid.fdroid.R;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A transparent activity as a wrapper around Android's PackageInstaller Intents
|
||||||
|
*/
|
||||||
|
public class DefaultInstallerActivity extends FragmentActivity {
|
||||||
|
public static final String TAG = "AndroidInstallerAct";
|
||||||
|
|
||||||
|
public static final String ACTION_INSTALL_PACKAGE = "org.fdroid.fdroid.INSTALL_PACKAGE";
|
||||||
|
public static final String ACTION_UNINSTALL_PACKAGE = "org.fdroid.fdroid.UNINSTALL_PACKAGE";
|
||||||
|
|
||||||
|
public static final String EXTRA_UNINSTALL_PACKAGE_NAME = "uninstallPackageName";
|
||||||
|
public static final String EXTRA_ORIGINATING_URI = "originatingUri";
|
||||||
|
|
||||||
|
private static final int REQUEST_CODE_INSTALL = 0;
|
||||||
|
private static final int REQUEST_CODE_UNINSTALL = 1;
|
||||||
|
|
||||||
|
private Uri installOriginatingUri;
|
||||||
|
private Uri installUri;
|
||||||
|
|
||||||
|
private String uninstallPackageName;
|
||||||
|
|
||||||
|
// for the broadcasts
|
||||||
|
private DefaultInstaller installer;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
|
installer = new DefaultInstaller(this);
|
||||||
|
|
||||||
|
Intent intent = getIntent();
|
||||||
|
String action = intent.getAction();
|
||||||
|
if (ACTION_INSTALL_PACKAGE.equals(action)) {
|
||||||
|
installUri = intent.getData();
|
||||||
|
installOriginatingUri = intent.getParcelableExtra(EXTRA_ORIGINATING_URI);
|
||||||
|
|
||||||
|
installPackage(installUri, installOriginatingUri);
|
||||||
|
} else if (ACTION_UNINSTALL_PACKAGE.equals(action)) {
|
||||||
|
uninstallPackageName = intent.getStringExtra(EXTRA_UNINSTALL_PACKAGE_NAME);
|
||||||
|
|
||||||
|
uninstallPackage(uninstallPackageName);
|
||||||
|
} else {
|
||||||
|
throw new IllegalStateException("Intent action not specified!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("InlinedApi")
|
||||||
|
private void installPackage(Uri uri, Uri originatingUri) {
|
||||||
|
if (uri == null) {
|
||||||
|
throw new RuntimeException("Set the data uri to point to an apk location!");
|
||||||
|
}
|
||||||
|
// https://code.google.com/p/android/issues/detail?id=205827
|
||||||
|
if ((Build.VERSION.SDK_INT <= Build.VERSION_CODES.M)
|
||||||
|
&& (!uri.getScheme().equals("file"))) {
|
||||||
|
throw new RuntimeException("PackageInstaller <= Android 6 only supports file scheme!");
|
||||||
|
}
|
||||||
|
if (("N".equals(Build.VERSION.CODENAME))
|
||||||
|
&& (!uri.getScheme().equals("content"))) {
|
||||||
|
throw new RuntimeException("PackageInstaller >= Android N only supports content scheme!");
|
||||||
|
}
|
||||||
|
|
||||||
|
Intent intent = new Intent();
|
||||||
|
intent.setData(uri);
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
|
||||||
|
intent.setAction(Intent.ACTION_VIEW);
|
||||||
|
intent.setType("application/vnd.android.package-archive");
|
||||||
|
} else {
|
||||||
|
intent.setAction(Intent.ACTION_INSTALL_PACKAGE);
|
||||||
|
|
||||||
|
// EXTRA_RETURN_RESULT throws a RuntimeException on N
|
||||||
|
// https://gitlab.com/fdroid/fdroidclient/issues/631
|
||||||
|
if (!"N".equals(Build.VERSION.CODENAME)) {
|
||||||
|
intent.putExtra(Intent.EXTRA_RETURN_RESULT, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// following extras only work when being installed as system-app
|
||||||
|
// https://code.google.com/p/android/issues/detail?id=42253
|
||||||
|
intent.putExtra(Intent.EXTRA_NOT_UNKNOWN_SOURCE, true);
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
|
||||||
|
// deprecated in Android 4.1
|
||||||
|
intent.putExtra(Intent.EXTRA_ALLOW_REPLACE, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
startActivityForResult(intent, REQUEST_CODE_INSTALL);
|
||||||
|
} catch (ActivityNotFoundException e) {
|
||||||
|
Log.e(TAG, "ActivityNotFoundException", e);
|
||||||
|
installer.sendBroadcastInstall(uri, originatingUri, Installer.ACTION_INSTALL_INTERRUPTED,
|
||||||
|
"This Android rom does not support ACTION_INSTALL_PACKAGE!");
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
installer.sendBroadcastInstall(installUri, installOriginatingUri,
|
||||||
|
Installer.ACTION_INSTALL_STARTED);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void uninstallPackage(String packageName) {
|
||||||
|
// check that the package is installed
|
||||||
|
try {
|
||||||
|
getPackageManager().getPackageInfo(packageName, 0);
|
||||||
|
} catch (PackageManager.NameNotFoundException e) {
|
||||||
|
Log.e(TAG, "NameNotFoundException", e);
|
||||||
|
installer.sendBroadcastUninstall(packageName, Installer.ACTION_UNINSTALL_INTERRUPTED,
|
||||||
|
"Package that is scheduled for uninstall is not installed!");
|
||||||
|
finish();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Uri uri = Uri.fromParts("package", packageName, null);
|
||||||
|
Intent intent = new Intent();
|
||||||
|
intent.setData(uri);
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
|
||||||
|
intent.setAction(Intent.ACTION_DELETE);
|
||||||
|
} else {
|
||||||
|
intent.setAction(Intent.ACTION_UNINSTALL_PACKAGE);
|
||||||
|
intent.putExtra(Intent.EXTRA_RETURN_RESULT, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
startActivityForResult(intent, REQUEST_CODE_UNINSTALL);
|
||||||
|
} catch (ActivityNotFoundException e) {
|
||||||
|
Log.e(TAG, "ActivityNotFoundException", e);
|
||||||
|
installer.sendBroadcastUninstall(packageName, Installer.ACTION_UNINSTALL_INTERRUPTED,
|
||||||
|
"This Android rom does not support ACTION_UNINSTALL_PACKAGE!");
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||||
|
switch (requestCode) {
|
||||||
|
case REQUEST_CODE_INSTALL:
|
||||||
|
/**
|
||||||
|
* resultCode is always 0 on Android < 4.0. See
|
||||||
|
* com.android.packageinstaller.PackageInstallerActivity: setResult is
|
||||||
|
* never executed on Androids < 4.0
|
||||||
|
*/
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
|
||||||
|
installer.sendBroadcastInstall(installUri, installOriginatingUri,
|
||||||
|
Installer.ACTION_INSTALL_COMPLETE);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback on N for https://gitlab.com/fdroid/fdroidclient/issues/631
|
||||||
|
if ("N".equals(Build.VERSION.CODENAME)) {
|
||||||
|
installer.sendBroadcastInstall(installUri, installOriginatingUri,
|
||||||
|
Installer.ACTION_INSTALL_COMPLETE);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (resultCode) {
|
||||||
|
case Activity.RESULT_OK:
|
||||||
|
installer.sendBroadcastInstall(installUri, installOriginatingUri,
|
||||||
|
Installer.ACTION_INSTALL_COMPLETE);
|
||||||
|
break;
|
||||||
|
case Activity.RESULT_CANCELED:
|
||||||
|
installer.sendBroadcastInstall(installUri, installOriginatingUri,
|
||||||
|
Installer.ACTION_INSTALL_INTERRUPTED);
|
||||||
|
break;
|
||||||
|
case Activity.RESULT_FIRST_USER:
|
||||||
|
default:
|
||||||
|
// AOSP returns Activity.RESULT_FIRST_USER on error
|
||||||
|
installer.sendBroadcastInstall(installUri, installOriginatingUri,
|
||||||
|
Installer.ACTION_INSTALL_INTERRUPTED,
|
||||||
|
getString(R.string.install_error_unknown));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
case REQUEST_CODE_UNINSTALL:
|
||||||
|
// resultCode is always 0 on Android < 4.0.
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
|
||||||
|
installer.sendBroadcastUninstall(uninstallPackageName,
|
||||||
|
Installer.ACTION_UNINSTALL_COMPLETE);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (resultCode) {
|
||||||
|
case Activity.RESULT_OK:
|
||||||
|
installer.sendBroadcastUninstall(uninstallPackageName,
|
||||||
|
Installer.ACTION_UNINSTALL_COMPLETE);
|
||||||
|
break;
|
||||||
|
case Activity.RESULT_CANCELED:
|
||||||
|
installer.sendBroadcastUninstall(uninstallPackageName,
|
||||||
|
Installer.ACTION_UNINSTALL_INTERRUPTED);
|
||||||
|
break;
|
||||||
|
case Activity.RESULT_FIRST_USER:
|
||||||
|
default:
|
||||||
|
// AOSP UninstallAppProgress returns RESULT_FIRST_USER on error
|
||||||
|
installer.sendBroadcastUninstall(uninstallPackageName,
|
||||||
|
Installer.ACTION_UNINSTALL_INTERRUPTED,
|
||||||
|
getString(R.string.uninstall_error_unknown));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new RuntimeException("Invalid request code!");
|
||||||
|
}
|
||||||
|
|
||||||
|
// after doing the broadcasts, finish this transparent wrapper activity
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,134 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
|
|
||||||
*
|
|
||||||
* This program is free software; you can redistribute it and/or
|
|
||||||
* modify it under the terms of the GNU General Public License
|
|
||||||
* as published by the Free Software Foundation; either version 3
|
|
||||||
* of the License, or (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program; if not, write to the Free Software
|
|
||||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
|
||||||
* MA 02110-1301, USA.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.fdroid.fdroid.installer;
|
|
||||||
|
|
||||||
import android.annotation.TargetApi;
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.content.ActivityNotFoundException;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.pm.PackageInfo;
|
|
||||||
import android.content.pm.PackageManager;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.os.Build;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* For Android >= 4.0: Default Installer using the public PackageManager API of
|
|
||||||
* Android to install/delete packages. This starts a Activity from the Android
|
|
||||||
* OS showing all permissions/changed permissions. The the user needs to
|
|
||||||
* manually press an install button, this Installer cannot be used for
|
|
||||||
* unattended installations.
|
|
||||||
*/
|
|
||||||
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
|
|
||||||
public class DefaultSdk14Installer extends Installer {
|
|
||||||
private final Activity mActivity;
|
|
||||||
|
|
||||||
public DefaultSdk14Installer(Activity activity, PackageManager pm, InstallerCallback callback)
|
|
||||||
throws InstallFailedException {
|
|
||||||
super(activity, pm, callback);
|
|
||||||
this.mActivity = activity;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final int REQUEST_CODE_INSTALL = 0;
|
|
||||||
private static final int REQUEST_CODE_DELETE = 1;
|
|
||||||
|
|
||||||
@SuppressWarnings("deprecation")
|
|
||||||
@Override
|
|
||||||
protected void installPackageInternal(File apkFile) throws InstallFailedException {
|
|
||||||
Intent intent = new Intent();
|
|
||||||
intent.setAction(Intent.ACTION_INSTALL_PACKAGE);
|
|
||||||
intent.setData(Uri.fromFile(apkFile));
|
|
||||||
// EXTRA_RETURN_RESULT throws a RuntimeException on N
|
|
||||||
// https://gitlab.com/fdroid/fdroidclient/issues/631
|
|
||||||
if (!"N".equals(Build.VERSION.CODENAME)) {
|
|
||||||
intent.putExtra(Intent.EXTRA_RETURN_RESULT, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// following extras only work when being installed as system-app
|
|
||||||
// https://code.google.com/p/android/issues/detail?id=42253
|
|
||||||
intent.putExtra(Intent.EXTRA_NOT_UNKNOWN_SOURCE, true);
|
|
||||||
if (Build.VERSION.SDK_INT < 16) {
|
|
||||||
// deprecated in Android 4.1
|
|
||||||
intent.putExtra(Intent.EXTRA_ALLOW_REPLACE, true);
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
mActivity.startActivityForResult(intent, REQUEST_CODE_INSTALL);
|
|
||||||
} catch (ActivityNotFoundException e) {
|
|
||||||
throw new InstallFailedException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void deletePackageInternal(String packageName) throws InstallFailedException {
|
|
||||||
try {
|
|
||||||
PackageInfo pkgInfo = mPm.getPackageInfo(packageName, 0);
|
|
||||||
|
|
||||||
Uri uri = Uri.fromParts("package", pkgInfo.packageName, null);
|
|
||||||
Intent intent = new Intent(Intent.ACTION_UNINSTALL_PACKAGE, uri);
|
|
||||||
intent.putExtra(Intent.EXTRA_RETURN_RESULT, true);
|
|
||||||
try {
|
|
||||||
mActivity.startActivityForResult(intent, REQUEST_CODE_DELETE);
|
|
||||||
} catch (ActivityNotFoundException e) {
|
|
||||||
throw new InstallFailedException(e);
|
|
||||||
}
|
|
||||||
} catch (PackageManager.NameNotFoundException e) {
|
|
||||||
// already checked in super class
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean handleOnActivityResult(int requestCode, int resultCode, Intent data) {
|
|
||||||
switch (requestCode) {
|
|
||||||
case REQUEST_CODE_INSTALL:
|
|
||||||
if (resultCode == Activity.RESULT_OK) {
|
|
||||||
mCallback.onSuccess(InstallerCallback.OPERATION_INSTALL);
|
|
||||||
} else if (resultCode == Activity.RESULT_CANCELED) {
|
|
||||||
mCallback.onError(InstallerCallback.OPERATION_INSTALL,
|
|
||||||
InstallerCallback.ERROR_CODE_CANCELED);
|
|
||||||
} else {
|
|
||||||
mCallback.onError(InstallerCallback.OPERATION_INSTALL,
|
|
||||||
InstallerCallback.ERROR_CODE_OTHER);
|
|
||||||
}
|
|
||||||
// Fallback on N for https://gitlab.com/fdroid/fdroidclient/issues/631
|
|
||||||
if ("N".equals(Build.VERSION.CODENAME)) {
|
|
||||||
mCallback.onSuccess(InstallerCallback.OPERATION_INSTALL);
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
case REQUEST_CODE_DELETE:
|
|
||||||
if (resultCode == Activity.RESULT_OK) {
|
|
||||||
mCallback.onSuccess(InstallerCallback.OPERATION_DELETE);
|
|
||||||
} else if (resultCode == Activity.RESULT_CANCELED) {
|
|
||||||
mCallback.onError(InstallerCallback.OPERATION_DELETE,
|
|
||||||
InstallerCallback.ERROR_CODE_CANCELED);
|
|
||||||
} else {
|
|
||||||
// UninstallAppProgress actually returns
|
|
||||||
// Activity.RESULT_FIRST_USER if something breaks
|
|
||||||
mCallback.onError(InstallerCallback.OPERATION_DELETE,
|
|
||||||
InstallerCallback.ERROR_CODE_OTHER);
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
default:
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,70 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016 Dominik Schürmann <dominik@dominikschuermann.de>
|
||||||
|
*
|
||||||
|
* This program is free software; you can redistribute it and/or
|
||||||
|
* modify it under the terms of the GNU General Public License
|
||||||
|
* as published by the Free Software Foundation; either version 3
|
||||||
|
* of the License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program; if not, write to the Free Software
|
||||||
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
||||||
|
* MA 02110-1301, USA.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.fdroid.fdroid.installer;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.content.DialogInterface;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
import android.support.v4.app.FragmentActivity;
|
||||||
|
import android.support.v7.app.AlertDialog;
|
||||||
|
import android.view.ContextThemeWrapper;
|
||||||
|
|
||||||
|
import org.fdroid.fdroid.FDroidApp;
|
||||||
|
|
||||||
|
public class ErrorDialogActivity extends FragmentActivity {
|
||||||
|
|
||||||
|
public static final String EXTRA_TITLE = "title";
|
||||||
|
public static final String EXTRA_MESSAGE = "message";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
|
final Intent intent = getIntent();
|
||||||
|
final String title = intent.getStringExtra(EXTRA_TITLE);
|
||||||
|
final String message = intent.getStringExtra(EXTRA_MESSAGE);
|
||||||
|
|
||||||
|
// hack to get theme applied (which is not automatically applied due to activity's Theme.NoDisplay
|
||||||
|
ContextThemeWrapper theme = new ContextThemeWrapper(this, FDroidApp.getCurThemeResId());
|
||||||
|
|
||||||
|
final AlertDialog.Builder builder = new AlertDialog.Builder(theme);
|
||||||
|
builder.setTitle(title);
|
||||||
|
builder.setNeutralButton(android.R.string.ok,
|
||||||
|
new DialogInterface.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(DialogInterface dialog, int which) {
|
||||||
|
setResult(Activity.RESULT_OK);
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
builder.setOnCancelListener(
|
||||||
|
new DialogInterface.OnCancelListener() {
|
||||||
|
@Override
|
||||||
|
public void onCancel(DialogInterface dialog) {
|
||||||
|
setResult(Activity.RESULT_CANCELED);
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
builder.setMessage(message);
|
||||||
|
builder.create().show();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,108 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016 Dominik Schürmann <dominik@dominikschuermann.de>
|
||||||
|
*
|
||||||
|
* This program is free software; you can redistribute it and/or
|
||||||
|
* modify it under the terms of the GNU General Public License
|
||||||
|
* as published by the Free Software Foundation; either version 3
|
||||||
|
* of the License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program; if not, write to the Free Software
|
||||||
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
||||||
|
* MA 02110-1301, USA.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.fdroid.fdroid.installer;
|
||||||
|
|
||||||
|
import android.app.PendingIntent;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import org.fdroid.fdroid.BuildConfig;
|
||||||
|
import org.fdroid.fdroid.privileged.install.InstallExtensionDialogActivity;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Special Installer that is only useful to install the Privileged Extension apk
|
||||||
|
* as a privileged app into the system partition of Android.
|
||||||
|
* <p/>
|
||||||
|
* This is installer requires user interaction and thus install/uninstall directly
|
||||||
|
* return PendingIntents.
|
||||||
|
*/
|
||||||
|
public class ExtensionInstaller extends Installer {
|
||||||
|
|
||||||
|
private static final String TAG = "ExtensionInstaller";
|
||||||
|
|
||||||
|
ExtensionInstaller(Context context) {
|
||||||
|
super(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void installPackage(Uri uri, Uri originatingUri, String packageName) {
|
||||||
|
Uri sanitizedUri;
|
||||||
|
try {
|
||||||
|
sanitizedUri = Installer.prepareApkFile(context, uri, packageName);
|
||||||
|
} catch (InstallFailedException e) {
|
||||||
|
Log.e(TAG, "prepareApkFile failed", e);
|
||||||
|
sendBroadcastInstall(uri, originatingUri, Installer.ACTION_INSTALL_INTERRUPTED,
|
||||||
|
e.getMessage());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// extension must be signed with the same public key as main F-Droid
|
||||||
|
// NOTE: Disabled for debug builds to be able to use official extension from repo
|
||||||
|
ApkSignatureVerifier signatureVerifier = new ApkSignatureVerifier(context);
|
||||||
|
if (!BuildConfig.DEBUG && !signatureVerifier.hasFDroidSignature(new File(sanitizedUri.getPath()))) {
|
||||||
|
sendBroadcastInstall(uri, originatingUri, Installer.ACTION_INSTALL_INTERRUPTED,
|
||||||
|
"APK signature of extension not correct!");
|
||||||
|
}
|
||||||
|
Intent installIntent = new Intent(context, InstallExtensionDialogActivity.class);
|
||||||
|
installIntent.setAction(InstallExtensionDialogActivity.ACTION_INSTALL);
|
||||||
|
installIntent.setData(sanitizedUri);
|
||||||
|
|
||||||
|
PendingIntent installPendingIntent = PendingIntent.getActivity(
|
||||||
|
context.getApplicationContext(),
|
||||||
|
uri.hashCode(),
|
||||||
|
installIntent,
|
||||||
|
PendingIntent.FLAG_UPDATE_CURRENT);
|
||||||
|
|
||||||
|
sendBroadcastInstall(uri, originatingUri,
|
||||||
|
Installer.ACTION_INSTALL_USER_INTERACTION, installPendingIntent);
|
||||||
|
|
||||||
|
// don't use broadcasts for the rest of this special installer
|
||||||
|
sendBroadcastInstall(uri, originatingUri, Installer.ACTION_INSTALL_COMPLETE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void uninstallPackage(String packageName) {
|
||||||
|
sendBroadcastUninstall(packageName, Installer.ACTION_UNINSTALL_STARTED);
|
||||||
|
|
||||||
|
Intent uninstallIntent = new Intent(context, InstallExtensionDialogActivity.class);
|
||||||
|
uninstallIntent.setAction(InstallExtensionDialogActivity.ACTION_UNINSTALL);
|
||||||
|
|
||||||
|
PendingIntent uninstallPendingIntent = PendingIntent.getActivity(
|
||||||
|
context.getApplicationContext(),
|
||||||
|
packageName.hashCode(),
|
||||||
|
uninstallIntent,
|
||||||
|
PendingIntent.FLAG_UPDATE_CURRENT);
|
||||||
|
|
||||||
|
sendBroadcastUninstall(packageName,
|
||||||
|
Installer.ACTION_UNINSTALL_USER_INTERACTION, uninstallPendingIntent);
|
||||||
|
|
||||||
|
// don't use broadcasts for the rest of this special installer
|
||||||
|
sendBroadcastUninstall(packageName, Installer.ACTION_UNINSTALL_COMPLETE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean isUnattended() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
@ -19,6 +19,7 @@ import android.text.TextUtils;
|
|||||||
import org.fdroid.fdroid.AppDetails;
|
import org.fdroid.fdroid.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) {
|
||||||
|
@ -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);
|
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,77 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016 Dominik Schürmann <dominik@dominikschuermann.de>
|
||||||
|
*
|
||||||
|
* This program is free software; you can redistribute it and/or
|
||||||
|
* modify it under the terms of the GNU General Public License
|
||||||
|
* as published by the Free Software Foundation; either version 3
|
||||||
|
* of the License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program; if not, write to the Free Software
|
||||||
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
||||||
|
* MA 02110-1301, USA.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.fdroid.fdroid.installer;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import org.fdroid.fdroid.Preferences;
|
||||||
|
import org.fdroid.fdroid.Utils;
|
||||||
|
|
||||||
|
public class InstallerFactory {
|
||||||
|
|
||||||
|
private static final String TAG = "InstallerFactory";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an instance of an appropriate installer.
|
||||||
|
* Either DefaultInstaller, PrivilegedInstaller, or in the special
|
||||||
|
* case to install the "F-Droid Privileged Extension" ExtensionInstaller.
|
||||||
|
*
|
||||||
|
* @param context current {@link Context}
|
||||||
|
* @param packageName package name of apk to be installed. Required to select
|
||||||
|
* the ExtensionInstaller.
|
||||||
|
* If this is null, the ExtensionInstaller will never be returned.
|
||||||
|
* @return instance of an Installer
|
||||||
|
*/
|
||||||
|
public static Installer create(Context context, String packageName) {
|
||||||
|
Installer installer;
|
||||||
|
|
||||||
|
if (packageName != null
|
||||||
|
&& packageName.equals(PrivilegedInstaller.PRIVILEGED_EXTENSION_PACKAGE_NAME)) {
|
||||||
|
// special case for "F-Droid Privileged Extension"
|
||||||
|
installer = new ExtensionInstaller(context);
|
||||||
|
} else if (isPrivilegedInstallerEnabled()) {
|
||||||
|
if (PrivilegedInstaller.isExtensionInstalledCorrectly(context)
|
||||||
|
== PrivilegedInstaller.IS_EXTENSION_INSTALLED_YES) {
|
||||||
|
Utils.debugLog(TAG, "privileged extension correctly installed -> PrivilegedInstaller");
|
||||||
|
|
||||||
|
installer = new PrivilegedInstaller(context);
|
||||||
|
} else {
|
||||||
|
Log.e(TAG, "PrivilegedInstaller is enabled in prefs, but permissions are not granted!");
|
||||||
|
// TODO: better error handling?
|
||||||
|
|
||||||
|
// fallback to default installer
|
||||||
|
installer = new DefaultInstaller(context);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
installer = new DefaultInstaller(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
return installer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extension has privileged permissions and preference is enabled?
|
||||||
|
*/
|
||||||
|
private static boolean isPrivilegedInstallerEnabled() {
|
||||||
|
return Preferences.get().isPrivilegedInstallerEnabled();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,91 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016 Dominik Schürmann <dominik@dominikschuermann.de>
|
||||||
|
*
|
||||||
|
* This program is free software; you can redistribute it and/or
|
||||||
|
* modify it under the terms of the GNU General Public License
|
||||||
|
* as published by the Free Software Foundation; either version 3
|
||||||
|
* of the License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program; if not, write to the Free Software
|
||||||
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
||||||
|
* MA 02110-1301, USA.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.fdroid.fdroid.installer;
|
||||||
|
|
||||||
|
import android.app.IntentService;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.net.Uri;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This service handles the install process of apk files and
|
||||||
|
* uninstall process of apps.
|
||||||
|
* <p/>
|
||||||
|
* This service is based on an IntentService because:
|
||||||
|
* - no parallel installs/uninstalls should be allowed,
|
||||||
|
* i.e., runs sequentially
|
||||||
|
* - no cancel operation is needed. Cancelling an installation
|
||||||
|
* would be the same as starting uninstall afterwards
|
||||||
|
*/
|
||||||
|
public class InstallerService extends IntentService {
|
||||||
|
|
||||||
|
private static final String ACTION_INSTALL = "org.fdroid.fdroid.installer.InstallerService.action.INSTALL";
|
||||||
|
private static final String ACTION_UNINSTALL = "org.fdroid.fdroid.installer.InstallerService.action.UNINSTALL";
|
||||||
|
|
||||||
|
public InstallerService() {
|
||||||
|
super("InstallerService");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onHandleIntent(Intent intent) {
|
||||||
|
String packageName = intent.getStringExtra(Installer.EXTRA_PACKAGE_NAME);
|
||||||
|
Installer installer = InstallerFactory.create(this, packageName);
|
||||||
|
|
||||||
|
if (ACTION_INSTALL.equals(intent.getAction())) {
|
||||||
|
Uri uri = intent.getData();
|
||||||
|
Uri originatingUri = intent.getParcelableExtra(Installer.EXTRA_ORIGINATING_URI);
|
||||||
|
|
||||||
|
installer.installPackage(uri, originatingUri, packageName);
|
||||||
|
} else if (ACTION_UNINSTALL.equals(intent.getAction())) {
|
||||||
|
installer.uninstallPackage(packageName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Install an apk from {@link Uri}
|
||||||
|
*
|
||||||
|
* @param context this app's {@link Context}
|
||||||
|
* @param uri {@link Uri} pointing to (downloaded) local apk file
|
||||||
|
* @param originatingUri {@link Uri} where the apk has been downloaded from
|
||||||
|
* @param packageName package name of the app that should be installed
|
||||||
|
*/
|
||||||
|
public static void install(Context context, Uri uri, Uri originatingUri, String packageName) {
|
||||||
|
Intent intent = new Intent(context, InstallerService.class);
|
||||||
|
intent.setAction(ACTION_INSTALL);
|
||||||
|
intent.setData(uri);
|
||||||
|
intent.putExtra(Installer.EXTRA_ORIGINATING_URI, originatingUri);
|
||||||
|
intent.putExtra(Installer.EXTRA_PACKAGE_NAME, packageName);
|
||||||
|
context.startService(intent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uninstall an app
|
||||||
|
*
|
||||||
|
* @param context this app's {@link Context}
|
||||||
|
* @param packageName package name of the app that will be uninstalled
|
||||||
|
*/
|
||||||
|
public static void uninstall(Context context, String packageName) {
|
||||||
|
Intent intent = new Intent(context, InstallerService.class);
|
||||||
|
intent.setAction(ACTION_UNINSTALL);
|
||||||
|
intent.putExtra(Installer.EXTRA_PACKAGE_NAME, packageName);
|
||||||
|
context.startService(intent);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
|
* Copyright (C) 2014-2016 Dominik Schürmann <dominik@dominikschuermann.de>
|
||||||
* Copyright (C) 2015 Daniel Martí <mvdan@mvdan.cc>
|
* 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;
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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");
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,106 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016 Dominik Schürmann <dominik@dominikschuermann.de>
|
||||||
|
*
|
||||||
|
* This program is free software; you can redistribute it and/or
|
||||||
|
* modify it under the terms of the GNU General Public License
|
||||||
|
* as published by the Free Software Foundation; either version 3
|
||||||
|
* of the License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program; if not, write to the Free Software
|
||||||
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
||||||
|
* MA 02110-1301, USA.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.fdroid.fdroid.privileged.views;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.content.DialogInterface;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.pm.ApplicationInfo;
|
||||||
|
import android.content.pm.PackageManager;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
import android.support.v4.app.FragmentActivity;
|
||||||
|
import android.support.v7.app.AlertDialog;
|
||||||
|
import android.view.ContextThemeWrapper;
|
||||||
|
|
||||||
|
import org.fdroid.fdroid.FDroidApp;
|
||||||
|
import org.fdroid.fdroid.R;
|
||||||
|
import org.fdroid.fdroid.installer.Installer;
|
||||||
|
|
||||||
|
public class UninstallDialogActivity extends FragmentActivity {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
|
final Intent intent = getIntent();
|
||||||
|
final String packageName = intent.getStringExtra(Installer.EXTRA_PACKAGE_NAME);
|
||||||
|
|
||||||
|
PackageManager pm = getPackageManager();
|
||||||
|
|
||||||
|
ApplicationInfo appInfo;
|
||||||
|
try {
|
||||||
|
//noinspection WrongConstant (lint is actually wrong here!)
|
||||||
|
appInfo = pm.getApplicationInfo(packageName, PackageManager.GET_UNINSTALLED_PACKAGES);
|
||||||
|
} catch (PackageManager.NameNotFoundException e) {
|
||||||
|
throw new RuntimeException("Failed to get ApplicationInfo for uninstalling");
|
||||||
|
}
|
||||||
|
|
||||||
|
final boolean isSystem = (appInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0;
|
||||||
|
final boolean isUpdate = (appInfo.flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0;
|
||||||
|
|
||||||
|
if (isSystem && !isUpdate) {
|
||||||
|
// Cannot remove system apps unless we're uninstalling updates
|
||||||
|
throw new RuntimeException("Cannot remove system apps unless we're uninstalling updates");
|
||||||
|
}
|
||||||
|
|
||||||
|
int messageId;
|
||||||
|
if (isUpdate) {
|
||||||
|
messageId = R.string.uninstall_update_confirm;
|
||||||
|
} else {
|
||||||
|
messageId = R.string.uninstall_confirm;
|
||||||
|
}
|
||||||
|
|
||||||
|
// hack to get theme applied (which is not automatically applied due to activity's Theme.NoDisplay
|
||||||
|
ContextThemeWrapper theme = new ContextThemeWrapper(this, FDroidApp.getCurThemeResId());
|
||||||
|
|
||||||
|
final AlertDialog.Builder builder = new AlertDialog.Builder(theme);
|
||||||
|
builder.setTitle(appInfo.loadLabel(pm));
|
||||||
|
builder.setIcon(appInfo.loadIcon(pm));
|
||||||
|
builder.setPositiveButton(android.R.string.ok,
|
||||||
|
new DialogInterface.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(DialogInterface dialog, int which) {
|
||||||
|
Intent data = new Intent();
|
||||||
|
data.putExtra(Installer.EXTRA_PACKAGE_NAME, packageName);
|
||||||
|
setResult(Activity.RESULT_OK, intent);
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
builder.setNegativeButton(android.R.string.cancel,
|
||||||
|
new DialogInterface.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(DialogInterface dialog, int which) {
|
||||||
|
setResult(Activity.RESULT_CANCELED);
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
builder.setOnCancelListener(
|
||||||
|
new DialogInterface.OnCancelListener() {
|
||||||
|
@Override
|
||||||
|
public void onCancel(DialogInterface dialog) {
|
||||||
|
setResult(Activity.RESULT_CANCELED);
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
builder.setMessage(messageId);
|
||||||
|
builder.create().show();
|
||||||
|
}
|
||||||
|
}
|
@ -225,9 +225,6 @@ public class PreferencesFragment extends PreferenceFragment
|
|||||||
case PrivilegedInstaller.IS_EXTENSION_INSTALLED_SIGNATURE_PROBLEM:
|
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");
|
||||||
}
|
}
|
||||||
|
@ -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!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
|
||||||
|
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user