From 7ed69c93fc208f4318f9cf8a37195fe818bb5e13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Sch=C3=BCrmann?= Date: Sun, 27 Apr 2014 18:56:47 +0200 Subject: [PATCH] Installation/Delete with root shell, add preference for root access, fixes to Installer classes --- res/values/strings.xml | 5 +- res/xml/preferences.xml | 6 +- src/org/fdroid/fdroid/AppDetails.java | 40 +--- src/org/fdroid/fdroid/Preferences.java | 8 +- .../fdroid/fdroid/PreferencesActivity.java | 7 +- .../fdroid/installer/DefaultInstaller.java | 6 +- .../fdroid/fdroid/installer/Installer.java | 51 +++-- .../fdroid/installer/RootInstaller.java | 203 ++++++++++++++++++ .../installer/SystemPermissionInstaller.java | 32 ++- 9 files changed, 298 insertions(+), 60 deletions(-) create mode 100644 src/org/fdroid/fdroid/installer/RootInstaller.java 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);