From 185868fb527c3d4ba819c541d128403d60ebb12c Mon Sep 17 00:00:00 2001 From: Jorrit Jongma Date: Wed, 5 Feb 2014 14:30:48 +0100 Subject: [PATCH] Whitespace: tabs to spaces, as per Android guidelines --- .../chainfire/libsuperuser/Application.java | 102 +- .../src/eu/chainfire/libsuperuser/Debug.java | 428 +-- .../src/eu/chainfire/libsuperuser/Shell.java | 2482 ++++++++--------- .../libsuperuser/ShellNotClosedException.java | 8 +- .../ShellOnMainThreadException.java | 14 +- .../chainfire/libsuperuser/StreamGobbler.java | 146 +- .../BackgroundIntentService.java | 94 +- .../BootCompleteReceiver.java | 30 +- .../InteractiveActivity.java | 162 +- .../libsuperuser_example/MainActivity.java | 148 +- 10 files changed, 1807 insertions(+), 1807 deletions(-) diff --git a/libsuperuser/src/eu/chainfire/libsuperuser/Application.java b/libsuperuser/src/eu/chainfire/libsuperuser/Application.java index 454740a..55bbf32 100644 --- a/libsuperuser/src/eu/chainfire/libsuperuser/Application.java +++ b/libsuperuser/src/eu/chainfire/libsuperuser/Application.java @@ -25,55 +25,55 @@ * toasts and AsyncTasks you are likely to run into */ public class Application extends android.app.Application { - /** - * Shows a toast message - * - * @param context Any context belonging to this application - * @param message The message to show - */ - public static void toast(Context context, String message) { - // this is a static method so it is easier to call, - // as the context checking and casting is done for you - - if (context == null) return; - - if (!(context instanceof Application)) { - context = context.getApplicationContext(); - } - - if (context instanceof Application) { - final Context c = context; - final String m = message; - - ((Application)context).runInApplicationThread(new Runnable() { - @Override - public void run() { - Toast.makeText(c, m, Toast.LENGTH_LONG).show(); - } - }); - } - } - - private static Handler mApplicationHandler = new Handler(); - - /** - * Run a runnable in the main application thread - * - * @param r Runnable to run - */ - public void runInApplicationThread(Runnable r) { - mApplicationHandler.post(r); - } - - @Override - public void onCreate() { - super.onCreate(); - - try { - // workaround bug in AsyncTask, can show up (for example) when you toast from a service - // this makes sure AsyncTask's internal handler is created from the right (main) thread - Class.forName("android.os.AsyncTask"); - } catch (ClassNotFoundException e) { - } - } + /** + * Shows a toast message + * + * @param context Any context belonging to this application + * @param message The message to show + */ + public static void toast(Context context, String message) { + // this is a static method so it is easier to call, + // as the context checking and casting is done for you + + if (context == null) return; + + if (!(context instanceof Application)) { + context = context.getApplicationContext(); + } + + if (context instanceof Application) { + final Context c = context; + final String m = message; + + ((Application)context).runInApplicationThread(new Runnable() { + @Override + public void run() { + Toast.makeText(c, m, Toast.LENGTH_LONG).show(); + } + }); + } + } + + private static Handler mApplicationHandler = new Handler(); + + /** + * Run a runnable in the main application thread + * + * @param r Runnable to run + */ + public void runInApplicationThread(Runnable r) { + mApplicationHandler.post(r); + } + + @Override + public void onCreate() { + super.onCreate(); + + try { + // workaround bug in AsyncTask, can show up (for example) when you toast from a service + // this makes sure AsyncTask's internal handler is created from the right (main) thread + Class.forName("android.os.AsyncTask"); + } catch (ClassNotFoundException e) { + } + } } diff --git a/libsuperuser/src/eu/chainfire/libsuperuser/Debug.java b/libsuperuser/src/eu/chainfire/libsuperuser/Debug.java index 2d059e3..bbf029a 100644 --- a/libsuperuser/src/eu/chainfire/libsuperuser/Debug.java +++ b/libsuperuser/src/eu/chainfire/libsuperuser/Debug.java @@ -23,218 +23,218 @@ * Utility class for logging and debug features that (by default) does nothing when not in debug mode */ public class Debug { - - // ----- DEBUGGING ----- - - private static boolean debug = BuildConfig.DEBUG; - - /** - *

Enable or disable debug mode

- * - *

By default, debug mode is enabled for development - * builds and disabled for exported APKs - see - * BuildConfig.DEBUG

- * - * @param enabled Enable debug mode ? - */ - public static void setDebug(boolean enable) { - debug = enable; - } - - /** - *

Is debug mode enabled ?

- * - * @return Debug mode enabled - */ - public static boolean getDebug() { - return debug; - } - - // ----- LOGGING ----- - - public interface OnLogListener { - public void onLog(int type, String typeIndicator, String message); - } - - public static final String TAG = "libsuperuser"; - - public static final int LOG_GENERAL = 0x0001; - public static final int LOG_COMMAND = 0x0002; - public static final int LOG_OUTPUT = 0x0004; - - public static final int LOG_NONE = 0x0000; - public static final int LOG_ALL = 0xFFFF; - - private static int logTypes = LOG_ALL; - - private static OnLogListener logListener = null; - - /** - *

Log a message (internal)

- * - *

Current debug and enabled logtypes decide what gets logged - - * even if a custom callback is registered

- * - * @param type Type of message to log - * @param typeIndicator String indicator for message type - * @param message The message to log - */ - private static void logCommon(int type, String typeIndicator, String message) { - if (debug && ((logTypes & type) == type)) { - if (logListener != null) { - logListener.onLog(type, typeIndicator, message); - } else { - Log.d(TAG, "[" + TAG + "][" + typeIndicator + "]" + (!message.startsWith("[") && !message.startsWith(" ") ? " " : "") + message); - } - } - } - - /** - *

Log a "general" message

- * - *

These messages are infrequent and mostly occur at startup/shutdown or on error

- * - * @param message The message to log - */ - public static void log(String message) { - logCommon(LOG_GENERAL, "G", message); - } - - /** - *

Log a "per-command" message

- * - *

This could produce a lot of output if the client runs many commands in the session

- * - * @param message The message to log - */ - public static void logCommand(String message) { - logCommon(LOG_COMMAND, "C", message); - } - - /** - *

Log a line of stdout/stderr output

- * - *

This could produce a lot of output if the shell commands are noisy

- * - * @param message The message to log - */ - public static void logOutput(String message) { - logCommon(LOG_OUTPUT, "O", message); - } - - /** - *

Enable or disable logging specific types of message

- * - *

You may | (or) LOG_* constants together. Note that - * debug mode must also be enabled for actual logging to - * occur.

- * - * @param type LOG_* constants - * @param enabled Enable or disable - */ - public static void setLogTypeEnabled(int type, boolean enable) { - if (enable) { - logTypes |= type; - } else { - logTypes &= ~type; - } - } - - /** - *

Is logging for specific types of messages enabled ?

- * - *

You may | (or) LOG_* constants together, to learn if - * all passed message types are enabled for logging. Note - * that debug mode must also be enabled for actual logging - * to occur.

- * - * @param type LOG_* constants - */ - public static boolean getLogTypeEnabled(int type) { - return ((logTypes & type) == type); - } - - /** - *

Is logging for specific types of messages enabled ?

- * - *

You may | (or) LOG_* constants together, to learn if - * all message types are enabled for logging. Takes - * debug mode into account for the result.

- * - * @param type LOG_* constants - */ - public static boolean getLogTypeEnabledEffective(int type) { - return getDebug() && getLogTypeEnabled(type); - } - - /** - *

Register a custom log handler

- * - *

Replaces the log method (write to logcat) with your own - * handler. Whether your handler gets called is still dependent - * on debug mode and message types being enabled for logging.

- * - * @param onLogListener Custom log listener or NULL to revert to default - */ - public static void setOnLogListener(OnLogListener onLogListener) { - logListener = onLogListener; - } - - /** - *

Get the currently registered custom log handler

- * - * @return Current custom log handler or NULL if none is present - */ - public static OnLogListener getOnLogListener() { - return logListener; - } - - // ----- SANITY CHECKS ----- - - private static boolean sanityChecks = true; - - /** - *

Enable or disable sanity checks

- * - *

Enables or disables the library crashing when su is called - * from the main thread.

- * - * @param enabled Enable or disable - */ - public static void setSanityChecksEnabled(boolean enable) { - sanityChecks = enable; - } - - /** - *

Are sanity checks enabled ?

- * - *

Note that debug mode must also be enabled for actual - * sanity checks to occur.

- * - * @return True if enabled - */ - public static boolean getSanityChecksEnabled() { - return sanityChecks; - } - - /** - *

Are sanity checks enabled ?

- * - *

Takes debug mode into account for the result.

- * - * @return True if enabled - */ - public static boolean getSanityChecksEnabledEffective() { - return getDebug() && getSanityChecksEnabled(); - } - - /** - *

Are we running on the main thread ?

- * - * @return Running on main thread ? - */ - public static boolean onMainThread() { - return ((Looper.myLooper() != null) && (Looper.myLooper() == Looper.getMainLooper())); - } - + + // ----- DEBUGGING ----- + + private static boolean debug = BuildConfig.DEBUG; + + /** + *

Enable or disable debug mode

+ * + *

By default, debug mode is enabled for development + * builds and disabled for exported APKs - see + * BuildConfig.DEBUG

+ * + * @param enabled Enable debug mode ? + */ + public static void setDebug(boolean enable) { + debug = enable; + } + + /** + *

Is debug mode enabled ?

+ * + * @return Debug mode enabled + */ + public static boolean getDebug() { + return debug; + } + + // ----- LOGGING ----- + + public interface OnLogListener { + public void onLog(int type, String typeIndicator, String message); + } + + public static final String TAG = "libsuperuser"; + + public static final int LOG_GENERAL = 0x0001; + public static final int LOG_COMMAND = 0x0002; + public static final int LOG_OUTPUT = 0x0004; + + public static final int LOG_NONE = 0x0000; + public static final int LOG_ALL = 0xFFFF; + + private static int logTypes = LOG_ALL; + + private static OnLogListener logListener = null; + + /** + *

Log a message (internal)

+ * + *

Current debug and enabled logtypes decide what gets logged - + * even if a custom callback is registered

+ * + * @param type Type of message to log + * @param typeIndicator String indicator for message type + * @param message The message to log + */ + private static void logCommon(int type, String typeIndicator, String message) { + if (debug && ((logTypes & type) == type)) { + if (logListener != null) { + logListener.onLog(type, typeIndicator, message); + } else { + Log.d(TAG, "[" + TAG + "][" + typeIndicator + "]" + (!message.startsWith("[") && !message.startsWith(" ") ? " " : "") + message); + } + } + } + + /** + *

Log a "general" message

+ * + *

These messages are infrequent and mostly occur at startup/shutdown or on error

+ * + * @param message The message to log + */ + public static void log(String message) { + logCommon(LOG_GENERAL, "G", message); + } + + /** + *

Log a "per-command" message

+ * + *

This could produce a lot of output if the client runs many commands in the session

+ * + * @param message The message to log + */ + public static void logCommand(String message) { + logCommon(LOG_COMMAND, "C", message); + } + + /** + *

Log a line of stdout/stderr output

+ * + *

This could produce a lot of output if the shell commands are noisy

+ * + * @param message The message to log + */ + public static void logOutput(String message) { + logCommon(LOG_OUTPUT, "O", message); + } + + /** + *

Enable or disable logging specific types of message

+ * + *

You may | (or) LOG_* constants together. Note that + * debug mode must also be enabled for actual logging to + * occur.

+ * + * @param type LOG_* constants + * @param enabled Enable or disable + */ + public static void setLogTypeEnabled(int type, boolean enable) { + if (enable) { + logTypes |= type; + } else { + logTypes &= ~type; + } + } + + /** + *

Is logging for specific types of messages enabled ?

+ * + *

You may | (or) LOG_* constants together, to learn if + * all passed message types are enabled for logging. Note + * that debug mode must also be enabled for actual logging + * to occur.

+ * + * @param type LOG_* constants + */ + public static boolean getLogTypeEnabled(int type) { + return ((logTypes & type) == type); + } + + /** + *

Is logging for specific types of messages enabled ?

+ * + *

You may | (or) LOG_* constants together, to learn if + * all message types are enabled for logging. Takes + * debug mode into account for the result.

+ * + * @param type LOG_* constants + */ + public static boolean getLogTypeEnabledEffective(int type) { + return getDebug() && getLogTypeEnabled(type); + } + + /** + *

Register a custom log handler

+ * + *

Replaces the log method (write to logcat) with your own + * handler. Whether your handler gets called is still dependent + * on debug mode and message types being enabled for logging.

+ * + * @param onLogListener Custom log listener or NULL to revert to default + */ + public static void setOnLogListener(OnLogListener onLogListener) { + logListener = onLogListener; + } + + /** + *

Get the currently registered custom log handler

+ * + * @return Current custom log handler or NULL if none is present + */ + public static OnLogListener getOnLogListener() { + return logListener; + } + + // ----- SANITY CHECKS ----- + + private static boolean sanityChecks = true; + + /** + *

Enable or disable sanity checks

+ * + *

Enables or disables the library crashing when su is called + * from the main thread.

+ * + * @param enabled Enable or disable + */ + public static void setSanityChecksEnabled(boolean enable) { + sanityChecks = enable; + } + + /** + *

Are sanity checks enabled ?

+ * + *

Note that debug mode must also be enabled for actual + * sanity checks to occur.

+ * + * @return True if enabled + */ + public static boolean getSanityChecksEnabled() { + return sanityChecks; + } + + /** + *

Are sanity checks enabled ?

+ * + *

Takes debug mode into account for the result.

+ * + * @return True if enabled + */ + public static boolean getSanityChecksEnabledEffective() { + return getDebug() && getSanityChecksEnabled(); + } + + /** + *

Are we running on the main thread ?

+ * + * @return Running on main thread ? + */ + public static boolean onMainThread() { + return ((Looper.myLooper() != null) && (Looper.myLooper() == Looper.getMainLooper())); + } + } diff --git a/libsuperuser/src/eu/chainfire/libsuperuser/Shell.java b/libsuperuser/src/eu/chainfire/libsuperuser/Shell.java index 5c9caad..496ba0f 100644 --- a/libsuperuser/src/eu/chainfire/libsuperuser/Shell.java +++ b/libsuperuser/src/eu/chainfire/libsuperuser/Shell.java @@ -41,304 +41,304 @@ * Class providing functionality to execute commands in a (root) shell */ public class Shell { - /** - *

Runs commands using the supplied shell, and returns the output, or null in - * case of errors.

- * - *

This method is deprecated and only provided for backwards compatibility. - * Use {@link #run(String, String[], String[], boolean)} instead, and see that - * same method for usage notes.

- * - * @param shell The shell to use for executing the commands - * @param commands The commands to execute - * @param wantSTDERR Return STDERR in the output ? - * @return Output of the commands, or null in case of an error - */ - @Deprecated - public static List run(String shell, String[] commands, boolean wantSTDERR) { - return run(shell, commands, null, wantSTDERR); - } - - /** - *

Runs commands using the supplied shell, and returns the output, or null in - * case of errors.

- * - *

Note that due to compatibility with older Android versions, - * wantSTDERR is not implemented using redirectErrorStream, but rather appended - * to the output. STDOUT and STDERR are thus not guaranteed to be in the correct - * order in the output.

- * - *

Note as well that this code will intentionally crash when run in debug mode - * from the main thread of the application. You should always execute shell - * commands from a background thread.

- * - *

When in debug mode, the code will also excessively log the commands passed to - * and the output returned from the shell.

- * - *

Though this function uses background threads to gobble STDOUT and STDERR so - * a deadlock does not occur if the shell produces massive output, the output is - * still stored in a List<String>, and as such doing something like 'ls -lR /' - * will probably have you run out of memory.

- * - * @param shell The shell to use for executing the commands - * @param commands The commands to execute - * @param environment List of all environment variables (in 'key=value' format) or null for defaults - * @param wantSTDERR Return STDERR in the output ? - * @return Output of the commands, or null in case of an error - */ - public static List run(String shell, String[] commands, String[] environment, boolean wantSTDERR) { - String shellUpper = shell.toUpperCase(Locale.ENGLISH); - - if (Debug.getSanityChecksEnabledEffective() && Debug.onMainThread()) { - // check if we're running in the main thread, and if so, crash if we're in debug mode, - // to let the developer know attention is needed here. - - Debug.log(ShellOnMainThreadException.EXCEPTION_COMMAND); - throw new ShellOnMainThreadException(ShellOnMainThreadException.EXCEPTION_COMMAND); - } - Debug.logCommand(String.format("[%s%%] START", shellUpper)); - - List res = Collections.synchronizedList(new ArrayList()); - - try { - // Combine passed environment with system environment - if (environment != null) { - Map newEnvironment = new HashMap(); - newEnvironment.putAll(System.getenv()); - int split; - for (String entry : environment) { - if ((split = entry.indexOf("=")) >= 0) { - newEnvironment.put(entry.substring(0, split), entry.substring(split + 1)); - } - } - int i = 0; - environment = new String[newEnvironment.size()]; - for (Map.Entry entry : newEnvironment.entrySet()) { - environment[i] = entry.getKey() + "=" + entry.getValue(); - i++; - } - } - - // setup our process, retrieve STDIN stream, and STDOUT/STDERR gobblers - Process process = Runtime.getRuntime().exec(shell, environment); - DataOutputStream STDIN = new DataOutputStream(process.getOutputStream()); - StreamGobbler STDOUT = new StreamGobbler(shellUpper + "-", process.getInputStream(), res); - StreamGobbler STDERR = new StreamGobbler(shellUpper + "*", process.getErrorStream(), wantSTDERR ? res : null); - - // start gobbling and write our commands to the shell - STDOUT.start(); - STDERR.start(); - for (String write : commands) { - Debug.logCommand(String.format("[%s+] %s", shellUpper, write)); - STDIN.write((write + "\n").getBytes("UTF-8")); - STDIN.flush(); - } - STDIN.write("exit\n".getBytes("UTF-8")); - STDIN.flush(); - - // wait for our process to finish, while we gobble away in the background - process.waitFor(); - - // make sure our threads are done gobbling, our streams are closed, and the process is - // destroyed - while the latter two shouldn't be needed in theory, and may even produce - // warnings, in "normal" Java they are required for guaranteed cleanup of resources, so - // lets be safe and do this on Android as well - try { - STDIN.close(); - } catch (IOException e) { - } - STDOUT.join(); - STDERR.join(); - process.destroy(); - - // in case of su, 255 usually indicates access denied - if (SU.isSU(shell) && (process.exitValue() == 255)) { - res = null; - } - } catch (IOException e) { - // shell probably not found - res = null; - } catch (InterruptedException e) { - // this should really be re-thrown - res = null; - } - - Debug.logCommand(String.format("[%s%%] END", shell.toUpperCase(Locale.ENGLISH))); - return res; - } - - protected static String[] availableTestCommands = new String[] { - "echo -BOC-", - "id" - }; - - /** - * See if the shell is alive, and if so, check the UID - * - * @param ret Standard output from running availableTestCommands - * @param checkForRoot true if we are expecting this shell to be running as root - * @return true on success, false on error - */ - protected static boolean parseAvailableResult(List ret, boolean checkForRoot) { - if (ret == null) return false; - - // this is only one of many ways this can be done - boolean echo_seen = false; - - for (String line : ret) { - if (line.contains("uid=")) { - // id command is working, let's see if we are actually root - return !checkForRoot || line.contains("uid=0"); - } else if (line.contains("-BOC-")) { - // if we end up here, at least the su command starts some kind of shell, - // let's hope it has root privileges - no way to know without additional - // native binaries - echo_seen = true; - } - } - - return echo_seen; - } - - /** - * This class provides utility functions to easily execute commands using SH - */ - public static class SH { - /** - * Runs command and return output - * - * @param command The command to run - * @return Output of the command, or null in case of an error - */ - public static List run(String command) { - return Shell.run("sh", new String[] { command }, null, false); - } - - /** - * Runs commands and return output - * - * @param commands The commands to run - * @return Output of the commands, or null in case of an error - */ - public static List run(List commands) { - return Shell.run("sh", commands.toArray(new String[commands.size()]), null, false); - } - - /** - * Runs commands and return output - * - * @param commands The commands to run - * @return Output of the commands, or null in case of an error - */ - public static List run(String[] commands) { - return Shell.run("sh", commands, null, false); - } - } - - /** - * This class provides utility functions to easily execute commands using SU - * (root shell), as well as detecting whether or not root is available, and - * if so which version. - */ - public static class SU { - /** - * Runs command as root (if available) and return output - * - * @param command The command to run - * @return Output of the command, or null if root isn't available or in case of an error - */ - public static List run(String command) { - return Shell.run("su", new String[] { command }, null, false); - } - - /** - * Runs commands as root (if available) and return output - * - * @param commands The commands to run - * @return Output of the commands, or null if root isn't available or in case of an error - */ - public static List run(List commands) { - return Shell.run("su", commands.toArray(new String[commands.size()]), null, false); - } - - /** - * Runs commands as root (if available) and return output - * - * @param commands The commands to run - * @return Output of the commands, or null if root isn't available or in case of an error - */ - public static List run(String[] commands) { - return Shell.run("su", commands, null, false); - } - - /** - * Detects whether or not superuser access is available, by checking the output - * of the "id" command if available, checking if a shell runs at all otherwise - * - * @return True if superuser access available - */ - public static boolean available() { - // this is only one of many ways this can be done - - List ret = run(Shell.availableTestCommands); - return Shell.parseAvailableResult(ret, true); - } - - /** - *

Detects the version of the su binary installed (if any), if supported by the binary. - * Most binaries support two different version numbers, the public version that is - * displayed to users, and an internal version number that is used for version number - * comparisons. Returns null if su not available or retrieving the version isn't supported.

- * - *

Note that su binary version and GUI (APK) version can be completely different.

- * - * @param internal Request human-readable version or application internal version - * @return String containing the su version or null - */ - public static String version(boolean internal) { - List ret = Shell.run( - internal ? "su -V" : "su -v", - new String[] { }, - null, - false - ); - if (ret == null) return null; - - for (String line : ret) { - if (!internal) { - if (line.contains(".")) return line; - } else { - try { - if (Integer.parseInt(line) > 0) return line; - } catch(NumberFormatException e) { - } - } - } - return null; - } - - /** - * Attempts to deduce if the shell command refers to a su shell - * - * @param shell Shell command to run - * @return Shell command appears to be su - */ - public static boolean isSU(String shell) { - // Strip parameters - int pos = shell.indexOf(' '); - if (pos >= 0) { - shell = shell.substring(0, pos); - } - - // Strip path - pos = shell.lastIndexOf('/'); - if (pos >= 0) { - shell = shell.substring(pos + 1); - } - - return shell.equals("su"); - } - + /** + *

Runs commands using the supplied shell, and returns the output, or null in + * case of errors.

+ * + *

This method is deprecated and only provided for backwards compatibility. + * Use {@link #run(String, String[], String[], boolean)} instead, and see that + * same method for usage notes.

+ * + * @param shell The shell to use for executing the commands + * @param commands The commands to execute + * @param wantSTDERR Return STDERR in the output ? + * @return Output of the commands, or null in case of an error + */ + @Deprecated + public static List run(String shell, String[] commands, boolean wantSTDERR) { + return run(shell, commands, null, wantSTDERR); + } + + /** + *

Runs commands using the supplied shell, and returns the output, or null in + * case of errors.

+ * + *

Note that due to compatibility with older Android versions, + * wantSTDERR is not implemented using redirectErrorStream, but rather appended + * to the output. STDOUT and STDERR are thus not guaranteed to be in the correct + * order in the output.

+ * + *

Note as well that this code will intentionally crash when run in debug mode + * from the main thread of the application. You should always execute shell + * commands from a background thread.

+ * + *

When in debug mode, the code will also excessively log the commands passed to + * and the output returned from the shell.

+ * + *

Though this function uses background threads to gobble STDOUT and STDERR so + * a deadlock does not occur if the shell produces massive output, the output is + * still stored in a List<String>, and as such doing something like 'ls -lR /' + * will probably have you run out of memory.

+ * + * @param shell The shell to use for executing the commands + * @param commands The commands to execute + * @param environment List of all environment variables (in 'key=value' format) or null for defaults + * @param wantSTDERR Return STDERR in the output ? + * @return Output of the commands, or null in case of an error + */ + public static List run(String shell, String[] commands, String[] environment, boolean wantSTDERR) { + String shellUpper = shell.toUpperCase(Locale.ENGLISH); + + if (Debug.getSanityChecksEnabledEffective() && Debug.onMainThread()) { + // check if we're running in the main thread, and if so, crash if we're in debug mode, + // to let the developer know attention is needed here. + + Debug.log(ShellOnMainThreadException.EXCEPTION_COMMAND); + throw new ShellOnMainThreadException(ShellOnMainThreadException.EXCEPTION_COMMAND); + } + Debug.logCommand(String.format("[%s%%] START", shellUpper)); + + List res = Collections.synchronizedList(new ArrayList()); + + try { + // Combine passed environment with system environment + if (environment != null) { + Map newEnvironment = new HashMap(); + newEnvironment.putAll(System.getenv()); + int split; + for (String entry : environment) { + if ((split = entry.indexOf("=")) >= 0) { + newEnvironment.put(entry.substring(0, split), entry.substring(split + 1)); + } + } + int i = 0; + environment = new String[newEnvironment.size()]; + for (Map.Entry entry : newEnvironment.entrySet()) { + environment[i] = entry.getKey() + "=" + entry.getValue(); + i++; + } + } + + // setup our process, retrieve STDIN stream, and STDOUT/STDERR gobblers + Process process = Runtime.getRuntime().exec(shell, environment); + DataOutputStream STDIN = new DataOutputStream(process.getOutputStream()); + StreamGobbler STDOUT = new StreamGobbler(shellUpper + "-", process.getInputStream(), res); + StreamGobbler STDERR = new StreamGobbler(shellUpper + "*", process.getErrorStream(), wantSTDERR ? res : null); + + // start gobbling and write our commands to the shell + STDOUT.start(); + STDERR.start(); + for (String write : commands) { + Debug.logCommand(String.format("[%s+] %s", shellUpper, write)); + STDIN.write((write + "\n").getBytes("UTF-8")); + STDIN.flush(); + } + STDIN.write("exit\n".getBytes("UTF-8")); + STDIN.flush(); + + // wait for our process to finish, while we gobble away in the background + process.waitFor(); + + // make sure our threads are done gobbling, our streams are closed, and the process is + // destroyed - while the latter two shouldn't be needed in theory, and may even produce + // warnings, in "normal" Java they are required for guaranteed cleanup of resources, so + // lets be safe and do this on Android as well + try { + STDIN.close(); + } catch (IOException e) { + } + STDOUT.join(); + STDERR.join(); + process.destroy(); + + // in case of su, 255 usually indicates access denied + if (SU.isSU(shell) && (process.exitValue() == 255)) { + res = null; + } + } catch (IOException e) { + // shell probably not found + res = null; + } catch (InterruptedException e) { + // this should really be re-thrown + res = null; + } + + Debug.logCommand(String.format("[%s%%] END", shell.toUpperCase(Locale.ENGLISH))); + return res; + } + + protected static String[] availableTestCommands = new String[] { + "echo -BOC-", + "id" + }; + + /** + * See if the shell is alive, and if so, check the UID + * + * @param ret Standard output from running availableTestCommands + * @param checkForRoot true if we are expecting this shell to be running as root + * @return true on success, false on error + */ + protected static boolean parseAvailableResult(List ret, boolean checkForRoot) { + if (ret == null) return false; + + // this is only one of many ways this can be done + boolean echo_seen = false; + + for (String line : ret) { + if (line.contains("uid=")) { + // id command is working, let's see if we are actually root + return !checkForRoot || line.contains("uid=0"); + } else if (line.contains("-BOC-")) { + // if we end up here, at least the su command starts some kind of shell, + // let's hope it has root privileges - no way to know without additional + // native binaries + echo_seen = true; + } + } + + return echo_seen; + } + + /** + * This class provides utility functions to easily execute commands using SH + */ + public static class SH { + /** + * Runs command and return output + * + * @param command The command to run + * @return Output of the command, or null in case of an error + */ + public static List run(String command) { + return Shell.run("sh", new String[] { command }, null, false); + } + + /** + * Runs commands and return output + * + * @param commands The commands to run + * @return Output of the commands, or null in case of an error + */ + public static List run(List commands) { + return Shell.run("sh", commands.toArray(new String[commands.size()]), null, false); + } + + /** + * Runs commands and return output + * + * @param commands The commands to run + * @return Output of the commands, or null in case of an error + */ + public static List run(String[] commands) { + return Shell.run("sh", commands, null, false); + } + } + + /** + * This class provides utility functions to easily execute commands using SU + * (root shell), as well as detecting whether or not root is available, and + * if so which version. + */ + public static class SU { + /** + * Runs command as root (if available) and return output + * + * @param command The command to run + * @return Output of the command, or null if root isn't available or in case of an error + */ + public static List run(String command) { + return Shell.run("su", new String[] { command }, null, false); + } + + /** + * Runs commands as root (if available) and return output + * + * @param commands The commands to run + * @return Output of the commands, or null if root isn't available or in case of an error + */ + public static List run(List commands) { + return Shell.run("su", commands.toArray(new String[commands.size()]), null, false); + } + + /** + * Runs commands as root (if available) and return output + * + * @param commands The commands to run + * @return Output of the commands, or null if root isn't available or in case of an error + */ + public static List run(String[] commands) { + return Shell.run("su", commands, null, false); + } + + /** + * Detects whether or not superuser access is available, by checking the output + * of the "id" command if available, checking if a shell runs at all otherwise + * + * @return True if superuser access available + */ + public static boolean available() { + // this is only one of many ways this can be done + + List ret = run(Shell.availableTestCommands); + return Shell.parseAvailableResult(ret, true); + } + + /** + *

Detects the version of the su binary installed (if any), if supported by the binary. + * Most binaries support two different version numbers, the public version that is + * displayed to users, and an internal version number that is used for version number + * comparisons. Returns null if su not available or retrieving the version isn't supported.

+ * + *

Note that su binary version and GUI (APK) version can be completely different.

+ * + * @param internal Request human-readable version or application internal version + * @return String containing the su version or null + */ + public static String version(boolean internal) { + List ret = Shell.run( + internal ? "su -V" : "su -v", + new String[] { }, + null, + false + ); + if (ret == null) return null; + + for (String line : ret) { + if (!internal) { + if (line.contains(".")) return line; + } else { + try { + if (Integer.parseInt(line) > 0) return line; + } catch(NumberFormatException e) { + } + } + } + return null; + } + + /** + * Attempts to deduce if the shell command refers to a su shell + * + * @param shell Shell command to run + * @return Shell command appears to be su + */ + public static boolean isSU(String shell) { + // Strip parameters + int pos = shell.indexOf(' '); + if (pos >= 0) { + shell = shell.substring(0, pos); + } + + // Strip path + pos = shell.lastIndexOf('/'); + if (pos >= 0) { + shell = shell.substring(pos + 1); + } + + return shell.equals("su"); + } + /** * Constructs a shell command to start a su shell using the supplied * uid and SELinux context. This is can be an expensive operation, @@ -351,11 +351,11 @@ public static boolean isSU(String shell) { public static String shell(int uid, String context) { // su[ --context ][ ] String shell = "su"; - + // First known firmware with SELinux built-in was a 4.2 (17) leak if ((context != null) && (android.os.Build.VERSION.SDK_INT >= 17)) { Boolean enforcing = null; - + // Detect enforcing through sysfs, not always present if (enforcing == null) { File f = new File("/sys/fs/selinux/enforce"); @@ -371,7 +371,7 @@ public static String shell(int uid, String context) { } } } - + // 4.4+ builds are enforcing by default, take the gamble if (enforcing == null) { enforcing = (android.os.Build.VERSION.SDK_INT >= 19); @@ -383,960 +383,960 @@ public static String shell(int uid, String context) { if (enforcing) { String display = version(false); String internal = version(true); - + // We only know the format for SuperSU v1.90+ right now if ( (display != null) && (internal != null) && (display.endsWith("SUPERSU")) && (Integer.valueOf(internal) >= 190) - ) { + ) { shell = String.format(Locale.ENGLISH, "%s --context %s", shell, context); } } } - + // Most su binaries support the "su " format, but in case // they don't, lets skip it for the default 0 (root) case if (uid > 0) { shell = String.format(Locale.ENGLISH, "%s %d", shell, uid); } - + return shell; - } - } - - /** - * Command result callback, notifies the recipient of the completion of a command - * block, including the (last) exit code, and the full output - */ - public interface OnCommandResultListener { - /** - *

Command result callback

- * - *

Depending on how and on which thread the shell was created, this callback - * may be executed on one of the gobbler threads. In that case, it is important - * the callback returns as quickly as possible, as delays in this callback may - * pause the native process or even result in a deadlock

- * - *

See {@link Shell.Interactive} for threading details

- * - * @param commandCode Value previously supplied to addCommand - * @param exitCode Exit code of the last command in the block - * @param output All output generated by the command block - */ - public void onCommandResult(int commandCode, int exitCode, List output); - - // for any onCommandResult callback - public static final int WATCHDOG_EXIT = -1; - public static final int SHELL_DIED = -2; - - // for Interactive.open() callbacks only - public static final int SHELL_EXEC_FAILED = -3; - public static final int SHELL_WRONG_UID = -4; - public static final int SHELL_RUNNING = 0; - } - - /** - * Internal class to store command block properties - */ - private static class Command { - private static int commandCounter = 0; - - private final String[] commands; - private final int code; - private final OnCommandResultListener onCommandResultListener; - private final String marker; - - public Command(String[] commands, int code, OnCommandResultListener onCommandResultListener) { - this.commands = commands; - this.code = code; - this.onCommandResultListener = onCommandResultListener; - this.marker = UUID.randomUUID().toString() + String.format("-%08x", ++commandCounter); - } - } - - /** - * Builder class for {@link Shell.Interactive} - */ - public static class Builder { - private Handler handler = null; - private boolean autoHandler = true; - private String shell = "sh"; - private boolean wantSTDERR = false; - private List commands = new LinkedList(); - private Map environment = new HashMap(); - private OnLineListener onSTDOUTLineListener = null; - private OnLineListener onSTDERRLineListener = null; - private int watchdogTimeout = 0; - - /** - *

Set a custom handler that will be used to post all callbacks to

- * - *

See {@link Shell.Interactive} for further details on threading and handlers

- * - * @param handler Handler to use - * @return This Builder object for method chaining - */ - public Builder setHandler(Handler handler) { this.handler = handler; return this; } - - /** - *

Automatically create a handler if possible ? Default to true

- * - *

See {@link Shell.Interactive} for further details on threading and handlers

- * - * @param autoHandler Auto-create handler ? - * @return This Builder object for method chaining - */ - public Builder setAutoHandler(boolean autoHandler) { this.autoHandler = autoHandler; return this; } - - /** - * Set shell binary to use. Usually "sh" or "su", do not use a full path - * unless you have a good reason to - * - * @param shell Shell to use - * @return This Builder object for method chaining - */ - public Builder setShell(String shell) { this.shell = shell; return this; } - - /** - * Convenience function to set "sh" as used shell - * - * @return This Builder object for method chaining - */ - public Builder useSH() { return setShell("sh"); } - - /** - * Convenience function to set "su" as used shell - * - * @return This Builder object for method chaining - */ - public Builder useSU() { return setShell("su"); } - - /** - * Set if error output should be appended to command block result output - * - * @param wantSTDERR Want error output ? - * @return This Builder object for method chaining - */ - public Builder setWantSTDERR(boolean wantSTDERR) { this.wantSTDERR = wantSTDERR; return this; } - - /** - * Add or update an environment variable - * - * @param key Key of the environment variable - * @param value Value of the environment variable - * @return This Builder object for method chaining - */ - public Builder addEnvironment(String key, String value) { environment.put(key, value); return this; } - - /** - * Add or update environment variables - * - * @param addEnvironment Map of environment variables - * @return This Builder object for method chaining - */ - public Builder addEnvironment(Map addEnvironment) { environment.putAll(addEnvironment); return this; } - - /** - * Add a command to execute - * - * @param command Command to execute - * @return This Builder object for method chaining - */ - public Builder addCommand(String command) { return addCommand(command, 0, null); } - - /** - *

Add a command to execute, with a callback to be called on completion

- * - *

The thread on which the callback executes is dependent on various factors, see {@link Shell.Interactive} for further details

- * - * @param command Command to execute - * @param code User-defined value passed back to the callback - * @param onCommandResultListener Callback to be called on completion - * @return This Builder object for method chaining - */ - public Builder addCommand(String command, int code, OnCommandResultListener onCommandResultListener) { return addCommand(new String[] { command }, code, onCommandResultListener); } - - /** - * Add commands to execute - * - * @param commands Commands to execute - * @return This Builder object for method chaining - */ - public Builder addCommand(List commands) { return addCommand(commands, 0, null); } - - /** - *

Add commands to execute, with a callback to be called on completion (of all commands)

- * - *

The thread on which the callback executes is dependent on various factors, see {@link Shell.Interactive} for further details

- * - * @param commands Commands to execute - * @param code User-defined value passed back to the callback - * @param onCommandResultListener Callback to be called on completion (of all commands) - * @return This Builder object for method chaining - */ - public Builder addCommand(List commands, int code, OnCommandResultListener onCommandResultListener) { return addCommand(commands.toArray(new String[commands.size()]), code, onCommandResultListener); } - - /** - * Add commands to execute - * - * @param commands Commands to execute - * @return This Builder object for method chaining - */ - public Builder addCommand(String[] commands) { return addCommand(commands, 0, null); } - - /** - *

Add commands to execute, with a callback to be called on completion (of all commands)

- * - *

The thread on which the callback executes is dependent on various factors, see {@link Shell.Interactive} for further details

- * - * @param commands Commands to execute - * @param code User-defined value passed back to the callback - * @param onCommandResultListener Callback to be called on completion (of all commands) - * @return This Builder object for method chaining - */ - public Builder addCommand(String[] commands, int code, OnCommandResultListener onCommandResultListener) { this.commands.add(new Command(commands, code, onCommandResultListener)); return this; } - - /** - *

Set a callback called for every line output to STDOUT by the shell

- * - *

The thread on which the callback executes is dependent on various factors, see {@link Shell.Interactive} for further details

- * - * @param onLineListener Callback to be called for each line - * @return This Builder object for method chaining - */ - public Builder setOnSTDOUTLineListener(OnLineListener onLineListener) { this.onSTDOUTLineListener = onLineListener; return this; } - - /** - *

Set a callback called for every line output to STDERR by the shell

- * - *

The thread on which the callback executes is dependent on various factors, see {@link Shell.Interactive} for further details

- * - * @param onLineListener Callback to be called for each line - * @return This Builder object for method chaining - */ - public Builder setOnSTDERRLineListener(OnLineListener onLineListener) { this.onSTDERRLineListener = onLineListener; return this; } - - /** - *

Enable command timeout callback

- * - *

This will invoke the onCommandResult() callback with exitCode WATCHDOG_EXIT if a command takes longer than watchdogTimeout - * seconds to complete.

- * - *

If a watchdog timeout occurs, it generally means that the Interactive session is out of sync with the shell process. The - * caller should close the current session and open a new one.

- * - * @param watchdogTimeout Timeout, in seconds; 0 to disable - * @return This Builder object for method chaining - */ - public Builder setWatchdogTimeout(int watchdogTimeout) { this.watchdogTimeout = watchdogTimeout; return this; } - - /** - *

Enable/disable reduced logcat output

- * - *

Note that this is a global setting

- * - * @param useMinimal true for reduced output, false for full output - * @return This Builder object for method chaining - */ - public Builder setMinimalLogging(boolean useMinimal) { - Debug.setLogTypeEnabled(Debug.LOG_COMMAND | Debug.LOG_OUTPUT, !useMinimal); - return this; - } - - /** - * Construct a {@link Shell.Interactive} instance, and start the shell - */ - public Interactive open() { return new Interactive(this, null); } - - /** - * Construct a {@link Shell.Interactive} instance, try to start the shell, and - * call onCommandResultListener to report success or failure - * - * @param onCommandResultListener Callback to return shell open status - */ - public Interactive open(OnCommandResultListener onCommandResultListener) { - return new Interactive(this, onCommandResultListener); - } - } - - /** - *

An interactive shell - initially created with {@link Shell.Builder} - that - * executes blocks of commands you supply in the background, optionally calling - * callbacks as each block completes.

- * - *

STDERR output can be supplied as well, but due to compatibility with older - * Android versions, wantSTDERR is not implemented using redirectErrorStream, - * but rather appended to the output. STDOUT and STDERR are thus not guaranteed to - * be in the correct order in the output.

- * - *

Note as well that the close() and waitForIdle() methods will intentionally - * crash when run in debug mode from the main thread of the application. Any blocking - * call should be run from a background thread.

- * - *

When in debug mode, the code will also excessively log the commands passed to - * and the output returned from the shell.

- * - *

Though this function uses background threads to gobble STDOUT and STDERR so - * a deadlock does not occur if the shell produces massive output, the output is - * still stored in a List<String>, and as such doing something like 'ls -lR /' - * will probably have you run out of memory when using a - * {@link Shell.OnCommandResultListener}. A work-around is to not supply this callback, - * but using (only) {@link Shell.Builder#setOnSTDOUTLineListener(OnLineListener)}. This - * way, an internal buffer will not be created and wasting your memory.

- * - *

Callbacks, threads and handlers

- * - *

On which thread the callbacks execute is dependent on your initialization. You can - * supply a custom Handler using {@link Shell.Builder#setHandler(Handler)} if needed. - * If you do not supply a custom Handler - unless you set {@link Shell.Builder#setAutoHandler(boolean)} - * to false - a Handler will be auto-created if the thread used for instantiation - * of the object has a Looper.

- * - *

If no Handler was supplied and it was also not auto-created, all callbacks will - * be called from either the STDOUT or STDERR gobbler threads. These are important - * threads that should be blocked as little as possible, as blocking them may in rare - * cases pause the native process or even create a deadlock.

- * - *

The main thread must certainly have a Looper, thus if you call {@link Shell.Builder#open()} - * from the main thread, a handler will (by default) be auto-created, and all the callbacks - * will be called on the main thread. While this is often convenient and easy to code with, - * you should be aware that if your callbacks are 'expensive' to execute, this may negatively - * impact UI performance.

- * - *

Background threads usually do not have a Looper, so calling {@link Shell.Builder#open()} - * from such a background thread will (by default) result in all the callbacks being executed - * in one of the gobbler threads. You will have to make sure the code you execute in these callbacks - * is thread-safe.

- */ - public static class Interactive { - private final Handler handler; - private final boolean autoHandler; - private final String shell; - private final boolean wantSTDERR; - private final List commands; - private final Map environment; - private final OnLineListener onSTDOUTLineListener; - private final OnLineListener onSTDERRLineListener; - private int watchdogTimeout; - - private Process process = null; - private DataOutputStream STDIN = null; - private StreamGobbler STDOUT = null; - private StreamGobbler STDERR = null; - private ScheduledThreadPoolExecutor watchdog = null; - - private volatile boolean running = false; - private volatile boolean idle = true; // read/write only synchronized - private volatile boolean closed = true; - private volatile int callbacks = 0; - private volatile int watchdogCount; - - private Object idleSync = new Object(); - private Object callbackSync = new Object(); - - private volatile int lastExitCode = 0; - private volatile String lastMarkerSTDOUT = null; - private volatile String lastMarkerSTDERR = null; - private volatile Command command = null; - private volatile List buffer = null; - - /** - * The only way to create an instance: Shell.Builder::open() - * - * @param builder Builder class to take values from - */ - private Interactive(final Builder builder, final OnCommandResultListener onCommandResultListener) { - autoHandler = builder.autoHandler; - shell = builder.shell; - wantSTDERR = builder.wantSTDERR; - commands = builder.commands; - environment = builder.environment; - onSTDOUTLineListener = builder.onSTDOUTLineListener; - onSTDERRLineListener = builder.onSTDERRLineListener; - watchdogTimeout = builder.watchdogTimeout; - - // If a looper is available, we offload the callbacks from the gobbling threads - // to whichever thread created us. Would normally do this in open(), - // but then we could not declare handler as final - if ((Looper.myLooper() != null) && (builder.handler == null) && autoHandler) { - handler = new Handler(); - } else { - handler = builder.handler; - } - - boolean ret = open(); - if (onCommandResultListener == null) { - return; - } else if (ret == false) { - onCommandResultListener.onCommandResult(0, OnCommandResultListener.SHELL_EXEC_FAILED, null); - return; - } - - // Allow up to 60 seconds for SuperSU/Superuser dialog, then enable the user-specified - // timeout for all subsequent operations - watchdogTimeout = 60; - addCommand(Shell.availableTestCommands, 0, new OnCommandResultListener() { - public void onCommandResult(int commandCode, int exitCode, List output) { - if (exitCode == OnCommandResultListener.SHELL_RUNNING && - Shell.parseAvailableResult(output, Shell.SU.isSU(shell)) != true) { - // shell is up, but it's brain-damaged - exitCode = OnCommandResultListener.SHELL_WRONG_UID; - } - watchdogTimeout = builder.watchdogTimeout; - onCommandResultListener.onCommandResult(0, exitCode, output); - } - }); - } - - @Override - protected void finalize() throws Throwable { - if (!closed && Debug.getSanityChecksEnabledEffective()) { - // waste of resources - Debug.log(ShellNotClosedException.EXCEPTION_NOT_CLOSED); - throw new ShellNotClosedException(); - } - super.finalize(); - } - - /** - * Add a command to execute - * - * @param command Command to execute - */ - public void addCommand(String command) { addCommand(command, 0, null); } - - /** - *

Add a command to execute, with a callback to be called on completion

- * - *

The thread on which the callback executes is dependent on various factors, see {@link Shell.Interactive} for further details

- * - * @param command Command to execute - * @param code User-defined value passed back to the callback - * @param onCommandResultListener Callback to be called on completion - */ - public void addCommand(String command, int code, OnCommandResultListener onCommandResultListener) { addCommand(new String[] { command }, code, onCommandResultListener); } - - /** - * Add commands to execute - * - * @param commands Commands to execute - */ - public void addCommand(List commands) { addCommand(commands, 0, null); } - - /** - *

Add commands to execute, with a callback to be called on completion (of all commands)

- * - *

The thread on which the callback executes is dependent on various factors, see {@link Shell.Interactive} for further details

- * - * @param commands Commands to execute - * @param code User-defined value passed back to the callback - * @param onCommandResultListener Callback to be called on completion (of all commands) - */ - public void addCommand(List commands, int code, OnCommandResultListener onCommandResultListener) { addCommand(commands.toArray(new String[commands.size()]), code, onCommandResultListener); } - - /** - * Add commands to execute - * - * @param commands Commands to execute - */ - public void addCommand(String[] commands) { addCommand(commands, 0, null); } - - /** - *

Add commands to execute, with a callback to be called on completion (of all commands)

- * - *

The thread on which the callback executes is dependent on various factors, see {@link Shell.Interactive} for further details

- * - * @param commands Commands to execute - * @param code User-defined value passed back to the callback - * @param onCommandResultListener Callback to be called on completion (of all commands) - */ - public synchronized void addCommand(String[] commands, int code, OnCommandResultListener onCommandResultListener) { - this.commands.add(new Command(commands, code, onCommandResultListener)); - runNextCommand(); - } - - /** - * Run the next command if any and if ready, signals idle state if no commands left - */ - private void runNextCommand() { - runNextCommand(true); - } - - /** - * Called from a ScheduledThreadPoolExecutor timer thread every second when there is an outstanding command - */ - private synchronized void handleWatchdog() { - final int exitCode; - - if (watchdog == null) return; - if (watchdogTimeout == 0) return; - - if (!isRunning()) { - exitCode = OnCommandResultListener.SHELL_DIED; - Debug.log(String.format("[%s%%] SHELL_DIED", shell.toUpperCase(Locale.ENGLISH))); - } else if (watchdogCount++ < watchdogTimeout) { - return; - } else { - exitCode = OnCommandResultListener.WATCHDOG_EXIT; - Debug.log(String.format("[%s%%] WATCHDOG_EXIT", shell.toUpperCase(Locale.ENGLISH))); - } - - if (handler != null) { - postCallback(command, exitCode, buffer); - } - - // prevent multiple callbacks for the same command - command = null; - buffer = null; - idle = true; - - watchdog.shutdown(); - watchdog = null; - kill(); - } - - /** - * Start the periodic timer when a command is submitted - */ - private void startWatchdog() { - if (watchdogTimeout == 0) { - return; - } - watchdogCount = 0; - watchdog = new ScheduledThreadPoolExecutor(1); - watchdog.scheduleAtFixedRate(new Runnable() { - @Override - public void run() { - handleWatchdog(); - } - }, 1, 1, TimeUnit.SECONDS); - } - - /** - * Disable the watchdog timer upon command completion - */ - private void stopWatchdog() { - if (watchdog != null) { - watchdog.shutdownNow(); - watchdog = null; - } - } - - /** - * Run the next command if any and if ready - * - * @param notifyIdle signals idle state if no commands left ? - */ - private void runNextCommand(boolean notifyIdle) { - // must always be called from a synchronized method - - boolean running = isRunning(); - if (!running) idle = true; - - if (running && idle && (commands.size() > 0)) { - Command command = commands.get(0); - commands.remove(0); - - buffer = null; - lastExitCode = 0; - lastMarkerSTDOUT = null; - lastMarkerSTDERR = null; - - if (command.commands.length > 0) { - try { - if (command.onCommandResultListener != null) { - // no reason to store the output if we don't have an OnCommandResultListener - // user should catch the output with an OnLineListener in this case - buffer = Collections.synchronizedList(new ArrayList()); - } - - idle = false; - this.command = command; - startWatchdog(); - for (String write : command.commands) { - Debug.logCommand(String.format("[%s+] %s", shell.toUpperCase(Locale.ENGLISH), write)); - STDIN.write((write + "\n").getBytes("UTF-8")); - } - STDIN.write(("echo " + command.marker + " $?\n").getBytes("UTF-8")); - STDIN.write(("echo " + command.marker + " >&2\n").getBytes("UTF-8")); - STDIN.flush(); - } catch (IOException e) { - } - } else { - runNextCommand(false); - } - } else if (!running) { - // our shell died for unknown reasons - abort all submissions - while (commands.size() > 0) { - postCallback(commands.remove(0), OnCommandResultListener.SHELL_DIED, null); - } - } - - if (idle && notifyIdle) { - synchronized(idleSync) { - idleSync.notifyAll(); - } - } - } - - /** - * Processes a STDOUT/STDERR line containing an end/exitCode marker - */ - private synchronized void processMarker() { - if (command.marker.equals(lastMarkerSTDOUT) && (command.marker.equals(lastMarkerSTDERR))) { - if (buffer != null) { - postCallback(command, lastExitCode, buffer); - } - - stopWatchdog(); - command = null; - buffer = null; - idle = true; - runNextCommand(); - } - } - - /** - * Process a normal STDOUT/STDERR line - * - * @param line Line to process - * @param listener Callback to call or null - */ - private synchronized void processLine(String line, OnLineListener listener) { - if (listener != null) { - if (handler != null) { - final String fLine = line; - final OnLineListener fListener = listener; - - startCallback(); - handler.post(new Runnable() { - @Override - public void run() { - try { - fListener.onLine(fLine); - } finally { - endCallback(); - } - } - }); - } else { - listener.onLine(line); - } - } - } - - /** - * Add line to internal buffer - * - * @param line Line to add - */ - private synchronized void addBuffer(String line) { - if (buffer != null) { - buffer.add(line); - } - } - - /** - * Increase callback counter - */ - private void startCallback() { - synchronized (callbackSync) { - callbacks++; - } - } - - /** - * Schedule a callback to run on the appropriate thread - */ - private void postCallback(final Command fCommand, final int fExitCode, final List fOutput) { - if (fCommand.onCommandResultListener == null) { - return; - } - if (handler == null) { - fCommand.onCommandResultListener.onCommandResult(fCommand.code, fExitCode, fOutput); - return; - } - startCallback(); - handler.post(new Runnable() { - @Override - public void run() { - try { - fCommand.onCommandResultListener.onCommandResult(fCommand.code, fExitCode, fOutput); - } finally { - endCallback(); - } - } - }); - } - - /** - * Decrease callback counter, signals callback complete state when dropped to 0 - */ - private void endCallback() { - synchronized (callbackSync) { - callbacks--; - if (callbacks == 0) { - callbackSync.notifyAll(); - } - } - } - - /** - * Internal call that launches the shell, starts gobbling, and starts executing commands. - * See {@link Shell.Interactive} - * - * @return Opened successfully ? - */ - private synchronized boolean open() { - Debug.log(String.format("[%s%%] START", shell.toUpperCase(Locale.ENGLISH))); - - try { - // setup our process, retrieve STDIN stream, and STDOUT/STDERR gobblers - if (environment.size() == 0) { - process = Runtime.getRuntime().exec(shell); - } else { - Map newEnvironment = new HashMap(); - newEnvironment.putAll(System.getenv()); - newEnvironment.putAll(environment); - int i = 0; - String[] env = new String[newEnvironment.size()]; - for (Map.Entry entry : newEnvironment.entrySet()) { - env[i] = entry.getKey() + "=" + entry.getValue(); - i++; - } - process = Runtime.getRuntime().exec(shell, env); - } - - STDIN = new DataOutputStream(process.getOutputStream()); - STDOUT = new StreamGobbler(shell.toUpperCase(Locale.ENGLISH) + "-", process.getInputStream(), new OnLineListener() { - @Override - public void onLine(String line) { - synchronized (Interactive.this) { - if (command == null) { - return; - } - if (line.startsWith(command.marker)) { - try { - lastExitCode = Integer.valueOf(line.substring(command.marker.length() + 1), 10); - } catch (Exception e) { - } - lastMarkerSTDOUT = command.marker; - processMarker(); - } else { - addBuffer(line); - processLine(line, onSTDOUTLineListener); - } - } - } - }); - STDERR = new StreamGobbler(shell.toUpperCase(Locale.ENGLISH) + "*", process.getErrorStream(), new OnLineListener() { - @Override - public void onLine(String line) { - synchronized (Interactive.this) { - if (command == null) { - return; - } - if (line.startsWith(command.marker)) { - lastMarkerSTDERR = command.marker; - processMarker(); - } else { - if (wantSTDERR) addBuffer(line); - processLine(line, onSTDERRLineListener); - } - } - } - }); - - // start gobbling and write our commands to the shell - STDOUT.start(); - STDERR.start(); - - running = true; - closed = false; - - runNextCommand(); - - return true; - } catch (IOException e) { - // shell probably not found - return false; - } - } - - /** - * Close shell and clean up all resources. Call this when you are done with the shell. - * If the shell is not idle (all commands completed) you should not call this method - * from the main UI thread because it may block for a long time. This method will - * intentionally crash your app (if in debug mode) if you try to do this anyway. - */ - public void close() { - boolean _idle = isIdle(); // idle must be checked synchronized - - synchronized (this) { - if (!running) return; - running = false; - closed = true; - } - - // This method should not be called from the main thread unless the shell is idle - // and can be cleaned up with (minimal) waiting. Only throw in debug mode. - if (!_idle && Debug.getSanityChecksEnabledEffective() && Debug.onMainThread()) { - Debug.log(ShellOnMainThreadException.EXCEPTION_NOT_IDLE); - throw new ShellOnMainThreadException(ShellOnMainThreadException.EXCEPTION_NOT_IDLE); - } - - if (!_idle) waitForIdle(); - - try { - STDIN.write(("exit\n").getBytes("UTF-8")); - STDIN.flush(); - - // wait for our process to finish, while we gobble away in the background - process.waitFor(); - - // make sure our threads are done gobbling, our streams are closed, and the process is - // destroyed - while the latter two shouldn't be needed in theory, and may even produce - // warnings, in "normal" Java they are required for guaranteed cleanup of resources, so - // lets be safe and do this on Android as well - try { - STDIN.close(); - } catch (IOException e) { - } - STDOUT.join(); - STDERR.join(); - stopWatchdog(); - process.destroy(); - } catch (IOException e) { - // shell probably not found - } catch (InterruptedException e) { - // this should really be re-thrown - } - - Debug.log(String.format("[%s%%] END", shell.toUpperCase(Locale.ENGLISH))); - } - - /** - * Try to clean up as much as possible from a shell that's gotten itself wedged. - * Hopefully the StreamGobblers will croak on their own when the other side of - * the pipe is closed. - */ - public synchronized void kill() { - running = false; - closed = true; - - try { - STDIN.close(); - } catch (IOException e) { - } - try { - process.destroy(); - } catch (Exception e) { - } - } - - /** - * Is our shell still running ? - * - * @return Shell running ? - */ - public boolean isRunning() { - try { - // if this throws, we're still running - process.exitValue(); - return false; - } catch (IllegalThreadStateException e) { - } - return true; - } - - /** - * Have all commands completed executing ? - * - * @return Shell idle ? - */ - public synchronized boolean isIdle() { - if (!isRunning()) { - idle = true; - synchronized(idleSync) { - idleSync.notifyAll(); - } - } - return idle; - } - - /** - *

Wait for idle state. As this is a blocking call, you should not call it from the main UI thread. - * If you do so and debug mode is enabled, this method will intentionally crash your app.

- * - *

If not interrupted, this method will not return until all commands have finished executing. - * Note that this does not necessarily mean that all the callbacks have fired yet.

- * - *

If no Handler is used, all callbacks will have been executed when this method returns. If - * a Handler is used, and this method is called from a different thread than associated with the - * Handler's Looper, all callbacks will have been executed when this method returns as well. - * If however a Handler is used but this method is called from the same thread as associated - * with the Handler's Looper, there is no way to know.

- * - *

In practice this means that in most simple cases all callbacks will have completed when this - * method returns, but if you actually depend on this behavior, you should make certain this is - * indeed the case.

- * - *

See {@link Shell.Interactive} for further details on threading and handlers

- * - * @return True if wait complete, false if wait interrupted - */ - public boolean waitForIdle() { - if (Debug.getSanityChecksEnabledEffective() && Debug.onMainThread()) { - Debug.log(ShellOnMainThreadException.EXCEPTION_WAIT_IDLE); - throw new ShellOnMainThreadException(ShellOnMainThreadException.EXCEPTION_WAIT_IDLE); - } - - if (isRunning()) { - synchronized (idleSync) { - while (!idle) { - try { - idleSync.wait(); - } catch (InterruptedException e) { - return false; - } - } - } - - if ( - (handler != null) && - (handler.getLooper() != null) && - (handler.getLooper() != Looper.myLooper()) - ) { - // If the callbacks are posted to a different thread than this one, we can wait until - // all callbacks have called before returning. If we don't use a Handler at all, - // the callbacks are already called before we get here. If we do use a Handler but - // we use the same Looper, waiting here would actually block the callbacks from being - // called - - synchronized (callbackSync) { - while (callbacks > 0) { - try { - callbackSync.wait(); - } catch (InterruptedException e) { - return false; - } - } - } - } - } - - return true; - } - - /** - * Are we using a Handler to post callbacks ? - * - * @return Handler used ? - */ - public boolean hasHandler() { - return (handler != null); - } - } + } + } + + /** + * Command result callback, notifies the recipient of the completion of a command + * block, including the (last) exit code, and the full output + */ + public interface OnCommandResultListener { + /** + *

Command result callback

+ * + *

Depending on how and on which thread the shell was created, this callback + * may be executed on one of the gobbler threads. In that case, it is important + * the callback returns as quickly as possible, as delays in this callback may + * pause the native process or even result in a deadlock

+ * + *

See {@link Shell.Interactive} for threading details

+ * + * @param commandCode Value previously supplied to addCommand + * @param exitCode Exit code of the last command in the block + * @param output All output generated by the command block + */ + public void onCommandResult(int commandCode, int exitCode, List output); + + // for any onCommandResult callback + public static final int WATCHDOG_EXIT = -1; + public static final int SHELL_DIED = -2; + + // for Interactive.open() callbacks only + public static final int SHELL_EXEC_FAILED = -3; + public static final int SHELL_WRONG_UID = -4; + public static final int SHELL_RUNNING = 0; + } + + /** + * Internal class to store command block properties + */ + private static class Command { + private static int commandCounter = 0; + + private final String[] commands; + private final int code; + private final OnCommandResultListener onCommandResultListener; + private final String marker; + + public Command(String[] commands, int code, OnCommandResultListener onCommandResultListener) { + this.commands = commands; + this.code = code; + this.onCommandResultListener = onCommandResultListener; + this.marker = UUID.randomUUID().toString() + String.format("-%08x", ++commandCounter); + } + } + + /** + * Builder class for {@link Shell.Interactive} + */ + public static class Builder { + private Handler handler = null; + private boolean autoHandler = true; + private String shell = "sh"; + private boolean wantSTDERR = false; + private List commands = new LinkedList(); + private Map environment = new HashMap(); + private OnLineListener onSTDOUTLineListener = null; + private OnLineListener onSTDERRLineListener = null; + private int watchdogTimeout = 0; + + /** + *

Set a custom handler that will be used to post all callbacks to

+ * + *

See {@link Shell.Interactive} for further details on threading and handlers

+ * + * @param handler Handler to use + * @return This Builder object for method chaining + */ + public Builder setHandler(Handler handler) { this.handler = handler; return this; } + + /** + *

Automatically create a handler if possible ? Default to true

+ * + *

See {@link Shell.Interactive} for further details on threading and handlers

+ * + * @param autoHandler Auto-create handler ? + * @return This Builder object for method chaining + */ + public Builder setAutoHandler(boolean autoHandler) { this.autoHandler = autoHandler; return this; } + + /** + * Set shell binary to use. Usually "sh" or "su", do not use a full path + * unless you have a good reason to + * + * @param shell Shell to use + * @return This Builder object for method chaining + */ + public Builder setShell(String shell) { this.shell = shell; return this; } + + /** + * Convenience function to set "sh" as used shell + * + * @return This Builder object for method chaining + */ + public Builder useSH() { return setShell("sh"); } + + /** + * Convenience function to set "su" as used shell + * + * @return This Builder object for method chaining + */ + public Builder useSU() { return setShell("su"); } + + /** + * Set if error output should be appended to command block result output + * + * @param wantSTDERR Want error output ? + * @return This Builder object for method chaining + */ + public Builder setWantSTDERR(boolean wantSTDERR) { this.wantSTDERR = wantSTDERR; return this; } + + /** + * Add or update an environment variable + * + * @param key Key of the environment variable + * @param value Value of the environment variable + * @return This Builder object for method chaining + */ + public Builder addEnvironment(String key, String value) { environment.put(key, value); return this; } + + /** + * Add or update environment variables + * + * @param addEnvironment Map of environment variables + * @return This Builder object for method chaining + */ + public Builder addEnvironment(Map addEnvironment) { environment.putAll(addEnvironment); return this; } + + /** + * Add a command to execute + * + * @param command Command to execute + * @return This Builder object for method chaining + */ + public Builder addCommand(String command) { return addCommand(command, 0, null); } + + /** + *

Add a command to execute, with a callback to be called on completion

+ * + *

The thread on which the callback executes is dependent on various factors, see {@link Shell.Interactive} for further details

+ * + * @param command Command to execute + * @param code User-defined value passed back to the callback + * @param onCommandResultListener Callback to be called on completion + * @return This Builder object for method chaining + */ + public Builder addCommand(String command, int code, OnCommandResultListener onCommandResultListener) { return addCommand(new String[] { command }, code, onCommandResultListener); } + + /** + * Add commands to execute + * + * @param commands Commands to execute + * @return This Builder object for method chaining + */ + public Builder addCommand(List commands) { return addCommand(commands, 0, null); } + + /** + *

Add commands to execute, with a callback to be called on completion (of all commands)

+ * + *

The thread on which the callback executes is dependent on various factors, see {@link Shell.Interactive} for further details

+ * + * @param commands Commands to execute + * @param code User-defined value passed back to the callback + * @param onCommandResultListener Callback to be called on completion (of all commands) + * @return This Builder object for method chaining + */ + public Builder addCommand(List commands, int code, OnCommandResultListener onCommandResultListener) { return addCommand(commands.toArray(new String[commands.size()]), code, onCommandResultListener); } + + /** + * Add commands to execute + * + * @param commands Commands to execute + * @return This Builder object for method chaining + */ + public Builder addCommand(String[] commands) { return addCommand(commands, 0, null); } + + /** + *

Add commands to execute, with a callback to be called on completion (of all commands)

+ * + *

The thread on which the callback executes is dependent on various factors, see {@link Shell.Interactive} for further details

+ * + * @param commands Commands to execute + * @param code User-defined value passed back to the callback + * @param onCommandResultListener Callback to be called on completion (of all commands) + * @return This Builder object for method chaining + */ + public Builder addCommand(String[] commands, int code, OnCommandResultListener onCommandResultListener) { this.commands.add(new Command(commands, code, onCommandResultListener)); return this; } + + /** + *

Set a callback called for every line output to STDOUT by the shell

+ * + *

The thread on which the callback executes is dependent on various factors, see {@link Shell.Interactive} for further details

+ * + * @param onLineListener Callback to be called for each line + * @return This Builder object for method chaining + */ + public Builder setOnSTDOUTLineListener(OnLineListener onLineListener) { this.onSTDOUTLineListener = onLineListener; return this; } + + /** + *

Set a callback called for every line output to STDERR by the shell

+ * + *

The thread on which the callback executes is dependent on various factors, see {@link Shell.Interactive} for further details

+ * + * @param onLineListener Callback to be called for each line + * @return This Builder object for method chaining + */ + public Builder setOnSTDERRLineListener(OnLineListener onLineListener) { this.onSTDERRLineListener = onLineListener; return this; } + + /** + *

Enable command timeout callback

+ * + *

This will invoke the onCommandResult() callback with exitCode WATCHDOG_EXIT if a command takes longer than watchdogTimeout + * seconds to complete.

+ * + *

If a watchdog timeout occurs, it generally means that the Interactive session is out of sync with the shell process. The + * caller should close the current session and open a new one.

+ * + * @param watchdogTimeout Timeout, in seconds; 0 to disable + * @return This Builder object for method chaining + */ + public Builder setWatchdogTimeout(int watchdogTimeout) { this.watchdogTimeout = watchdogTimeout; return this; } + + /** + *

Enable/disable reduced logcat output

+ * + *

Note that this is a global setting

+ * + * @param useMinimal true for reduced output, false for full output + * @return This Builder object for method chaining + */ + public Builder setMinimalLogging(boolean useMinimal) { + Debug.setLogTypeEnabled(Debug.LOG_COMMAND | Debug.LOG_OUTPUT, !useMinimal); + return this; + } + + /** + * Construct a {@link Shell.Interactive} instance, and start the shell + */ + public Interactive open() { return new Interactive(this, null); } + + /** + * Construct a {@link Shell.Interactive} instance, try to start the shell, and + * call onCommandResultListener to report success or failure + * + * @param onCommandResultListener Callback to return shell open status + */ + public Interactive open(OnCommandResultListener onCommandResultListener) { + return new Interactive(this, onCommandResultListener); + } + } + + /** + *

An interactive shell - initially created with {@link Shell.Builder} - that + * executes blocks of commands you supply in the background, optionally calling + * callbacks as each block completes.

+ * + *

STDERR output can be supplied as well, but due to compatibility with older + * Android versions, wantSTDERR is not implemented using redirectErrorStream, + * but rather appended to the output. STDOUT and STDERR are thus not guaranteed to + * be in the correct order in the output.

+ * + *

Note as well that the close() and waitForIdle() methods will intentionally + * crash when run in debug mode from the main thread of the application. Any blocking + * call should be run from a background thread.

+ * + *

When in debug mode, the code will also excessively log the commands passed to + * and the output returned from the shell.

+ * + *

Though this function uses background threads to gobble STDOUT and STDERR so + * a deadlock does not occur if the shell produces massive output, the output is + * still stored in a List<String>, and as such doing something like 'ls -lR /' + * will probably have you run out of memory when using a + * {@link Shell.OnCommandResultListener}. A work-around is to not supply this callback, + * but using (only) {@link Shell.Builder#setOnSTDOUTLineListener(OnLineListener)}. This + * way, an internal buffer will not be created and wasting your memory.

+ * + *

Callbacks, threads and handlers

+ * + *

On which thread the callbacks execute is dependent on your initialization. You can + * supply a custom Handler using {@link Shell.Builder#setHandler(Handler)} if needed. + * If you do not supply a custom Handler - unless you set {@link Shell.Builder#setAutoHandler(boolean)} + * to false - a Handler will be auto-created if the thread used for instantiation + * of the object has a Looper.

+ * + *

If no Handler was supplied and it was also not auto-created, all callbacks will + * be called from either the STDOUT or STDERR gobbler threads. These are important + * threads that should be blocked as little as possible, as blocking them may in rare + * cases pause the native process or even create a deadlock.

+ * + *

The main thread must certainly have a Looper, thus if you call {@link Shell.Builder#open()} + * from the main thread, a handler will (by default) be auto-created, and all the callbacks + * will be called on the main thread. While this is often convenient and easy to code with, + * you should be aware that if your callbacks are 'expensive' to execute, this may negatively + * impact UI performance.

+ * + *

Background threads usually do not have a Looper, so calling {@link Shell.Builder#open()} + * from such a background thread will (by default) result in all the callbacks being executed + * in one of the gobbler threads. You will have to make sure the code you execute in these callbacks + * is thread-safe.

+ */ + public static class Interactive { + private final Handler handler; + private final boolean autoHandler; + private final String shell; + private final boolean wantSTDERR; + private final List commands; + private final Map environment; + private final OnLineListener onSTDOUTLineListener; + private final OnLineListener onSTDERRLineListener; + private int watchdogTimeout; + + private Process process = null; + private DataOutputStream STDIN = null; + private StreamGobbler STDOUT = null; + private StreamGobbler STDERR = null; + private ScheduledThreadPoolExecutor watchdog = null; + + private volatile boolean running = false; + private volatile boolean idle = true; // read/write only synchronized + private volatile boolean closed = true; + private volatile int callbacks = 0; + private volatile int watchdogCount; + + private Object idleSync = new Object(); + private Object callbackSync = new Object(); + + private volatile int lastExitCode = 0; + private volatile String lastMarkerSTDOUT = null; + private volatile String lastMarkerSTDERR = null; + private volatile Command command = null; + private volatile List buffer = null; + + /** + * The only way to create an instance: Shell.Builder::open() + * + * @param builder Builder class to take values from + */ + private Interactive(final Builder builder, final OnCommandResultListener onCommandResultListener) { + autoHandler = builder.autoHandler; + shell = builder.shell; + wantSTDERR = builder.wantSTDERR; + commands = builder.commands; + environment = builder.environment; + onSTDOUTLineListener = builder.onSTDOUTLineListener; + onSTDERRLineListener = builder.onSTDERRLineListener; + watchdogTimeout = builder.watchdogTimeout; + + // If a looper is available, we offload the callbacks from the gobbling threads + // to whichever thread created us. Would normally do this in open(), + // but then we could not declare handler as final + if ((Looper.myLooper() != null) && (builder.handler == null) && autoHandler) { + handler = new Handler(); + } else { + handler = builder.handler; + } + + boolean ret = open(); + if (onCommandResultListener == null) { + return; + } else if (ret == false) { + onCommandResultListener.onCommandResult(0, OnCommandResultListener.SHELL_EXEC_FAILED, null); + return; + } + + // Allow up to 60 seconds for SuperSU/Superuser dialog, then enable the user-specified + // timeout for all subsequent operations + watchdogTimeout = 60; + addCommand(Shell.availableTestCommands, 0, new OnCommandResultListener() { + public void onCommandResult(int commandCode, int exitCode, List output) { + if (exitCode == OnCommandResultListener.SHELL_RUNNING && + Shell.parseAvailableResult(output, Shell.SU.isSU(shell)) != true) { + // shell is up, but it's brain-damaged + exitCode = OnCommandResultListener.SHELL_WRONG_UID; + } + watchdogTimeout = builder.watchdogTimeout; + onCommandResultListener.onCommandResult(0, exitCode, output); + } + }); + } + + @Override + protected void finalize() throws Throwable { + if (!closed && Debug.getSanityChecksEnabledEffective()) { + // waste of resources + Debug.log(ShellNotClosedException.EXCEPTION_NOT_CLOSED); + throw new ShellNotClosedException(); + } + super.finalize(); + } + + /** + * Add a command to execute + * + * @param command Command to execute + */ + public void addCommand(String command) { addCommand(command, 0, null); } + + /** + *

Add a command to execute, with a callback to be called on completion

+ * + *

The thread on which the callback executes is dependent on various factors, see {@link Shell.Interactive} for further details

+ * + * @param command Command to execute + * @param code User-defined value passed back to the callback + * @param onCommandResultListener Callback to be called on completion + */ + public void addCommand(String command, int code, OnCommandResultListener onCommandResultListener) { addCommand(new String[] { command }, code, onCommandResultListener); } + + /** + * Add commands to execute + * + * @param commands Commands to execute + */ + public void addCommand(List commands) { addCommand(commands, 0, null); } + + /** + *

Add commands to execute, with a callback to be called on completion (of all commands)

+ * + *

The thread on which the callback executes is dependent on various factors, see {@link Shell.Interactive} for further details

+ * + * @param commands Commands to execute + * @param code User-defined value passed back to the callback + * @param onCommandResultListener Callback to be called on completion (of all commands) + */ + public void addCommand(List commands, int code, OnCommandResultListener onCommandResultListener) { addCommand(commands.toArray(new String[commands.size()]), code, onCommandResultListener); } + + /** + * Add commands to execute + * + * @param commands Commands to execute + */ + public void addCommand(String[] commands) { addCommand(commands, 0, null); } + + /** + *

Add commands to execute, with a callback to be called on completion (of all commands)

+ * + *

The thread on which the callback executes is dependent on various factors, see {@link Shell.Interactive} for further details

+ * + * @param commands Commands to execute + * @param code User-defined value passed back to the callback + * @param onCommandResultListener Callback to be called on completion (of all commands) + */ + public synchronized void addCommand(String[] commands, int code, OnCommandResultListener onCommandResultListener) { + this.commands.add(new Command(commands, code, onCommandResultListener)); + runNextCommand(); + } + + /** + * Run the next command if any and if ready, signals idle state if no commands left + */ + private void runNextCommand() { + runNextCommand(true); + } + + /** + * Called from a ScheduledThreadPoolExecutor timer thread every second when there is an outstanding command + */ + private synchronized void handleWatchdog() { + final int exitCode; + + if (watchdog == null) return; + if (watchdogTimeout == 0) return; + + if (!isRunning()) { + exitCode = OnCommandResultListener.SHELL_DIED; + Debug.log(String.format("[%s%%] SHELL_DIED", shell.toUpperCase(Locale.ENGLISH))); + } else if (watchdogCount++ < watchdogTimeout) { + return; + } else { + exitCode = OnCommandResultListener.WATCHDOG_EXIT; + Debug.log(String.format("[%s%%] WATCHDOG_EXIT", shell.toUpperCase(Locale.ENGLISH))); + } + + if (handler != null) { + postCallback(command, exitCode, buffer); + } + + // prevent multiple callbacks for the same command + command = null; + buffer = null; + idle = true; + + watchdog.shutdown(); + watchdog = null; + kill(); + } + + /** + * Start the periodic timer when a command is submitted + */ + private void startWatchdog() { + if (watchdogTimeout == 0) { + return; + } + watchdogCount = 0; + watchdog = new ScheduledThreadPoolExecutor(1); + watchdog.scheduleAtFixedRate(new Runnable() { + @Override + public void run() { + handleWatchdog(); + } + }, 1, 1, TimeUnit.SECONDS); + } + + /** + * Disable the watchdog timer upon command completion + */ + private void stopWatchdog() { + if (watchdog != null) { + watchdog.shutdownNow(); + watchdog = null; + } + } + + /** + * Run the next command if any and if ready + * + * @param notifyIdle signals idle state if no commands left ? + */ + private void runNextCommand(boolean notifyIdle) { + // must always be called from a synchronized method + + boolean running = isRunning(); + if (!running) idle = true; + + if (running && idle && (commands.size() > 0)) { + Command command = commands.get(0); + commands.remove(0); + + buffer = null; + lastExitCode = 0; + lastMarkerSTDOUT = null; + lastMarkerSTDERR = null; + + if (command.commands.length > 0) { + try { + if (command.onCommandResultListener != null) { + // no reason to store the output if we don't have an OnCommandResultListener + // user should catch the output with an OnLineListener in this case + buffer = Collections.synchronizedList(new ArrayList()); + } + + idle = false; + this.command = command; + startWatchdog(); + for (String write : command.commands) { + Debug.logCommand(String.format("[%s+] %s", shell.toUpperCase(Locale.ENGLISH), write)); + STDIN.write((write + "\n").getBytes("UTF-8")); + } + STDIN.write(("echo " + command.marker + " $?\n").getBytes("UTF-8")); + STDIN.write(("echo " + command.marker + " >&2\n").getBytes("UTF-8")); + STDIN.flush(); + } catch (IOException e) { + } + } else { + runNextCommand(false); + } + } else if (!running) { + // our shell died for unknown reasons - abort all submissions + while (commands.size() > 0) { + postCallback(commands.remove(0), OnCommandResultListener.SHELL_DIED, null); + } + } + + if (idle && notifyIdle) { + synchronized(idleSync) { + idleSync.notifyAll(); + } + } + } + + /** + * Processes a STDOUT/STDERR line containing an end/exitCode marker + */ + private synchronized void processMarker() { + if (command.marker.equals(lastMarkerSTDOUT) && (command.marker.equals(lastMarkerSTDERR))) { + if (buffer != null) { + postCallback(command, lastExitCode, buffer); + } + + stopWatchdog(); + command = null; + buffer = null; + idle = true; + runNextCommand(); + } + } + + /** + * Process a normal STDOUT/STDERR line + * + * @param line Line to process + * @param listener Callback to call or null + */ + private synchronized void processLine(String line, OnLineListener listener) { + if (listener != null) { + if (handler != null) { + final String fLine = line; + final OnLineListener fListener = listener; + + startCallback(); + handler.post(new Runnable() { + @Override + public void run() { + try { + fListener.onLine(fLine); + } finally { + endCallback(); + } + } + }); + } else { + listener.onLine(line); + } + } + } + + /** + * Add line to internal buffer + * + * @param line Line to add + */ + private synchronized void addBuffer(String line) { + if (buffer != null) { + buffer.add(line); + } + } + + /** + * Increase callback counter + */ + private void startCallback() { + synchronized (callbackSync) { + callbacks++; + } + } + + /** + * Schedule a callback to run on the appropriate thread + */ + private void postCallback(final Command fCommand, final int fExitCode, final List fOutput) { + if (fCommand.onCommandResultListener == null) { + return; + } + if (handler == null) { + fCommand.onCommandResultListener.onCommandResult(fCommand.code, fExitCode, fOutput); + return; + } + startCallback(); + handler.post(new Runnable() { + @Override + public void run() { + try { + fCommand.onCommandResultListener.onCommandResult(fCommand.code, fExitCode, fOutput); + } finally { + endCallback(); + } + } + }); + } + + /** + * Decrease callback counter, signals callback complete state when dropped to 0 + */ + private void endCallback() { + synchronized (callbackSync) { + callbacks--; + if (callbacks == 0) { + callbackSync.notifyAll(); + } + } + } + + /** + * Internal call that launches the shell, starts gobbling, and starts executing commands. + * See {@link Shell.Interactive} + * + * @return Opened successfully ? + */ + private synchronized boolean open() { + Debug.log(String.format("[%s%%] START", shell.toUpperCase(Locale.ENGLISH))); + + try { + // setup our process, retrieve STDIN stream, and STDOUT/STDERR gobblers + if (environment.size() == 0) { + process = Runtime.getRuntime().exec(shell); + } else { + Map newEnvironment = new HashMap(); + newEnvironment.putAll(System.getenv()); + newEnvironment.putAll(environment); + int i = 0; + String[] env = new String[newEnvironment.size()]; + for (Map.Entry entry : newEnvironment.entrySet()) { + env[i] = entry.getKey() + "=" + entry.getValue(); + i++; + } + process = Runtime.getRuntime().exec(shell, env); + } + + STDIN = new DataOutputStream(process.getOutputStream()); + STDOUT = new StreamGobbler(shell.toUpperCase(Locale.ENGLISH) + "-", process.getInputStream(), new OnLineListener() { + @Override + public void onLine(String line) { + synchronized (Interactive.this) { + if (command == null) { + return; + } + if (line.startsWith(command.marker)) { + try { + lastExitCode = Integer.valueOf(line.substring(command.marker.length() + 1), 10); + } catch (Exception e) { + } + lastMarkerSTDOUT = command.marker; + processMarker(); + } else { + addBuffer(line); + processLine(line, onSTDOUTLineListener); + } + } + } + }); + STDERR = new StreamGobbler(shell.toUpperCase(Locale.ENGLISH) + "*", process.getErrorStream(), new OnLineListener() { + @Override + public void onLine(String line) { + synchronized (Interactive.this) { + if (command == null) { + return; + } + if (line.startsWith(command.marker)) { + lastMarkerSTDERR = command.marker; + processMarker(); + } else { + if (wantSTDERR) addBuffer(line); + processLine(line, onSTDERRLineListener); + } + } + } + }); + + // start gobbling and write our commands to the shell + STDOUT.start(); + STDERR.start(); + + running = true; + closed = false; + + runNextCommand(); + + return true; + } catch (IOException e) { + // shell probably not found + return false; + } + } + + /** + * Close shell and clean up all resources. Call this when you are done with the shell. + * If the shell is not idle (all commands completed) you should not call this method + * from the main UI thread because it may block for a long time. This method will + * intentionally crash your app (if in debug mode) if you try to do this anyway. + */ + public void close() { + boolean _idle = isIdle(); // idle must be checked synchronized + + synchronized (this) { + if (!running) return; + running = false; + closed = true; + } + + // This method should not be called from the main thread unless the shell is idle + // and can be cleaned up with (minimal) waiting. Only throw in debug mode. + if (!_idle && Debug.getSanityChecksEnabledEffective() && Debug.onMainThread()) { + Debug.log(ShellOnMainThreadException.EXCEPTION_NOT_IDLE); + throw new ShellOnMainThreadException(ShellOnMainThreadException.EXCEPTION_NOT_IDLE); + } + + if (!_idle) waitForIdle(); + + try { + STDIN.write(("exit\n").getBytes("UTF-8")); + STDIN.flush(); + + // wait for our process to finish, while we gobble away in the background + process.waitFor(); + + // make sure our threads are done gobbling, our streams are closed, and the process is + // destroyed - while the latter two shouldn't be needed in theory, and may even produce + // warnings, in "normal" Java they are required for guaranteed cleanup of resources, so + // lets be safe and do this on Android as well + try { + STDIN.close(); + } catch (IOException e) { + } + STDOUT.join(); + STDERR.join(); + stopWatchdog(); + process.destroy(); + } catch (IOException e) { + // shell probably not found + } catch (InterruptedException e) { + // this should really be re-thrown + } + + Debug.log(String.format("[%s%%] END", shell.toUpperCase(Locale.ENGLISH))); + } + + /** + * Try to clean up as much as possible from a shell that's gotten itself wedged. + * Hopefully the StreamGobblers will croak on their own when the other side of + * the pipe is closed. + */ + public synchronized void kill() { + running = false; + closed = true; + + try { + STDIN.close(); + } catch (IOException e) { + } + try { + process.destroy(); + } catch (Exception e) { + } + } + + /** + * Is our shell still running ? + * + * @return Shell running ? + */ + public boolean isRunning() { + try { + // if this throws, we're still running + process.exitValue(); + return false; + } catch (IllegalThreadStateException e) { + } + return true; + } + + /** + * Have all commands completed executing ? + * + * @return Shell idle ? + */ + public synchronized boolean isIdle() { + if (!isRunning()) { + idle = true; + synchronized(idleSync) { + idleSync.notifyAll(); + } + } + return idle; + } + + /** + *

Wait for idle state. As this is a blocking call, you should not call it from the main UI thread. + * If you do so and debug mode is enabled, this method will intentionally crash your app.

+ * + *

If not interrupted, this method will not return until all commands have finished executing. + * Note that this does not necessarily mean that all the callbacks have fired yet.

+ * + *

If no Handler is used, all callbacks will have been executed when this method returns. If + * a Handler is used, and this method is called from a different thread than associated with the + * Handler's Looper, all callbacks will have been executed when this method returns as well. + * If however a Handler is used but this method is called from the same thread as associated + * with the Handler's Looper, there is no way to know.

+ * + *

In practice this means that in most simple cases all callbacks will have completed when this + * method returns, but if you actually depend on this behavior, you should make certain this is + * indeed the case.

+ * + *

See {@link Shell.Interactive} for further details on threading and handlers

+ * + * @return True if wait complete, false if wait interrupted + */ + public boolean waitForIdle() { + if (Debug.getSanityChecksEnabledEffective() && Debug.onMainThread()) { + Debug.log(ShellOnMainThreadException.EXCEPTION_WAIT_IDLE); + throw new ShellOnMainThreadException(ShellOnMainThreadException.EXCEPTION_WAIT_IDLE); + } + + if (isRunning()) { + synchronized (idleSync) { + while (!idle) { + try { + idleSync.wait(); + } catch (InterruptedException e) { + return false; + } + } + } + + if ( + (handler != null) && + (handler.getLooper() != null) && + (handler.getLooper() != Looper.myLooper()) + ) { + // If the callbacks are posted to a different thread than this one, we can wait until + // all callbacks have called before returning. If we don't use a Handler at all, + // the callbacks are already called before we get here. If we do use a Handler but + // we use the same Looper, waiting here would actually block the callbacks from being + // called + + synchronized (callbackSync) { + while (callbacks > 0) { + try { + callbackSync.wait(); + } catch (InterruptedException e) { + return false; + } + } + } + } + } + + return true; + } + + /** + * Are we using a Handler to post callbacks ? + * + * @return Handler used ? + */ + public boolean hasHandler() { + return (handler != null); + } + } } diff --git a/libsuperuser/src/eu/chainfire/libsuperuser/ShellNotClosedException.java b/libsuperuser/src/eu/chainfire/libsuperuser/ShellNotClosedException.java index 6b2d7ae..fe31c0f 100644 --- a/libsuperuser/src/eu/chainfire/libsuperuser/ShellNotClosedException.java +++ b/libsuperuser/src/eu/chainfire/libsuperuser/ShellNotClosedException.java @@ -21,9 +21,9 @@ */ @SuppressWarnings("serial") public class ShellNotClosedException extends RuntimeException { - public static final String EXCEPTION_NOT_CLOSED = "Application did not close() interactive shell"; + public static final String EXCEPTION_NOT_CLOSED = "Application did not close() interactive shell"; - public ShellNotClosedException() { - super(EXCEPTION_NOT_CLOSED); - } + public ShellNotClosedException() { + super(EXCEPTION_NOT_CLOSED); + } } diff --git a/libsuperuser/src/eu/chainfire/libsuperuser/ShellOnMainThreadException.java b/libsuperuser/src/eu/chainfire/libsuperuser/ShellOnMainThreadException.java index 70cf23d..acb7764 100644 --- a/libsuperuser/src/eu/chainfire/libsuperuser/ShellOnMainThreadException.java +++ b/libsuperuser/src/eu/chainfire/libsuperuser/ShellOnMainThreadException.java @@ -22,11 +22,11 @@ */ @SuppressWarnings("serial") public class ShellOnMainThreadException extends RuntimeException { - public static final String EXCEPTION_COMMAND = "Application attempted to run a shell command from the main thread"; - public static final String EXCEPTION_NOT_IDLE = "Application attempted to wait for a non-idle shell to close on the main thread"; - public static final String EXCEPTION_WAIT_IDLE = "Application attempted to wait for a shell to become idle on the main thread"; - - public ShellOnMainThreadException(String message) { - super(message); - } + public static final String EXCEPTION_COMMAND = "Application attempted to run a shell command from the main thread"; + public static final String EXCEPTION_NOT_IDLE = "Application attempted to wait for a non-idle shell to close on the main thread"; + public static final String EXCEPTION_WAIT_IDLE = "Application attempted to wait for a shell to become idle on the main thread"; + + public ShellOnMainThreadException(String message) { + super(message); + } } diff --git a/libsuperuser/src/eu/chainfire/libsuperuser/StreamGobbler.java b/libsuperuser/src/eu/chainfire/libsuperuser/StreamGobbler.java index 893f75e..04c4fe7 100644 --- a/libsuperuser/src/eu/chainfire/libsuperuser/StreamGobbler.java +++ b/libsuperuser/src/eu/chainfire/libsuperuser/StreamGobbler.java @@ -26,78 +26,78 @@ * Thread utility class continuously reading from an InputStream */ public class StreamGobbler extends Thread { - /** - * Line callback interface - */ - public interface OnLineListener { - /** - *

Line callback

- * - *

This callback should process the line as quickly as possible. - * Delays in this callback may pause the native process or even - * result in a deadlock

- * - * @param line String that was gobbled - */ - public void onLine(String line); - } - - private String shell = null; - private BufferedReader reader = null; - private List writer = null; - private OnLineListener listener = null; - - /** - *

StreamGobbler constructor

- * - *

We use this class because shell STDOUT and STDERR should be read as quickly as - * possible to prevent a deadlock from occurring, or Process.waitFor() never - * returning (as the buffer is full, pausing the native process)

- * - * @param shell Name of the shell - * @param inputStream InputStream to read from - * @param outputList List to write to, or null - */ - public StreamGobbler(String shell, InputStream inputStream, List outputList) { - this.shell = shell; - reader = new BufferedReader(new InputStreamReader(inputStream)); - writer = outputList; - } - - /** - *

StreamGobbler constructor

- * - *

We use this class because shell STDOUT and STDERR should be read as quickly as - * possible to prevent a deadlock from occurring, or Process.waitFor() never - * returning (as the buffer is full, pausing the native process)

- * - * @param shell Name of the shell - * @param inputStream InputStream to read from - * @param onLineListener OnLineListener callback - */ - public StreamGobbler(String shell, InputStream inputStream, OnLineListener onLineListener) { - this.shell = shell; - reader = new BufferedReader(new InputStreamReader(inputStream)); - listener = onLineListener; - } + /** + * Line callback interface + */ + public interface OnLineListener { + /** + *

Line callback

+ * + *

This callback should process the line as quickly as possible. + * Delays in this callback may pause the native process or even + * result in a deadlock

+ * + * @param line String that was gobbled + */ + public void onLine(String line); + } - @Override - public void run() { - // keep reading the InputStream until it ends (or an error occurs) - try { - String line = null; - while ((line = reader.readLine()) != null) { - Debug.logOutput(String.format("[%s] %s", shell, line)); - if (writer != null) writer.add(line); - if (listener != null) listener.onLine(line); - } - } catch (IOException e) { - } - - // make sure our stream is closed and resources will be freed - try { - reader.close(); - } catch (IOException e) { - } - } + private String shell = null; + private BufferedReader reader = null; + private List writer = null; + private OnLineListener listener = null; + + /** + *

StreamGobbler constructor

+ * + *

We use this class because shell STDOUT and STDERR should be read as quickly as + * possible to prevent a deadlock from occurring, or Process.waitFor() never + * returning (as the buffer is full, pausing the native process)

+ * + * @param shell Name of the shell + * @param inputStream InputStream to read from + * @param outputList List to write to, or null + */ + public StreamGobbler(String shell, InputStream inputStream, List outputList) { + this.shell = shell; + reader = new BufferedReader(new InputStreamReader(inputStream)); + writer = outputList; + } + + /** + *

StreamGobbler constructor

+ * + *

We use this class because shell STDOUT and STDERR should be read as quickly as + * possible to prevent a deadlock from occurring, or Process.waitFor() never + * returning (as the buffer is full, pausing the native process)

+ * + * @param shell Name of the shell + * @param inputStream InputStream to read from + * @param onLineListener OnLineListener callback + */ + public StreamGobbler(String shell, InputStream inputStream, OnLineListener onLineListener) { + this.shell = shell; + reader = new BufferedReader(new InputStreamReader(inputStream)); + listener = onLineListener; + } + + @Override + public void run() { + // keep reading the InputStream until it ends (or an error occurs) + try { + String line = null; + while ((line = reader.readLine()) != null) { + Debug.logOutput(String.format("[%s] %s", shell, line)); + if (writer != null) writer.add(line); + if (listener != null) listener.onLine(line); + } + } catch (IOException e) { + } + + // make sure our stream is closed and resources will be freed + try { + reader.close(); + } catch (IOException e) { + } + } } diff --git a/libsuperuser_example/src/eu/chainfire/libsuperuser_example/BackgroundIntentService.java b/libsuperuser_example/src/eu/chainfire/libsuperuser_example/BackgroundIntentService.java index 79a6812..24bd885 100644 --- a/libsuperuser_example/src/eu/chainfire/libsuperuser_example/BackgroundIntentService.java +++ b/libsuperuser_example/src/eu/chainfire/libsuperuser_example/BackgroundIntentService.java @@ -49,53 +49,53 @@ * could have been a lot shorter. */ public class BackgroundIntentService extends IntentService { - // you could provide more options here, should you need them - public static final String ACTION_BOOT_COMPLETE = "boot_complete"; - - public static void performAction(Context context, String action) { - performAction(context, action, null); - } + // you could provide more options here, should you need them + public static final String ACTION_BOOT_COMPLETE = "boot_complete"; - public static void performAction(Context context, String action, Bundle extras) { - // this is utility call to easy starting the service and performing a task - // pass parameters in an bundle to be added to the intent as extras - // See BootCompleteReceiver.java - - if ((context == null) || (action == null) || action.equals("")) return; - - Intent svc = new Intent(context, BackgroundIntentService.class); - svc.setAction(action); - if (extras != null) svc.putExtras(extras); - context.startService(svc); - } - - public BackgroundIntentService() { - // If you forget this one, the app will crash - super("BackgroundIntentService"); - } + public static void performAction(Context context, String action) { + performAction(context, action, null); + } - @Override - protected void onHandleIntent(Intent intent) { - String action = intent.getAction(); - if ((action == null) || (action.equals(""))) return; - - if (action.equals(ACTION_BOOT_COMPLETE)) { - onBootComplete(); - } - // you can define more options here... pass parameters through the "extra" values - } - - protected void onBootComplete() { - // We are running in a background thread here! - - // This would crash (when debugging) if it was called from the main thread: - Shell.SU.run("ls -l /"); - - // Let's toast that we're done, using the work-arounds and utility function in - // out Application class. Without those modifications there would be a very high - // chance of crashing the app in various Android versions. The modifications are - // simple and easily ported to your own Application class, if you can't use the - // one from libsuperuser. - Application.toast(this, "This toast will self-destruct in five seconds"); - } + public static void performAction(Context context, String action, Bundle extras) { + // this is utility call to easy starting the service and performing a task + // pass parameters in an bundle to be added to the intent as extras + // See BootCompleteReceiver.java + + if ((context == null) || (action == null) || action.equals("")) return; + + Intent svc = new Intent(context, BackgroundIntentService.class); + svc.setAction(action); + if (extras != null) svc.putExtras(extras); + context.startService(svc); + } + + public BackgroundIntentService() { + // If you forget this one, the app will crash + super("BackgroundIntentService"); + } + + @Override + protected void onHandleIntent(Intent intent) { + String action = intent.getAction(); + if ((action == null) || (action.equals(""))) return; + + if (action.equals(ACTION_BOOT_COMPLETE)) { + onBootComplete(); + } + // you can define more options here... pass parameters through the "extra" values + } + + protected void onBootComplete() { + // We are running in a background thread here! + + // This would crash (when debugging) if it was called from the main thread: + Shell.SU.run("ls -l /"); + + // Let's toast that we're done, using the work-arounds and utility function in + // out Application class. Without those modifications there would be a very high + // chance of crashing the app in various Android versions. The modifications are + // simple and easily ported to your own Application class, if you can't use the + // one from libsuperuser. + Application.toast(this, "This toast will self-destruct in five seconds"); + } } diff --git a/libsuperuser_example/src/eu/chainfire/libsuperuser_example/BootCompleteReceiver.java b/libsuperuser_example/src/eu/chainfire/libsuperuser_example/BootCompleteReceiver.java index 8277125..6a942c2 100644 --- a/libsuperuser_example/src/eu/chainfire/libsuperuser_example/BootCompleteReceiver.java +++ b/libsuperuser_example/src/eu/chainfire/libsuperuser_example/BootCompleteReceiver.java @@ -26,19 +26,19 @@ * in a background thread */ public class BootCompleteReceiver extends BroadcastReceiver{ - @Override - public void onReceive(Context context, Intent intent) { - // What many beginners don't realize is that BroadcastReceivers like these - // usually run in the application's main thread, and can thus generate - // ANRs. This is increasingly likely with the BOOT_COMPLETED receiver, as - // the system is likely very busy when this receiver is called. - - // In this example we are starting our MyIntentService to actually do the - // work we want to happen, not only because "su" should specifically NEVER - // be called from a BroadcastReceiver, but also because you should be doing - // this even if you aren't calling "su". It's a good practise, and using - // IntentService is really easy. - - BackgroundIntentService.performAction(context, BackgroundIntentService.ACTION_BOOT_COMPLETE); - } + @Override + public void onReceive(Context context, Intent intent) { + // What many beginners don't realize is that BroadcastReceivers like these + // usually run in the application's main thread, and can thus generate + // ANRs. This is increasingly likely with the BOOT_COMPLETED receiver, as + // the system is likely very busy when this receiver is called. + + // In this example we are starting our MyIntentService to actually do the + // work we want to happen, not only because "su" should specifically NEVER + // be called from a BroadcastReceiver, but also because you should be doing + // this even if you aren't calling "su". It's a good practise, and using + // IntentService is really easy. + + BackgroundIntentService.performAction(context, BackgroundIntentService.ACTION_BOOT_COMPLETE); + } } \ No newline at end of file diff --git a/libsuperuser_example/src/eu/chainfire/libsuperuser_example/InteractiveActivity.java b/libsuperuser_example/src/eu/chainfire/libsuperuser_example/InteractiveActivity.java index 6b93579..9d2ccd5 100644 --- a/libsuperuser_example/src/eu/chainfire/libsuperuser_example/InteractiveActivity.java +++ b/libsuperuser_example/src/eu/chainfire/libsuperuser_example/InteractiveActivity.java @@ -31,79 +31,79 @@ import android.widget.TextView; public class InteractiveActivity extends Activity { - - private static Shell.Interactive rootSession; - - private void updateResultStatus(boolean suAvailable, List suResult) { - StringBuilder sb = (new StringBuilder()). - append("Root? ").append(suAvailable ? "Yes" : "No").append((char)10). - append((char)10); - if (suResult != null) { - for (String line : suResult) { - sb.append(line).append((char)10); - } - } - ((TextView)findViewById(R.id.text)).setText(sb.toString()); - } - - private void reportError(String error) { - List errorInfo = new ArrayList(); - errorInfo.add(error); - updateResultStatus(false, errorInfo); - rootSession = null; - } - - private void sendRootCommand() { - rootSession.addCommand(new String[] { "id", "date", "ls -l /" }, 0, - new Shell.OnCommandResultListener() { - public void onCommandResult(int commandCode, int exitCode, List output) { - if (exitCode < 0) { - reportError("Error executing commands: exitCode " + exitCode); - } else { - updateResultStatus(true, output); - } - } - }); - } - - private void openRootShell() { - if (rootSession != null) { - sendRootCommand(); - } else { - // We're creating a progress dialog here because we want the user to wait. - // If in your app your user can just continue on with clicking other things, - // don't do the dialog thing. - final ProgressDialog dialog = new ProgressDialog(this); - dialog.setTitle("Please wait"); - dialog.setMessage("Requesting root privilege..."); - dialog.setIndeterminate(true); - dialog.setCancelable(false); - dialog.show(); - - // start the shell in the background and keep it alive as long as the app is running - rootSession = new Shell.Builder(). - useSU(). - setWantSTDERR(true). - setWatchdogTimeout(5). - setMinimalLogging(true). - open(new Shell.OnCommandResultListener() { - - // Callback to report whether the shell was successfully started up - @Override - public void onCommandResult(int commandCode, int exitCode, List output) { - // note: this will FC if you rotate the phone while the dialog is up - dialog.dismiss(); - - if (exitCode != Shell.OnCommandResultListener.SHELL_RUNNING) { - reportError("Error opening root shell: exitCode " + exitCode); - } else { - // Shell is up: send our first request - sendRootCommand(); - } - } - }); - } - } + + private static Shell.Interactive rootSession; + + private void updateResultStatus(boolean suAvailable, List suResult) { + StringBuilder sb = (new StringBuilder()). + append("Root? ").append(suAvailable ? "Yes" : "No").append((char)10). + append((char)10); + if (suResult != null) { + for (String line : suResult) { + sb.append(line).append((char)10); + } + } + ((TextView)findViewById(R.id.text)).setText(sb.toString()); + } + + private void reportError(String error) { + List errorInfo = new ArrayList(); + errorInfo.add(error); + updateResultStatus(false, errorInfo); + rootSession = null; + } + + private void sendRootCommand() { + rootSession.addCommand(new String[] { "id", "date", "ls -l /" }, 0, + new Shell.OnCommandResultListener() { + public void onCommandResult(int commandCode, int exitCode, List output) { + if (exitCode < 0) { + reportError("Error executing commands: exitCode " + exitCode); + } else { + updateResultStatus(true, output); + } + } + }); + } + + private void openRootShell() { + if (rootSession != null) { + sendRootCommand(); + } else { + // We're creating a progress dialog here because we want the user to wait. + // If in your app your user can just continue on with clicking other things, + // don't do the dialog thing. + final ProgressDialog dialog = new ProgressDialog(this); + dialog.setTitle("Please wait"); + dialog.setMessage("Requesting root privilege..."); + dialog.setIndeterminate(true); + dialog.setCancelable(false); + dialog.show(); + + // start the shell in the background and keep it alive as long as the app is running + rootSession = new Shell.Builder(). + useSU(). + setWantSTDERR(true). + setWatchdogTimeout(5). + setMinimalLogging(true). + open(new Shell.OnCommandResultListener() { + + // Callback to report whether the shell was successfully started up + @Override + public void onCommandResult(int commandCode, int exitCode, List output) { + // note: this will FC if you rotate the phone while the dialog is up + dialog.dismiss(); + + if (exitCode != Shell.OnCommandResultListener.SHELL_RUNNING) { + reportError("Error opening root shell: exitCode " + exitCode); + } else { + // Shell is up: send our first request + sendRootCommand(); + } + } + }); + } + } @Override public void onCreate(Bundle savedInstanceState) { @@ -116,19 +116,19 @@ public void onCreate(Bundle savedInstanceState) { button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - startActivity(new Intent(v.getContext(), MainActivity.class)); - finish(); + startActivity(new Intent(v.getContext(), MainActivity.class)); + finish(); } }); // refresh button ((Button)findViewById(R.id.refresh_button)). - setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - openRootShell(); - } - }); + setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + openRootShell(); + } + }); openRootShell(); } diff --git a/libsuperuser_example/src/eu/chainfire/libsuperuser_example/MainActivity.java b/libsuperuser_example/src/eu/chainfire/libsuperuser_example/MainActivity.java index 80f02f7..6060d18 100644 --- a/libsuperuser_example/src/eu/chainfire/libsuperuser_example/MainActivity.java +++ b/libsuperuser_example/src/eu/chainfire/libsuperuser_example/MainActivity.java @@ -31,72 +31,72 @@ import android.widget.Button; public class MainActivity extends Activity { - private class Startup extends AsyncTask { - private ProgressDialog dialog = null; - private Context context = null; - private boolean suAvailable = false; - private String suVersion = null; - private String suVersionInternal = null; - private List suResult = null; - - public Startup setContext(Context context) { - this.context = context; - return this; - } - - @Override - protected void onPreExecute() { - // We're creating a progress dialog here because we want the user to wait. - // If in your app your user can just continue on with clicking other things, - // don't do the dialog thing. - - dialog = new ProgressDialog(context); - dialog.setTitle("Some title"); - dialog.setMessage("Doing something interesting ..."); - dialog.setIndeterminate(true); - dialog.setCancelable(false); - dialog.show(); - } - - @Override - protected Void doInBackground(Void... params) { - // Let's do some SU stuff - suAvailable = Shell.SU.available(); - if (suAvailable) { - suVersion = Shell.SU.version(false); - suVersionInternal = Shell.SU.version(true); - suResult = Shell.SU.run(new String[] { - "id", - "ls -l /" - }); - } - - // This is just so you see we had a progress dialog, - // don't do this in production code - try { Thread.sleep(5000); } catch(Exception e) { } - - return null; - } - - @Override - protected void onPostExecute(Void result) { - dialog.dismiss(); - - // output - StringBuilder sb = (new StringBuilder()). - append("Root? ").append(suAvailable ? "Yes" : "No").append((char)10). - append("Version: ").append(suVersion == null ? "N/A" : suVersion).append((char)10). - append("Version (internal): ").append(suVersionInternal == null ? "N/A" : suVersionInternal).append((char)10). - append((char)10); - if (suResult != null) { - for (String line : suResult) { - sb.append(line).append((char)10); - } - } - ((TextView)findViewById(R.id.text)).setText(sb.toString()); - } - } - + private class Startup extends AsyncTask { + private ProgressDialog dialog = null; + private Context context = null; + private boolean suAvailable = false; + private String suVersion = null; + private String suVersionInternal = null; + private List suResult = null; + + public Startup setContext(Context context) { + this.context = context; + return this; + } + + @Override + protected void onPreExecute() { + // We're creating a progress dialog here because we want the user to wait. + // If in your app your user can just continue on with clicking other things, + // don't do the dialog thing. + + dialog = new ProgressDialog(context); + dialog.setTitle("Some title"); + dialog.setMessage("Doing something interesting ..."); + dialog.setIndeterminate(true); + dialog.setCancelable(false); + dialog.show(); + } + + @Override + protected Void doInBackground(Void... params) { + // Let's do some SU stuff + suAvailable = Shell.SU.available(); + if (suAvailable) { + suVersion = Shell.SU.version(false); + suVersionInternal = Shell.SU.version(true); + suResult = Shell.SU.run(new String[] { + "id", + "ls -l /" + }); + } + + // This is just so you see we had a progress dialog, + // don't do this in production code + try { Thread.sleep(5000); } catch(Exception e) { } + + return null; + } + + @Override + protected void onPostExecute(Void result) { + dialog.dismiss(); + + // output + StringBuilder sb = (new StringBuilder()). + append("Root? ").append(suAvailable ? "Yes" : "No").append((char)10). + append("Version: ").append(suVersion == null ? "N/A" : suVersion).append((char)10). + append("Version (internal): ").append(suVersionInternal == null ? "N/A" : suVersionInternal).append((char)10). + append((char)10); + if (suResult != null) { + for (String line : suResult) { + sb.append(line).append((char)10); + } + } + ((TextView)findViewById(R.id.text)).setText(sb.toString()); + } + } + @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -108,19 +108,19 @@ public void onCreate(Bundle savedInstanceState) { button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - startActivity(new Intent(v.getContext(), InteractiveActivity.class)); - finish(); + startActivity(new Intent(v.getContext(), InteractiveActivity.class)); + finish(); } }); // refresh button ((Button)findViewById(R.id.refresh_button)). - setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - (new Startup()).setContext(v.getContext()).execute(); - } - }); + setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + (new Startup()).setContext(v.getContext()).execute(); + } + }); // Let's do some background stuff (new Startup()).setContext(this).execute();