diff --git a/res/values/strings.xml b/res/values/strings.xml
index b068d0ae9..5a75d1d49 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -28,7 +28,10 @@
Do not notify of any updates
Update history
Days to consider apps new or recent: %s
-
+ Root access for app installations
+ Root access is used to install/delete/update applications
+ Do not request root access to install/delete/update applications
+
Search Results
App Details
No such app found
diff --git a/res/xml/preferences.xml b/res/xml/preferences.xml
index 5ae1f7d10..cb331b4d9 100644
--- a/res/xml/preferences.xml
+++ b/res/xml/preferences.xml
@@ -3,7 +3,8 @@
+
RootInstaller
+ boolean useRootInstaller = Preferences.get().useRootInstaller();
+ if (useRootInstaller) {
+ try {
+ return new RootInstaller(activity, pm, callback);
+ } catch (AndroidNotCompatibleException e) {
+ Log.e(TAG, "Android not compatible with RootInstaller!", e);
+ }
+ }
+
// system permissions -> SystemPermissionInstaller
if (hasSystemPermissions(activity, pm)) {
Log.d(TAG, "system permissions -> SystemPermissionInstaller");
@@ -104,7 +123,7 @@ abstract public class Installer {
}
}
- // try default installer
+ // Fallback -> DefaultInstaller
try {
Log.d(TAG, "try default installer");
@@ -122,6 +141,16 @@ abstract public class Installer {
public static Installer getUnattendedInstaller(Context context, PackageManager pm,
InstallerCallback callback) throws AndroidNotCompatibleException {
+ // if root installer has been activated in preferences -> RootInstaller
+ boolean useRootInstaller = Preferences.get().useRootInstaller();
+ if (useRootInstaller) {
+ try {
+ return new RootInstaller(context, pm, callback);
+ } catch (AndroidNotCompatibleException e) {
+ Log.e(TAG, "Android not compatible with RootInstaller!", e);
+ }
+ }
+
if (hasSystemPermissions(context, pm)) {
// we have system permissions!
return new SystemPermissionInstaller(context, pm, callback);
@@ -139,29 +168,13 @@ abstract public class Installer {
boolean permissionsGranted = (checkInstallPermission == PackageManager.PERMISSION_GRANTED
&& checkDeletePermission == PackageManager.PERMISSION_GRANTED);
- boolean isSystemApp;
- try {
- isSystemApp = isSystemApp(pm.getApplicationInfo(context.getPackageName(), 0));
- } catch (NameNotFoundException e) {
- isSystemApp = false;
- }
-
- // TODO: is this right???
- // two ways to be able to get system permissions: somehow the
- // permissions where actually granted on install or the app has been
- // moved later to the system partition -> also access
- if (permissionsGranted || isSystemApp) {
+ if (permissionsGranted) {
return true;
} else {
return false;
}
}
- private static boolean isSystemApp(ApplicationInfo ai) {
- int mask = ApplicationInfo.FLAG_SYSTEM | ApplicationInfo.FLAG_UPDATED_SYSTEM_APP;
- return (ai.flags & mask) != 0;
- }
-
public void installPackage(File apkFile) throws AndroidNotCompatibleException {
// check if file exists...
if (!apkFile.exists()) {
diff --git a/src/org/fdroid/fdroid/installer/RootInstaller.java b/src/org/fdroid/fdroid/installer/RootInstaller.java
new file mode 100644
index 000000000..d57294140
--- /dev/null
+++ b/src/org/fdroid/fdroid/installer/RootInstaller.java
@@ -0,0 +1,203 @@
+/*
+ * Copyright (C) 2014 Dominik Schürmann
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 3
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+ * MA 02110-1301, USA.
+ */
+
+package org.fdroid.fdroid.installer;
+
+import java.io.File;
+import java.util.List;
+
+import eu.chainfire.libsuperuser.Shell;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+
+/**
+ * Installer using a root shell and "pm install", "pm uninstall" commands
+ */
+public class RootInstaller extends Installer {
+
+ Shell.Interactive rootSession;
+
+ public RootInstaller(Context context, PackageManager pm, InstallerCallback callback)
+ throws AndroidNotCompatibleException {
+ super(context, pm, callback);
+ }
+
+ @Override
+ public void installPackage(final File apkFile) throws AndroidNotCompatibleException {
+ super.installPackage(apkFile);
+
+ Shell.Builder shellBuilder = new Shell.Builder()
+ .useSU()
+ .setWantSTDERR(true)
+ .setWatchdogTimeout(5)
+ .setMinimalLogging(true);
+
+ rootSession = shellBuilder.open(new Shell.OnCommandResultListener() {
+
+ // Callback to report whether the shell was successfully
+ // started up
+ @Override
+ public void onCommandResult(int commandCode, int exitCode, List output) {
+ if (exitCode != Shell.OnCommandResultListener.SHELL_RUNNING) {
+ // TODO
+ // wrong uid
+ // Shell.OnCommandResultListener.SHELL_WRONG_UID
+ // exec failed
+ // Shell.OnCommandResultListener.SHELL_EXEC_FAILED
+
+ // reportError("Error opening root shell: exitCode " +
+ // exitCode);
+ } else {
+ // Shell is up: send our first request
+ sendInstallCommand(apkFile);
+ }
+ }
+ });
+ }
+
+ @Override
+ public void deletePackage(final String packageName) throws AndroidNotCompatibleException {
+ super.deletePackage(packageName);
+
+ Shell.Builder shellBuilder = new Shell.Builder()
+ .useSU()
+ .setWantSTDERR(true)
+ .setWatchdogTimeout(5)
+ .setMinimalLogging(true);
+
+ rootSession = shellBuilder.open(new Shell.OnCommandResultListener() {
+
+ // Callback to report whether the shell was successfully
+ // started up
+ @Override
+ public void onCommandResult(int commandCode, int exitCode, List output) {
+ if (exitCode != Shell.OnCommandResultListener.SHELL_RUNNING) {
+ // TODO
+ // wrong uid
+ // Shell.OnCommandResultListener.SHELL_WRONG_UID
+ // exec failed
+ // Shell.OnCommandResultListener.SHELL_EXEC_FAILED
+
+ // reportError("Error opening root shell: exitCode " +
+ // exitCode);
+ } else {
+ // Shell is up: send our first request
+ sendDeleteCommand(packageName);
+ }
+ }
+ });
+
+ }
+
+ @Override
+ public boolean handleOnActivityResult(int requestCode, int resultCode, Intent data) {
+ // no need to handle onActivityResult
+ return false;
+ }
+
+ private void sendInstallCommand(File apkFile) {
+ rootSession.addCommand("pm install -r " + apkFile.getAbsolutePath(), 0,
+ new Shell.OnCommandResultListener() {
+ public void onCommandResult(int commandCode, int exitCode, List output) {
+ // close su shell
+ rootSession.close();
+
+ if (exitCode < 0) {
+ // reportError("Error executing commands: exitCode "
+ // + exitCode);
+ mCallback.onPackageInstalled(InstallerCallback.RETURN_CANCEL, true);
+ } else {
+ // wait until Android's internal PackageManger has
+ // received the new package state
+ Thread wait = new Thread(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ Thread.sleep(2000);
+ } catch (InterruptedException e) {
+ }
+
+ mCallback.onPackageInstalled(InstallerCallback.RETURN_SUCCESS,
+ true);
+ }
+ });
+ wait.start();
+ }
+ }
+ });
+ }
+
+ private void sendDeleteCommand(String packageName) {
+ rootSession.addCommand("pm uninstall " + packageName, 0,
+ new Shell.OnCommandResultListener() {
+ public void onCommandResult(int commandCode, int exitCode, List output) {
+ // close su shell
+ rootSession.close();
+
+ if (exitCode < 0) {
+ // reportError("Error executing commands: exitCode "
+ // + exitCode);
+ mCallback.onPackageDeleted(InstallerCallback.RETURN_CANCEL, true);
+ } else {
+ // wait until Android's internal PackageManger has
+ // received the new package state
+ Thread wait = new Thread(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ Thread.sleep(2000);
+ } catch (InterruptedException e) {
+ }
+
+ mCallback.onPackageDeleted(InstallerCallback.RETURN_SUCCESS,
+ true);
+ }
+ });
+ wait.start();
+ }
+ }
+ });
+ }
+
+ /**
+ * pm install [-l] [-r] [-t] [-i INSTALLER_PACKAGE_NAME] [-s] [-f] [--algo
+ * --key --iv ] [--originating-uri
+ * ] [--referrer ] PATH
+ *
+ * pm install: installs a package to the system.
+ *
+ * Options:
+ * -l: install the package with FORWARD_LOCK.
+ * -r: reinstall an exisiting app, keeping its data.
+ * -t: allow test .apks to be installed.
+ * -i: specify the installer package name.
+ * -s: install package on sdcard.
+ * -f: install package on internal flash.
+ * -d: allow version code downgrade.
+ *
+ * pm uninstall [-k] PACKAGE
+ *
+ * pm uninstall: removes a package from the system.
+ *
+ * Options:
+ * -k: keep the data and cache directories around after package removal.
+ */
+
+}
diff --git a/src/org/fdroid/fdroid/installer/SystemPermissionInstaller.java b/src/org/fdroid/fdroid/installer/SystemPermissionInstaller.java
index 207cb57b9..6cd07e080 100644
--- a/src/org/fdroid/fdroid/installer/SystemPermissionInstaller.java
+++ b/src/org/fdroid/fdroid/installer/SystemPermissionInstaller.java
@@ -82,7 +82,21 @@ public class SystemPermissionInstaller extends Installer {
// TODO: propagate other return codes?
if (returnCode == INSTALL_SUCCEEDED) {
Log.d(TAG, "Install succeeded");
- mCallback.onPackageInstalled(InstallerCallback.RETURN_SUCCESS, true);
+
+ // wait until Android's internal PackageManger has
+ // received the new package state
+ Thread wait = new Thread(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ Thread.sleep(2000);
+ } catch (InterruptedException e) {
+ }
+
+ mCallback.onPackageInstalled(InstallerCallback.RETURN_SUCCESS, true);
+ }
+ });
+ wait.start();
} else {
Log.d(TAG, "Install failed: " + returnCode);
mCallback.onPackageInstalled(InstallerCallback.RETURN_CANCEL, true);
@@ -98,7 +112,21 @@ public class SystemPermissionInstaller extends Installer {
// TODO: propagate other return codes?
if (returnCode == DELETE_SUCCEEDED) {
Log.d(TAG, "Delete succeeded");
- mCallback.onPackageDeleted(InstallerCallback.RETURN_SUCCESS, true);
+
+ // wait until Android's internal PackageManger has
+ // received the new package state
+ Thread wait = new Thread(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ Thread.sleep(2000);
+ } catch (InterruptedException e) {
+ }
+
+ mCallback.onPackageDeleted(InstallerCallback.RETURN_SUCCESS, true);
+ }
+ });
+ wait.start();
} else {
Log.d(TAG, "Delete failed: " + returnCode);
mCallback.onPackageDeleted(InstallerCallback.RETURN_CANCEL, true);