diff --git a/.gitignore b/.gitignore index 0356383..57bdb53 100644 --- a/.gitignore +++ b/.gitignore @@ -1,17 +1,7 @@ -# built application files -*.apk -*.ap_ - -# files for the dex VM -*.dex - -# Java class files -*.class - -# generated files -bin/ -gen/ - - -# Local configuration file (sdk path, etc) -local.properties \ No newline at end of file +.gradle +/local.properties +/.idea/workspace.xml +/.idea/libraries +.DS_Store +/build +/gen diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 0000000..9045068 --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +RootShell \ No newline at end of file diff --git a/.idea/RootShell.iml b/.idea/RootShell.iml new file mode 100644 index 0000000..bf2c459 --- /dev/null +++ b/.idea/RootShell.iml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/.idea/artifacts/RootShell_jar.xml b/.idea/artifacts/RootShell_jar.xml new file mode 100644 index 0000000..4820238 --- /dev/null +++ b/.idea/artifacts/RootShell_jar.xml @@ -0,0 +1,8 @@ + + + $PROJECT_DIR$/out/artifacts/RootShell_jar + + + + + \ No newline at end of file diff --git a/.idea/codeStyleSettings.xml b/.idea/codeStyleSettings.xml new file mode 100644 index 0000000..2663acf --- /dev/null +++ b/.idea/codeStyleSettings.xml @@ -0,0 +1,174 @@ + + + + + + + diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..217af47 --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,23 @@ + + + + + + diff --git a/.idea/copyright/profiles_settings.xml b/.idea/copyright/profiles_settings.xml new file mode 100644 index 0000000..e7bedf3 --- /dev/null +++ b/.idea/copyright/profiles_settings.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 0000000..e206d70 --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..11d353a --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..76fd194 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/.idea/scopes/scope_settings.xml b/.idea/scopes/scope_settings.xml new file mode 100644 index 0000000..922003b --- /dev/null +++ b/.idea/scopes/scope_settings.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..275077f --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/AndroidManifest.xml b/AndroidManifest.xml new file mode 100644 index 0000000..329d2b8 --- /dev/null +++ b/AndroidManifest.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + diff --git a/README.md b/README.md new file mode 100644 index 0000000..9f05f47 --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +RootShell provides rooted developers with an easy to use Root Shell for their Android Applications. + +You can find the latest release here: https://github.com/Stericson/RootShell/releases + +You can find more information at our wiki: https://github.com/Stericson/RootShell/wiki diff --git a/app/app.iml b/app/app.iml new file mode 100644 index 0000000..a9eaf3a --- /dev/null +++ b/app/app.iml @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/drawable-hdpi/ic_launcher.png b/res/drawable-hdpi/ic_launcher.png new file mode 100644 index 0000000..96a442e Binary files /dev/null and b/res/drawable-hdpi/ic_launcher.png differ diff --git a/res/drawable-mdpi/ic_launcher.png b/res/drawable-mdpi/ic_launcher.png new file mode 100644 index 0000000..359047d Binary files /dev/null and b/res/drawable-mdpi/ic_launcher.png differ diff --git a/res/drawable-xhdpi/ic_launcher.png b/res/drawable-xhdpi/ic_launcher.png new file mode 100644 index 0000000..71c6d76 Binary files /dev/null and b/res/drawable-xhdpi/ic_launcher.png differ diff --git a/res/drawable-xxhdpi/ic_launcher.png b/res/drawable-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..4df1894 Binary files /dev/null and b/res/drawable-xxhdpi/ic_launcher.png differ diff --git a/res/values/strings.xml b/res/values/strings.xml new file mode 100644 index 0000000..0d1efdb --- /dev/null +++ b/res/values/strings.xml @@ -0,0 +1,3 @@ + + RootTools + diff --git a/src/com/stericson/RootShell/RootShell.java b/src/com/stericson/RootShell/RootShell.java new file mode 100644 index 0000000..16801dd --- /dev/null +++ b/src/com/stericson/RootShell/RootShell.java @@ -0,0 +1,555 @@ +/* + * This file is part of the RootShell Project: http://code.google.com/p/RootShell/ + * + * Copyright (c) 2014 Stephen Erickson, Chris Ravenscroft + * + * This code is dual-licensed under the terms of the Apache License Version 2.0 and + * the terms of the General Public License (GPL) Version 2. + * You may use this code according to either of these licenses as is most appropriate + * for your project on a case-by-case basis. + * + * The terms of each license can be found in the root directory of this project's repository as well as at: + * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * http://www.gnu.org/licenses/gpl-2.0.txt + * + * Unless required by applicable law or agreed to in writing, software + * distributed under these Licenses is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See each License for the specific language governing permissions and + * limitations under that License. + */ +package com.stericson.RootShell; + + +import com.stericson.RootShell.exceptions.RootDeniedException; +import com.stericson.RootShell.execution.Command; +import com.stericson.RootShell.execution.Shell; + +import android.util.Log; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.TimeoutException; + +public class RootShell { + + // -------------------- + // # Public Variables # + // -------------------- + + public static boolean debugMode = false; + + public static final String version = "RootShell v1.1"; + + /** + * Setting this to false will disable the handler that is used + * by default for the 3 callback methods for Command. + *

+ * By disabling this all callbacks will be called from a thread other than + * the main UI thread. + */ + public static boolean handlerEnabled = true; + + + /** + * Setting this will change the default command timeout. + *

+ * The default is 20000ms + */ + public static int defaultCommandTimeout = 20000; + + public static enum LogLevel { + VERBOSE, + ERROR, + DEBUG, + WARN + } + // -------------------- + // # Public Methods # + // -------------------- + + /** + * This will close all open shells. + */ + public static void closeAllShells() throws IOException { + Shell.closeAll(); + } + + /** + * This will close the custom shell that you opened. + */ + public static void closeCustomShell() throws IOException { + Shell.closeCustomShell(); + } + + /** + * This will close either the root shell or the standard shell depending on what you specify. + * + * @param root a boolean to specify whether to close the root shell or the standard shell. + */ + public static void closeShell(boolean root) throws IOException { + if (root) { + Shell.closeRootShell(); + } else { + Shell.closeShell(); + } + } + + /** + * Use this to check whether or not a file exists on the filesystem. + * + * @param file String that represent the file, including the full path to the + * file and its name. + * @return a boolean that will indicate whether or not the file exists. + */ + public static boolean exists(final String file) { + return exists(file, false); + } + + /** + * Use this to check whether or not a file OR directory exists on the filesystem. + * + * @param file String that represent the file OR the directory, including the full path to the + * file and its name. + * @param isDir boolean that represent whether or not we are looking for a directory + * @return a boolean that will indicate whether or not the file exists. + */ + public static boolean exists(final String file, boolean isDir) { + final List result = new ArrayList(); + + String cmdToExecute = "ls " + (isDir ? "-d " : " "); + + Command command = new Command(0, false, cmdToExecute + file) { + @Override + public void commandOutput(int id, String line) { + RootShell.log(line); + result.add(line); + + super.commandOutput(id, line); + } + }; + + try { + //Try without root... + RootShell.getShell(false).add(command); + commandWait(RootShell.getShell(false), command); + + } catch (Exception e) { + return false; + } + + for (String line : result) { + if (line.trim().equals(file)) { + return true; + } + } + + result.clear(); + + try { + RootShell.getShell(true).add(command); + commandWait(RootShell.getShell(true), command); + + } catch (Exception e) { + return false; + } + + //Avoid concurrent modification... + List final_result = new ArrayList(); + final_result.addAll(result); + + for (String line : final_result) { + if (line.trim().equals(file)) { + return true; + } + } + + return false; + + } + + /** + * @param binaryName String that represent the binary to find. + * @return List containing the locations the binary was found at. + */ + public static List findBinary(final String binaryName) { + boolean found = false; + + final List list = new ArrayList(); + String[] places = { + "/sbin/", "/system/bin/", "/system/xbin/", "/data/local/xbin/", + "/data/local/bin/", "/system/sd/xbin/", "/system/bin/failsafe/", "/data/local/" + }; + + RootShell.log("Checking for " + binaryName); + + //Try to use stat first + try { + for (final String path : places) { + Command cc = new Command(0, false, "stat " + path + binaryName) { + @Override + public void commandOutput(int id, String line) { + if (line.contains("File: ") && line.contains(binaryName)) { + list.add(path); + + RootShell.log(binaryName + " was found here: " + path); + } + + RootShell.log(line); + + super.commandOutput(id, line); + } + }; + + RootShell.getShell(false).add(cc); + commandWait(RootShell.getShell(false), cc); + + } + + found = !list.isEmpty(); + } catch (Exception e) { + RootShell.log(binaryName + " was not found, more information MAY be available with Debugging on."); + } + + if (!found) { + RootShell.log("Trying second method"); + + for (String where : places) { + if (RootShell.exists(where + binaryName)) { + RootShell.log(binaryName + " was found here: " + where); + list.add(where); + found = true; + } else { + RootShell.log(binaryName + " was NOT found here: " + where); + } + } + } + + if (!found) { + RootShell.log("Trying third method"); + + try { + List paths = RootShell.getPath(); + + if (paths != null) { + for (String path : paths) { + if (RootShell.exists(path + "/" + binaryName)) { + RootShell.log(binaryName + " was found here: " + path); + list.add(path); + } else { + RootShell.log(binaryName + " was NOT found here: " + path); + } + } + } + } catch (Exception e) { + RootShell.log(binaryName + " was not found, more information MAY be available with Debugging on."); + } + } + + Collections.reverse(list); + + return list; + } + + /** + * This will open or return, if one is already open, a custom shell, you are responsible for managing the shell, reading the output + * and for closing the shell when you are done using it. + * + * @param shellPath a String to Indicate the path to the shell that you want to open. + * @param timeout an int to Indicate the length of time before giving up on opening a shell. + * @throws TimeoutException + * @throws com.stericson.RootShell.exceptions.RootDeniedException + * @throws IOException + */ + public static Shell getCustomShell(String shellPath, int timeout) throws IOException, TimeoutException, RootDeniedException + { + return RootShell.getCustomShell(shellPath, timeout); + } + + /** + * This will return the environment variable PATH + * + * @return List A List of Strings representing the environment variable $PATH + */ + public static List getPath() { + return Arrays.asList(System.getenv("PATH").split(":")); + } + + /** + * This will open or return, if one is already open, a shell, you are responsible for managing the shell, reading the output + * and for closing the shell when you are done using it. + * + * @param root a boolean to Indicate whether or not you want to open a root shell or a standard shell + * @param timeout an int to Indicate the length of time to wait before giving up on opening a shell. + * @param shellContext the context to execute the shell with + * @param retry a int to indicate how many times the ROOT shell should try to open with root priviliges... + */ + public static Shell getShell(boolean root, int timeout, Shell.ShellContext shellContext, int retry) throws IOException, TimeoutException, RootDeniedException { + if (root) { + return Shell.startRootShell(timeout, shellContext, retry); + } else { + return Shell.startShell(timeout); + } + } + + /** + * This will open or return, if one is already open, a shell, you are responsible for managing the shell, reading the output + * and for closing the shell when you are done using it. + * + * @param root a boolean to Indicate whether or not you want to open a root shell or a standard shell + * @param timeout an int to Indicate the length of time to wait before giving up on opening a shell. + * @param shellContext the context to execute the shell with + */ + public static Shell getShell(boolean root, int timeout, Shell.ShellContext shellContext) throws IOException, TimeoutException, RootDeniedException { + return getShell(root, timeout, shellContext, 3); + } + + /** + * This will open or return, if one is already open, a shell, you are responsible for managing the shell, reading the output + * and for closing the shell when you are done using it. + * + * @param root a boolean to Indicate whether or not you want to open a root shell or a standard shell + * @param shellContext the context to execute the shell with + */ + public static Shell getShell(boolean root, Shell.ShellContext shellContext) throws IOException, TimeoutException, RootDeniedException { + return getShell(root, 0, shellContext, 3); + } + + /** + * This will open or return, if one is already open, a shell, you are responsible for managing the shell, reading the output + * and for closing the shell when you are done using it. + * + * @param root a boolean to Indicate whether or not you want to open a root shell or a standard shell + * @param timeout an int to Indicate the length of time to wait before giving up on opening a shell. + */ + public static Shell getShell(boolean root, int timeout) throws IOException, TimeoutException, RootDeniedException { + return getShell(root, timeout, Shell.defaultContext, 3); + } + + /** + * This will open or return, if one is already open, a shell, you are responsible for managing the shell, reading the output + * and for closing the shell when you are done using it. + * + * @param root a boolean to Indicate whether or not you want to open a root shell or a standard shell + */ + public static Shell getShell(boolean root) throws IOException, TimeoutException, RootDeniedException { + return RootShell.getShell(root, 0); + } + + /** + * @return true if your app has been given root access. + * @throws TimeoutException if this operation times out. (cannot determine if access is given) + */ + public static boolean isAccessGiven() { + final Set ID = new HashSet(); + final int IAG = 158; + + try { + RootShell.log("Checking for Root access"); + + Command command = new Command(IAG, false, "id") { + @Override + public void commandOutput(int id, String line) { + if (id == IAG) { + ID.addAll(Arrays.asList(line.split(" "))); + } + + super.commandOutput(id, line); + } + }; + + Shell.startRootShell().add(command); + commandWait(Shell.startRootShell(), command); + + //parse the userid + for (String userid : ID) { + RootShell.log(userid); + + if (userid.toLowerCase().contains("uid=0")) { + RootShell.log("Access Given"); + return true; + } + } + + return false; + } catch (Exception e) { + e.printStackTrace(); + return false; + } + } + + /** + * @return true if BusyBox was found. + */ + public static boolean isBusyboxAvailable() + { + return (findBinary("busybox")).size() > 0; + } + + /** + * @return true if su was found. + */ + public static boolean isRootAvailable() { + return (findBinary("su")).size() > 0; + } + + /** + * This method allows you to output debug messages only when debugging is on. This will allow + * you to add a debug option to your app, which by default can be left off for performance. + * However, when you need debugging information, a simple switch can enable it and provide you + * with detailed logging. + *

+ * This method handles whether or not to log the information you pass it depending whether or + * not RootShell.debugMode is on. So you can use this and not have to worry about handling it + * yourself. + * + * @param msg The message to output. + */ + public static void log(String msg) { + log(null, msg, LogLevel.DEBUG, null); + } + + /** + * This method allows you to output debug messages only when debugging is on. This will allow + * you to add a debug option to your app, which by default can be left off for performance. + * However, when you need debugging information, a simple switch can enable it and provide you + * with detailed logging. + *

+ * This method handles whether or not to log the information you pass it depending whether or + * not RootShell.debugMode is on. So you can use this and not have to worry about handling it + * yourself. + * + * @param TAG Optional parameter to define the tag that the Log will use. + * @param msg The message to output. + */ + public static void log(String TAG, String msg) { + log(TAG, msg, LogLevel.DEBUG, null); + } + + /** + * This method allows you to output debug messages only when debugging is on. This will allow + * you to add a debug option to your app, which by default can be left off for performance. + * However, when you need debugging information, a simple switch can enable it and provide you + * with detailed logging. + *

+ * This method handles whether or not to log the information you pass it depending whether or + * not RootShell.debugMode is on. So you can use this and not have to worry about handling it + * yourself. + * + * @param msg The message to output. + * @param type The type of log, 1 for verbose, 2 for error, 3 for debug, 4 for warn + * @param e The exception that was thrown (Needed for errors) + */ + public static void log(String msg, LogLevel type, Exception e) { + log(null, msg, type, e); + } + + /** + * This method allows you to check whether logging is enabled. + * Yes, it has a goofy name, but that's to keep it as short as possible. + * After all writing logging calls should be painless. + * This method exists to save Android going through the various Java layers + * that are traversed any time a string is created (i.e. what you are logging) + *

+ * Example usage: + * if(islog) { + * StrinbBuilder sb = new StringBuilder(); + * // ... + * // build string + * // ... + * log(sb.toString()); + * } + * + * @return true if logging is enabled + */ + public static boolean islog() { + return debugMode; + } + + /** + * This method allows you to output debug messages only when debugging is on. This will allow + * you to add a debug option to your app, which by default can be left off for performance. + * However, when you need debugging information, a simple switch can enable it and provide you + * with detailed logging. + *

+ * This method handles whether or not to log the information you pass it depending whether or + * not RootShell.debugMode is on. So you can use this and not have to worry about handling it + * yourself. + * + * @param TAG Optional parameter to define the tag that the Log will use. + * @param msg The message to output. + * @param type The type of log, 1 for verbose, 2 for error, 3 for debug + * @param e The exception that was thrown (Needed for errors) + */ + public static void log(String TAG, String msg, LogLevel type, Exception e) { + if (msg != null && !msg.equals("")) { + if (debugMode) { + if (TAG == null) { + TAG = version; + } + + switch (type) { + case VERBOSE: + Log.v(TAG, msg); + break; + case ERROR: + Log.e(TAG, msg, e); + break; + case DEBUG: + Log.d(TAG, msg); + break; + case WARN: + Log.w(TAG, msg); + break; + } + } + } + } + + // -------------------- + // # Public Methods # + // -------------------- + + private static void commandWait(Shell shell, Command cmd) throws Exception { + while (!cmd.isFinished()) { + + RootShell.log(version, shell.getCommandQueuePositionString(cmd)); + + synchronized (cmd) { + try { + if (!cmd.isFinished()) { + cmd.wait(2000); + } + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + if (!cmd.isExecuting() && !cmd.isFinished()) { + if (!shell.isExecuting && !shell.isReading) { + RootShell.log(version, "Waiting for a command to be executed in a shell that is not executing and not reading! \n\n Command: " + cmd.getCommand()); + Exception e = new Exception(); + e.setStackTrace(Thread.currentThread().getStackTrace()); + e.printStackTrace(); + } else if (shell.isExecuting && !shell.isReading) { + RootShell.log(version, "Waiting for a command to be executed in a shell that is executing but not reading! \n\n Command: " + cmd.getCommand()); + Exception e = new Exception(); + e.setStackTrace(Thread.currentThread().getStackTrace()); + e.printStackTrace(); + } else { + RootShell.log(version, "Waiting for a command to be executed in a shell that is not reading! \n\n Command: " + cmd.getCommand()); + Exception e = new Exception(); + e.setStackTrace(Thread.currentThread().getStackTrace()); + e.printStackTrace(); + } + } + + } + } +} diff --git a/src/com/stericson/RootShell/containers/RootClass.java b/src/com/stericson/RootShell/containers/RootClass.java new file mode 100644 index 0000000..95002d6 --- /dev/null +++ b/src/com/stericson/RootShell/containers/RootClass.java @@ -0,0 +1,332 @@ +package com.stericson.RootShell.containers; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileFilter; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.FilenameFilter; +import java.io.IOException; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/* #ANNOTATIONS @SupportedAnnotationTypes("com.stericson.RootShell.containers.RootClass.Candidate") */ +/* #ANNOTATIONS @SupportedSourceVersion(SourceVersion.RELEASE_6) */ +public class RootClass /* #ANNOTATIONS extends AbstractProcessor */ { + + /* #ANNOTATIONS + @Override + public boolean process(Set typeElements, RoundEnvironment roundEnvironment) { + processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, "I was invoked!!!"); + + return false; + } + */ + + static String PATH_TO_DX = "/Users/Chris/Projects/android-sdk-macosx/build-tools/18.0.1/dx"; + + enum READ_STATE { + STARTING, FOUND_ANNOTATION; + } + + ; + + public RootClass(String[] args) throws ClassNotFoundException, NoSuchMethodException, + IllegalAccessException, InvocationTargetException, InstantiationException { + + // Note: rather than calling System.load("/system/lib/libandroid_runtime.so"); + // which would leave a bunch of unresolved JNI references, + // we are using the 'withFramework' class as a preloader. + // So, yeah, russian dolls: withFramework > RootClass > actual method + + String className = args[0]; + RootArgs actualArgs = new RootArgs(); + actualArgs.args = new String[args.length - 1]; + System.arraycopy(args, 1, actualArgs.args, 0, args.length - 1); + Class classHandler = Class.forName(className); + Constructor classConstructor = classHandler.getConstructor(RootArgs.class); + classConstructor.newInstance(actualArgs); + } + + public @interface Candidate { + + } + + ; + + public class RootArgs { + + public String args[]; + } + + static void displayError(Exception e) { + // Not using system.err to make it easier to capture from + // calling library. + System.out.println("##ERR##" + e.getMessage() + "##"); + e.printStackTrace(); + } + + // I reckon it would be better to investigate classes using getAttribute() + // however this method allows the developer to simply select "Run" on RootClass + // and immediately re-generate the necessary jar file. + static public class AnnotationsFinder { + + private final String AVOIDDIRPATH = "stericson" + File.separator + "RootTools" + File.separator; + + private List classFiles; + + public AnnotationsFinder() throws IOException { + System.out.println("Discovering root class annotations..."); + classFiles = new ArrayList(); + lookup(new File("src"), classFiles); + System.out.println("Done discovering annotations. Building jar file."); + File builtPath = getBuiltPath(); + if (null != builtPath) { + // Android! Y U no have com.google.common.base.Joiner class? + String rc1 = "com" + File.separator + + "stericson" + File.separator + + "RootShell" + File.separator + + "containers" + File.separator + + "RootClass.class"; + String rc2 = "com" + File.separator + + "stericson" + File.separator + + "RootShell" + File.separator + + "containers" + File.separator + + "RootClass$RootArgs.class"; + String rc3 = "com" + File.separator + + "stericson" + File.separator + + "RootShell" + File.separator + + "containers" + File.separator + + "RootClass$AnnotationsFinder.class"; + String rc4 = "com" + File.separator + + "stericson" + File.separator + + "RootShell" + File.separator + + "containers" + File.separator + + "RootClass$AnnotationsFinder$1.class"; + String rc5 = "com" + File.separator + + "stericson" + File.separator + + "RootShell" + File.separator + + "containers" + File.separator + + "RootClass$AnnotationsFinder$2.class"; + String[] cmd; + boolean onWindows = (-1 != System.getProperty("os.name").toLowerCase().indexOf("win")); + if (onWindows) { + StringBuilder sb = new StringBuilder( + " " + rc1 + " " + rc2 + " " + rc3 + " " + rc4 + " " + rc5 + ); + for (File file : classFiles) { + sb.append(" " + file.getPath()); + } + cmd = new String[]{ + "cmd", "/C", + "jar cvf" + + " anbuild.jar" + + sb.toString() + }; + } else { + ArrayList al = new ArrayList(); + al.add("jar"); + al.add("cf"); + al.add("anbuild.jar"); + al.add(rc1); + al.add(rc2); + al.add(rc3); + al.add(rc4); + al.add(rc5); + for (File file : classFiles) { + al.add(file.getPath()); + } + cmd = al.toArray(new String[al.size()]); + } + ProcessBuilder jarBuilder = new ProcessBuilder(cmd); + jarBuilder.directory(builtPath); + try { + jarBuilder.start().waitFor(); + } catch (IOException e) { + } catch (InterruptedException e) { + } + + File rawFolder = new File("res/raw"); + if (!rawFolder.exists()) { + rawFolder.mkdirs(); + } + + System.out.println("Done building jar file. Creating dex file."); + if (onWindows) { + cmd = new String[]{ + "cmd", "/C", + "dx --dex --output=res/raw/anbuild.dex " + + builtPath + File.separator + "anbuild.jar" + }; + } else { + cmd = new String[]{ + getPathToDx(), + "--dex", + "--output=res/raw/anbuild.dex", + builtPath + File.separator + "anbuild.jar" + }; + } + ProcessBuilder dexBuilder = new ProcessBuilder(cmd); + try { + dexBuilder.start().waitFor(); + } catch (IOException e) { + } catch (InterruptedException e) { + } + } + System.out.println("All done. ::: anbuild.dex should now be in your project's res/raw/ folder :::"); + } + + protected void lookup(File path, List fileList) { + String desourcedPath = path.toString().replace("src/", ""); + File[] files = path.listFiles(); + for (File file : files) { + if (file.isDirectory()) { + if (-1 == file.getAbsolutePath().indexOf(AVOIDDIRPATH)) { + lookup(file, fileList); + } + } else { + if (file.getName().endsWith(".java")) { + if (hasClassAnnotation(file)) { + final String fileNamePrefix = file.getName().replace(".java", ""); + final File compiledPath = new File(getBuiltPath().toString() + File.separator + desourcedPath); + File[] classAndInnerClassFiles = compiledPath.listFiles(new FilenameFilter() { + @Override + public boolean accept(File dir, String filename) { + return filename.startsWith(fileNamePrefix); + } + }); + for (final File matchingFile : classAndInnerClassFiles) { + fileList.add(new File(desourcedPath + File.separator + matchingFile.getName())); + } + + } + } + } + } + } + + protected boolean hasClassAnnotation(File file) { + READ_STATE readState = READ_STATE.STARTING; + Pattern p = Pattern.compile(" class ([A-Za-z0-9_]+)"); + try { + BufferedReader reader = new BufferedReader(new FileReader(file)); + String line; + while (null != (line = reader.readLine())) { + switch (readState) { + case STARTING: + if (-1 < line.indexOf("@RootClass.Candidate")) { + readState = READ_STATE.FOUND_ANNOTATION; + } + break; + case FOUND_ANNOTATION: + Matcher m = p.matcher(line); + if (m.find()) { + System.out.println(" Found annotated class: " + m.group(0)); + return true; + } else { + System.err.println("Error: unmatched annotation in " + + file.getAbsolutePath()); + readState = READ_STATE.STARTING; + } + break; + } + } + } catch (FileNotFoundException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + return false; + } + + protected String getPathToDx() throws IOException { + String androidHome = System.getenv("ANDROID_HOME"); + if (null == androidHome) { + throw new IOException("Error: you need to set $ANDROID_HOME globally"); + } + String dxPath = null; + File[] files = new File(androidHome + File.separator + "build-tools").listFiles(); + int recentSdkVersion = 0; + for (File file : files) { + + String fileName = null; + if (file.getName().contains("-")) { + String[] splitFileName = file.getName().split("-"); + if (splitFileName[1].contains("W")) { + char[] fileNameChars = splitFileName[1].toCharArray(); + fileName = String.valueOf(fileNameChars[0]); + } else { + fileName = splitFileName[1]; + } + } else { + fileName = file.getName(); + } + + int sdkVersion; + + String[] sdkVersionBits = fileName.split("[.]"); + sdkVersion = Integer.parseInt(sdkVersionBits[0]) * 10000; + if (sdkVersionBits.length > 1) { + sdkVersion += Integer.parseInt(sdkVersionBits[1]) * 100; + if (sdkVersionBits.length > 2) { + sdkVersion += Integer.parseInt(sdkVersionBits[2]); + } + } + if (sdkVersion > recentSdkVersion) { + String tentativePath = file.getAbsolutePath() + File.separator + "dx"; + if (new File(tentativePath).exists()) { + recentSdkVersion = sdkVersion; + dxPath = tentativePath; + } + } + } + if (dxPath == null) { + throw new IOException("Error: unable to find dx binary in $ANDROID_HOME"); + } + return dxPath; + } + + protected File getBuiltPath() { + File foundPath = null; + + File ideaPath = new File("out" + File.separator + "production"); // IntelliJ + if (ideaPath.isDirectory()) { + File[] children = ideaPath.listFiles(new FileFilter() { + @Override + public boolean accept(File pathname) { + return pathname.isDirectory(); + } + }); + if (children.length > 0) { + foundPath = new File(ideaPath.getAbsolutePath() + File.separator + children[0].getName()); + } + } + if (null == foundPath) { + File eclipsePath = new File("bin" + File.separator + "classes"); // Eclipse IDE + if (eclipsePath.isDirectory()) { + foundPath = eclipsePath; + } + } + + return foundPath; + } + + + } + + public static void main(String[] args) { + try { + if (args.length == 0) { + new AnnotationsFinder(); + } else { + new RootClass(args); + } + } catch (Exception e) { + displayError(e); + } + } +} diff --git a/src/com/stericson/RootShell/exceptions/RootDeniedException.java b/src/com/stericson/RootShell/exceptions/RootDeniedException.java new file mode 100644 index 0000000..f2c35cd --- /dev/null +++ b/src/com/stericson/RootShell/exceptions/RootDeniedException.java @@ -0,0 +1,32 @@ +/* + * This file is part of the RootShell Project: http://code.google.com/p/roottools/ + * + * Copyright (c) 2014 Stephen Erickson, Chris Ravenscroft + * + * This code is dual-licensed under the terms of the Apache License Version 2.0 and + * the terms of the General Public License (GPL) Version 2. + * You may use this code according to either of these licenses as is most appropriate + * for your project on a case-by-case basis. + * + * The terms of each license can be found in the root directory of this project's repository as well as at: + * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * http://www.gnu.org/licenses/gpl-2.0.txt + * + * Unless required by applicable law or agreed to in writing, software + * distributed under these Licenses is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See each License for the specific language governing permissions and + * limitations under that License. + */ + +package com.stericson.RootShell.exceptions; + +public class RootDeniedException extends Exception { + + private static final long serialVersionUID = -8713947214162841310L; + + public RootDeniedException(String error) { + super(error); + } +} diff --git a/src/com/stericson/RootShell/execution/Command.java b/src/com/stericson/RootShell/execution/Command.java new file mode 100644 index 0000000..343c3d8 --- /dev/null +++ b/src/com/stericson/RootShell/execution/Command.java @@ -0,0 +1,334 @@ +/* + * This file is part of the RootShell Project: http://code.google.com/p/RootShell/ + * + * Copyright (c) 2014 Stephen Erickson, Chris Ravenscroft + * + * This code is dual-licensed under the terms of the Apache License Version 2.0 and + * the terms of the General Public License (GPL) Version 2. + * You may use this code according to either of these licenses as is most appropriate + * for your project on a case-by-case basis. + * + * The terms of each license can be found in the root directory of this project's repository as well as at: + * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * http://www.gnu.org/licenses/gpl-2.0.txt + * + * Unless required by applicable law or agreed to in writing, software + * distributed under these Licenses is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See each License for the specific language governing permissions and + * limitations under that License. + */ + +package com.stericson.RootShell.execution; + +import com.stericson.RootShell.RootShell; + +import android.content.Context; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; + +import java.io.IOException; + +public class Command { + + public int totalOutput = 0; + + public int totalOutputProcessed = 0; + + ExecutionMonitor executionMonitor = null; + + Handler mHandler = null; + + boolean executing = false; + + String[] command = {}; + + boolean javaCommand = false; + + Context context = null; + + boolean finished = false; + + boolean terminated = false; + + boolean handlerEnabled = true; + + int exitCode = -1; + + int id = 0; + + int timeout = RootShell.defaultCommandTimeout; + + /** + * Constructor for executing a normal shell command + * + * @param id the id of the command being executed + * @param command the command, or commands, to be executed. + */ + public Command(int id, String... command) { + this.command = command; + this.id = id; + + createHandler(RootShell.handlerEnabled); + } + + /** + * Constructor for executing a normal shell command + * + * @param id the id of the command being executed + * @param handlerEnabled when true the handler will be used to call the + * callback methods if possible. + * @param command the command, or commands, to be executed. + */ + public Command(int id, boolean handlerEnabled, String... command) { + this.command = command; + this.id = id; + + createHandler(handlerEnabled); + } + + /** + * Constructor for executing a normal shell command + * + * @param id the id of the command being executed + * @param timeout the time allowed before the shell will give up executing the command + * and throw a TimeoutException. + * @param command the command, or commands, to be executed. + */ + public Command(int id, int timeout, String... command) { + this.command = command; + this.id = id; + this.timeout = timeout; + + createHandler(RootShell.handlerEnabled); + } + + /** + * Constructor for executing Java commands rather than binaries + * + * @param javaCommand when True, it is a java command. + * @param context needed to execute java command. + */ + public Command(int id, boolean javaCommand, Context context, String... command) { + this(id, command); + this.javaCommand = javaCommand; + this.context = context; + } + + /** + * Constructor for executing Java commands rather than binaries + * + * @param javaCommand when True, it is a java command. + * @param context needed to execute java command. + */ + public Command(int id, boolean handlerEnabled, boolean javaCommand, Context context, String... command) { + this(id, handlerEnabled, command); + this.javaCommand = javaCommand; + this.context = context; + } + + /** + * Constructor for executing Java commands rather than binaries + * + * @param javaCommand when True, it is a java command. + * @param context needed to execute java command. + */ + public Command(int id, int timeout, boolean javaCommand, Context context, String... command) { + this(id, timeout, command); + this.javaCommand = javaCommand; + this.context = context; + } + + //If you override this you MUST make a final call + //to the super method. The super call should be the last line of this method. + public void commandOutput(int id, String line) { + RootShell.log("Command", "ID: " + id + ", " + line); + totalOutputProcessed++; + } + + public void commandTerminated(int id, String reason) { + //pass + } + + public void commandCompleted(int id, int exitcode) { + //pass + } + + protected final void finishCommand() { + executing = false; + finished = true; + this.notifyAll(); + } + + protected final void commandFinished() { + if (!terminated) { + synchronized (this) { + if (mHandler != null && handlerEnabled) { + Message msg = mHandler.obtainMessage(); + Bundle bundle = new Bundle(); + bundle.putInt(CommandHandler.ACTION, CommandHandler.COMMAND_COMPLETED); + msg.setData(bundle); + mHandler.sendMessage(msg); + } else { + commandCompleted(id, exitCode); + } + + RootShell.log("Command " + id + " finished."); + finishCommand(); + } + } + } + + private void createHandler(boolean handlerEnabled) { + + this.handlerEnabled = handlerEnabled; + + if (Looper.myLooper() != null && handlerEnabled) { + RootShell.log("CommandHandler created"); + mHandler = new CommandHandler(); + } else { + RootShell.log("CommandHandler not created"); + } + } + + public final String getCommand() { + StringBuilder sb = new StringBuilder(); + + for (int i = 0; i < command.length; i++) { + if (i > 0) { + sb.append('\n'); + } + + sb.append(command[i]); + } + + return sb.toString(); + } + + public final boolean isExecuting() { + return executing; + } + + public final boolean isHandlerEnabled() { + return handlerEnabled; + } + + public final boolean isFinished() { + return finished; + } + + public final int getExitCode() { + return this.exitCode; + } + + protected final void setExitCode(int code) { + synchronized (this) { + exitCode = code; + } + } + + protected final void startExecution() { + executionMonitor = new ExecutionMonitor(); + executionMonitor.setPriority(Thread.MIN_PRIORITY); + executionMonitor.start(); + executing = true; + } + + public final void terminate(String reason) { + try { + Shell.closeAll(); + RootShell.log("Terminating all shells."); + terminated(reason); + } catch (IOException e) { + } + } + + protected final void terminated(String reason) { + synchronized (Command.this) { + + if (mHandler != null && handlerEnabled) { + Message msg = mHandler.obtainMessage(); + Bundle bundle = new Bundle(); + bundle.putInt(CommandHandler.ACTION, CommandHandler.COMMAND_TERMINATED); + bundle.putString(CommandHandler.TEXT, reason); + msg.setData(bundle); + mHandler.sendMessage(msg); + } else { + commandTerminated(id, reason); + } + + RootShell.log("Command " + id + " did not finish because it was terminated. Termination reason: " + reason); + setExitCode(-1); + terminated = true; + finishCommand(); + } + } + + protected final void output(int id, String line) { + totalOutput++; + + if (mHandler != null && handlerEnabled) { + Message msg = mHandler.obtainMessage(); + Bundle bundle = new Bundle(); + bundle.putInt(CommandHandler.ACTION, CommandHandler.COMMAND_OUTPUT); + bundle.putString(CommandHandler.TEXT, line); + msg.setData(bundle); + mHandler.sendMessage(msg); + } else { + commandOutput(id, line); + } + } + + private class ExecutionMonitor extends Thread { + + public void run() { + while (!finished) { + + synchronized (Command.this) { + try { + Command.this.wait(timeout); + } catch (InterruptedException e) { + } + } + + if (!finished) { + RootShell.log("Timeout Exception has occurred."); + terminate("Timeout Exception"); + } + } + } + } + + private class CommandHandler extends Handler { + + static final public String ACTION = "action"; + + static final public String TEXT = "text"; + + static final public int COMMAND_OUTPUT = 0x01; + + static final public int COMMAND_COMPLETED = 0x02; + + static final public int COMMAND_TERMINATED = 0x03; + + public final void handleMessage(Message msg) { + int action = msg.getData().getInt(ACTION); + String text = msg.getData().getString(TEXT); + + switch (action) { + case COMMAND_OUTPUT: + commandOutput(id, text); + break; + case COMMAND_COMPLETED: + commandCompleted(id, exitCode); + break; + case COMMAND_TERMINATED: + commandTerminated(id, text); + break; + } + } + } +} diff --git a/src/com/stericson/RootShell/execution/JavaCommand.java b/src/com/stericson/RootShell/execution/JavaCommand.java new file mode 100644 index 0000000..f175436 --- /dev/null +++ b/src/com/stericson/RootShell/execution/JavaCommand.java @@ -0,0 +1,39 @@ +package com.stericson.RootShell.execution; + +import android.content.Context; + +public class JavaCommand extends Command +{ + public JavaCommand(int id, Context context, String... command) + { + super(id, true, context, command); + } + + public JavaCommand(int id, boolean handlerEnabled, Context context, String... command) + { + super(id, handlerEnabled, true, context, command); + } + + public JavaCommand(int id, int timeout, Context context, String... command) + { + super(id, timeout, true, context, command); + } + + @Override + public void commandOutput(int id, String line) + { + super.commandOutput(id, line); + } + + @Override + public void commandTerminated(int id, String reason) + { + // pass + } + + @Override + public void commandCompleted(int id, int exitCode) + { + // pass + } +} diff --git a/src/com/stericson/RootShell/execution/Shell.java b/src/com/stericson/RootShell/execution/Shell.java new file mode 100644 index 0000000..2de0a27 --- /dev/null +++ b/src/com/stericson/RootShell/execution/Shell.java @@ -0,0 +1,1015 @@ +/* + * This file is part of the RootShell Project: http://code.google.com/p/RootShell/ + * + * Copyright (c) 2014 Stephen Erickson, Chris Ravenscroft + * + * This code is dual-licensed under the terms of the Apache License Version 2.0 and + * the terms of the General Public License (GPL) Version 2. + * You may use this code according to either of these licenses as is most appropriate + * for your project on a case-by-case basis. + * + * The terms of each license can be found in the root directory of this project's repository as well as at: + * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * http://www.gnu.org/licenses/gpl-2.0.txt + * + * Unless required by applicable law or agreed to in writing, software + * distributed under these Licenses is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See each License for the specific language governing permissions and + * limitations under that License. + */ +package com.stericson.RootShell.execution; + +import com.stericson.RootShell.RootShell; +import com.stericson.RootShell.exceptions.RootDeniedException; + +import android.content.Context; +import android.util.Log; + +import java.io.BufferedReader; +import java.io.EOFException; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.io.Reader; +import java.io.Writer; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeoutException; + +public class Shell { + + public static enum ShellType { + NORMAL, + ROOT, + CUSTOM + } + + //this is only used with root shells + public static enum ShellContext { + NORMAL("normal"), //The normal context... + SHELL("u:r:shell:s0"), //Unpriviliged shell (such as an adb shell) + SYSTEM_SERVER("u:r:system_server:s0"), // system_server, u:r:system:s0 on some firmwares + SYSTEM_APP("u:r:system_app:s0"), // System apps + PLATFORM_APP("u:r:platform_app:s0"), // System apps + UNTRUSTED_APP("u:r:untrusted_app:s0"), // Third-party apps + RECOVERY("u:r:recovery:s0"); //Recovery + + private String value; + + private ShellContext(String value) { + this.value = value; + } + + public String getValue() { + return this.value; + } + + } + + //Statics -- visible to all + private static final String token = "F*D^W@#FGF"; + + private static Shell rootShell = null; + + private static Shell shell = null; + + private static Shell customShell = null; + + private static String[] suVersion = new String[]{ + null, null + }; + + //the default context for root shells... + public static ShellContext defaultContext = ShellContext.NORMAL; + + //per shell + private int shellTimeout = 25000; + + private ShellType shellType = null; + + private ShellContext shellContext = Shell.ShellContext.NORMAL; + + private String error = ""; + + private final Process proc; + + private final BufferedReader inputStream; + + private final BufferedReader errorStream; + + private final OutputStreamWriter outputStream; + + private final List commands = new ArrayList(); + + //indicates whether or not to close the shell + private boolean close = false; + + private Boolean isSELinuxEnforcing = null; + + public boolean isExecuting = false; + + public boolean isReading = false; + + public boolean isClosed = false; + + private int maxCommands = 5000; + + private int read = 0; + + private int write = 0; + + private int totalExecuted = 0; + + private int totalRead = 0; + + private boolean isCleaning = false; + + private Shell(String cmd, ShellType shellType, ShellContext shellContext, int shellTimeout) throws IOException, TimeoutException, RootDeniedException { + + RootShell.log("Starting shell: " + cmd); + RootShell.log("Context: " + shellContext.getValue()); + RootShell.log("Timeout: " + shellTimeout); + + this.shellType = shellType; + this.shellTimeout = shellTimeout > 0 ? shellTimeout : this.shellTimeout; + this.shellContext = shellContext; + + if (this.shellContext == ShellContext.NORMAL) { + this.proc = Runtime.getRuntime().exec(cmd); + } else { + String display = getSuVersion(false); + String internal = getSuVersion(true); + + //only done for root shell... + //Right now only SUPERSU supports the --context switch + if (isSELinuxEnforcing() && + (display != null) && + (internal != null) && + (display.endsWith("SUPERSU")) && + (Integer.valueOf(internal) >= 190)) { + cmd += " --context " + this.shellContext.getValue(); + } else { + RootShell.log("Su binary --context switch not supported!"); + RootShell.log("Su binary display version: " + display); + RootShell.log("Su binary internal version: " + internal); + RootShell.log("SELinuxEnforcing: " + isSELinuxEnforcing()); + } + + this.proc = Runtime.getRuntime().exec(cmd); + + } + + this.inputStream = new BufferedReader(new InputStreamReader(this.proc.getInputStream(), "UTF-8")); + this.errorStream = new BufferedReader(new InputStreamReader(this.proc.getErrorStream(), "UTF-8")); + this.outputStream = new OutputStreamWriter(this.proc.getOutputStream(), "UTF-8"); + + /** + * Thread responsible for carrying out the requested operations + */ + Worker worker = new Worker(this); + worker.start(); + + try { + /** + * The flow of execution will wait for the thread to die or wait until the + * given timeout has expired. + * + * The result of the worker, which is determined by the exit code of the worker, + * will tell us if the operation was completed successfully or it the operation + * failed. + */ + worker.join(this.shellTimeout); + + /** + * The operation could not be completed before the timeout occured. + */ + if (worker.exit == -911) { + + try { + this.proc.destroy(); + } catch (Exception e) { + } + + closeQuietly(this.inputStream); + closeQuietly(this.errorStream); + closeQuietly(this.outputStream); + + throw new TimeoutException(this.error); + } + /** + * Root access denied? + */ + else if (worker.exit == -42) { + + try { + this.proc.destroy(); + } catch (Exception e) { + } + + closeQuietly(this.inputStream); + closeQuietly(this.errorStream); + closeQuietly(this.outputStream); + + throw new RootDeniedException("Root Access Denied"); + } + /** + * Normal exit + */ + else { + /** + * The shell is open. + * + * Start two threads, one to handle the input and one to handle the output. + * + * input, and output are runnables that the threads execute. + */ + Thread si = new Thread(this.input, "Shell Input"); + si.setPriority(Thread.NORM_PRIORITY); + si.start(); + + Thread so = new Thread(this.output, "Shell Output"); + so.setPriority(Thread.NORM_PRIORITY); + so.start(); + } + } catch (InterruptedException ex) { + worker.interrupt(); + Thread.currentThread().interrupt(); + throw new TimeoutException(); + } + } + + + public Command add(Command command) throws IOException { + if (this.close) { + throw new IllegalStateException( + "Unable to add commands to a closed shell"); + } + + while (this.isCleaning) { + //Don't add commands while cleaning + ; + } + this.commands.add(command); + + this.notifyThreads(); + + return command; + } + + public final void useCWD(Context context) throws IOException, TimeoutException, RootDeniedException { + add( + new Command( + -1, + false, + "cd " + context.getApplicationInfo().dataDir) + ); + } + + private void cleanCommands() { + this.isCleaning = true; + int toClean = Math.abs(this.maxCommands - (this.maxCommands / 4)); + RootShell.log("Cleaning up: " + toClean); + + for (int i = 0; i < toClean; i++) { + this.commands.remove(0); + } + + this.read = this.commands.size() - 1; + this.write = this.commands.size() - 1; + this.isCleaning = false; + } + + private void closeQuietly(final Reader input) { + try { + if (input != null) { + input.close(); + } + } catch (Exception ignore) { + } + } + + private void closeQuietly(final Writer output) { + try { + if (output != null) { + output.close(); + } + } catch (Exception ignore) { + } + } + + public void close() throws IOException { + RootShell.log("Request to close shell!"); + + int count = 0; + while (isExecuting) { + RootShell.log("Waiting on shell to finish executing before closing..."); + count++; + + //fail safe + if (count > 10000) { + break; + } + + } + + synchronized (this.commands) { + /** + * instruct the two threads monitoring input and output + * of the shell to close. + */ + this.close = true; + this.notifyThreads(); + } + + RootShell.log("Shell Closed!"); + + if (this == Shell.rootShell) { + Shell.rootShell = null; + } else if (this == Shell.shell) { + Shell.shell = null; + } else if (this == Shell.customShell) { + Shell.customShell = null; + } + } + + public static void closeCustomShell() throws IOException { + RootShell.log("Request to close custom shell!"); + + if (Shell.customShell == null) { + return; + } + + Shell.customShell.close(); + } + + public static void closeRootShell() throws IOException { + RootShell.log("Request to close root shell!"); + + if (Shell.rootShell == null) { + return; + } + Shell.rootShell.close(); + } + + public static void closeShell() throws IOException { + RootShell.log("Request to close normal shell!"); + + if (Shell.shell == null) { + return; + } + Shell.shell.close(); + } + + public static void closeAll() throws IOException { + RootShell.log("Request to close all shells!"); + + Shell.closeShell(); + Shell.closeRootShell(); + Shell.closeCustomShell(); + } + + public int getCommandQueuePosition(Command cmd) { + return this.commands.indexOf(cmd); + } + + public String getCommandQueuePositionString(Command cmd) { + return "Command is in position " + getCommandQueuePosition(cmd) + " currently executing command at position " + this.write + " and the number of commands is " + commands.size(); + } + + public static Shell getOpenShell() { + if (Shell.customShell != null) { + return Shell.customShell; + } else if (Shell.rootShell != null) { + return Shell.rootShell; + } else { + return Shell.shell; + } + } + + /** + * From libsuperuser. + * + *

+ * 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. + *

+ *

+ * This function caches its result to improve performance on multiple + * calls + *

+ * + * @param internal Request human-readable version or application + * internal version + * @return String containing the su version or null + */ + private synchronized String getSuVersion(boolean internal) { + int idx = internal ? 0 : 1; + if (suVersion[idx] == null) { + String version = null; + + // Replace libsuperuser:Shell.run with manual process execution + Process process; + try { + process = Runtime.getRuntime().exec(internal ? "su -V" : "su -v", null); + process.waitFor(); + } catch (IOException e) { + e.printStackTrace(); + return null; + } catch (InterruptedException e) { + e.printStackTrace(); + return null; + } + + // From libsuperuser:StreamGobbler + List stdout = new ArrayList(); + + BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream())); + try { + String line = null; + while ((line = reader.readLine()) != null) { + stdout.add(line); + } + } catch (IOException e) { + } + // make sure our stream is closed and resources will be freed + try { + reader.close(); + } catch (IOException e) { + } + + process.destroy(); + + List ret = stdout; + + if (ret != null) { + for (String line : ret) { + if (!internal) { + if (line.contains(".")) { + version = line; + break; + } + } else { + try { + if (Integer.parseInt(line) > 0) { + version = line; + break; + } + } catch (NumberFormatException e) { + } + } + } + } + + suVersion[idx] = version; + } + return suVersion[idx]; + } + + public static boolean isShellOpen() { + return Shell.shell == null; + } + + public static boolean isCustomShellOpen() { + return Shell.customShell == null; + } + + public static boolean isRootShellOpen() { + return Shell.rootShell == null; + } + + public static boolean isAnyShellOpen() { + return Shell.shell != null || Shell.rootShell != null || Shell.customShell != null; + } + + /** + * From libsuperuser. + * + * Detect if SELinux is set to enforcing, caches result + * + * @return true if SELinux set to enforcing, or false in the case of + * permissive or not present + */ + public synchronized boolean isSELinuxEnforcing() { + if (isSELinuxEnforcing == null) { + Boolean enforcing = null; + + // First known firmware with SELinux built-in was a 4.2 (17) + // leak + if (android.os.Build.VERSION.SDK_INT >= 17) { + + // Detect enforcing through sysfs, not always present + File f = new File("/sys/fs/selinux/enforce"); + if (f.exists()) { + try { + InputStream is = new FileInputStream("/sys/fs/selinux/enforce"); + try { + enforcing = (is.read() == '1'); + } finally { + is.close(); + } + } catch (Exception e) { + } + } + + // 4.4+ builds are enforcing by default, take the gamble + if (enforcing == null) { + enforcing = (android.os.Build.VERSION.SDK_INT >= 19); + } + } + + if (enforcing == null) { + enforcing = false; + } + + isSELinuxEnforcing = enforcing; + } + return isSELinuxEnforcing; + } + + /** + * Runnable to write commands to the open shell. + *

+ * When writing commands we stay in a loop and wait for new + * commands to added to "commands" + *

+ * The notification of a new command is handled by the method add in this class + */ + private Runnable input = new Runnable() { + public void run() { + + try { + while (true) { + + synchronized (commands) { + /** + * While loop is used in the case that notifyAll is called + * and there are still no commands to be written, a rare + * case but one that could happen. + */ + while (!close && write >= commands.size()) { + isExecuting = false; + commands.wait(); + } + } + + if (write >= maxCommands) { + + /** + * wait for the read to catch up. + */ + while (read != write) { + RootShell.log("Waiting for read and write to catch up before cleanup."); + } + /** + * Clean up the commands, stay neat. + */ + cleanCommands(); + } + + /** + * Write the new command + * + * We write the command followed by the token to indicate + * the end of the command execution + */ + if (write < commands.size()) { + isExecuting = true; + Command cmd = commands.get(write); + cmd.startExecution(); + RootShell.log("Executing: " + cmd.getCommand() + " with context: " + shellContext); + + outputStream.write(cmd.getCommand()); + String line = "\necho " + token + " " + totalExecuted + " $?\n"; + outputStream.write(line); + outputStream.flush(); + write++; + totalExecuted++; + } else if (close) { + /** + * close the thread, the shell is closing. + */ + isExecuting = false; + outputStream.write("\nexit 0\n"); + outputStream.flush(); + RootShell.log("Closing shell"); + return; + } + } + } catch (IOException e) { + RootShell.log(e.getMessage(), RootShell.LogLevel.ERROR, e); + } catch (InterruptedException e) { + RootShell.log(e.getMessage(), RootShell.LogLevel.ERROR, e); + } finally { + write = 0; + closeQuietly(outputStream); + } + } + }; + + protected void notifyThreads() { + Thread t = new Thread() { + public void run() { + synchronized (commands) { + commands.notifyAll(); + } + } + }; + + t.start(); + } + + /** + * Runnable to monitor the responses from the open shell. + * + * This include the output and error stream + */ + private Runnable output = new Runnable() { + public void run() { + try { + Command command = null; + + //as long as there is something to read, we will keep reading. + while (!close || inputStream.ready() || read < commands.size()) { + isReading = false; + String outputLine = inputStream.readLine(); + isReading = true; + + /** + * If we recieve EOF then the shell closed? + */ + if (outputLine == null) { + break; + } + + if (command == null) { + if (read >= commands.size()) { + if (close) { + break; + } + + continue; + } + + command = commands.get(read); + } + + /** + * trying to determine if all commands have been completed. + * + * if the token is present then the command has finished execution. + */ + int pos = -1; + + pos = outputLine.indexOf(token); + + if (pos == -1) { + /** + * send the output for the implementer to process + */ + command.output(command.id, outputLine); + } else if (pos > 0) { + /** + * token is suffix of output, send output part to implementer + */ + command.output(command.id, outputLine.substring(0, pos)); + } + + if (pos >= 0) { + outputLine = outputLine.substring(pos); + String fields[] = outputLine.split(" "); + + if (fields.length >= 2 && fields[1] != null) { + int id = 0; + + try { + id = Integer.parseInt(fields[1]); + } catch (NumberFormatException e) { + } + + int exitCode = -1; + + try { + exitCode = Integer.parseInt(fields[2]); + } catch (NumberFormatException e) { + } + + if (id == totalRead) { + processErrors(command); + + + /** + * We will wait a bit for output to be processed... + * + * MAX, 10 iterations + */ + int iterations = 0; + while (command.totalOutput > command.totalOutputProcessed) { + + final int MAX_ITERATIONS = 10; + + if(iterations == 0) + { + RootShell.log("Waiting for output to be processed. " + command.totalOutputProcessed + " Of " + command.totalOutput); + } + else if (iterations > MAX_ITERATIONS) { + RootShell.log(RootShell.version, "All output not processed! Did you forget the super call for commandOutput???", RootShell.LogLevel.WARN, null); + RootShell.log(RootShell.version, command.totalOutputProcessed + " Of " + command.totalOutput + " processed", RootShell.LogLevel.WARN, null); + RootShell.log(RootShell.version, "This doesn't mean there is a problem, just that we couldn't confirm that all output was processed.", RootShell.LogLevel.WARN, null); + break; + } + + try { + iterations++; + this.wait(1000); + } catch (Exception e) { + } + } + + command.setExitCode(exitCode); + command.commandFinished(); + command = null; + + read++; + totalRead++; + continue; + } + } + } + } + + RootShell.log("Read all output"); + + try { + proc.waitFor(); + proc.destroy(); + } catch (Exception e) { + } + + while (read < commands.size()) { + if (command == null) { + command = commands.get(read); + } + + command.terminated("Unexpected Termination."); + command = null; + read++; + } + + read = 0; + + } catch (IOException e) { + RootShell.log(e.getMessage(), RootShell.LogLevel.ERROR, e); + } finally { + closeQuietly(outputStream); + closeQuietly(errorStream); + closeQuietly(inputStream); + + RootShell.log("Shell destroyed"); + isClosed = true; + isReading = false; + } + } + }; + + public void processErrors(Command command) { + try { + while (errorStream.ready() && command != null) { + String line = errorStream.readLine(); + + /** + * If we recieve EOF then the shell closed? + */ + if (line == null) { + break; + } + + /** + * send the output for the implementer to process + */ + command.output(command.id, line); + } + } catch (Exception e) { + RootShell.log(e.getMessage(), RootShell.LogLevel.ERROR, e); + } + } + + public static void runRootCommand(Command command) throws IOException, TimeoutException, RootDeniedException { + Shell.startRootShell().add(command); + } + + public static void runCommand(Command command) throws IOException, TimeoutException { + Shell.startShell().add(command); + } + + public static Shell startRootShell() throws IOException, TimeoutException, RootDeniedException { + return Shell.startRootShell(0, 3); + } + + public static Shell startRootShell(int timeout) throws IOException, TimeoutException, RootDeniedException { + return Shell.startRootShell(timeout, 3); + } + + public static Shell startRootShell(int timeout, int retry) throws IOException, TimeoutException, RootDeniedException { + return Shell.startRootShell(timeout, Shell.defaultContext, retry); + } + + public static Shell startRootShell(int timeout, ShellContext shellContext, int retry) throws IOException, TimeoutException, RootDeniedException { + // keep prompting the user until they accept for x amount of times... + int retries = 0; + + if (Shell.rootShell == null) { + + RootShell.log("Starting Root Shell!"); + String cmd = "su"; + while (Shell.rootShell == null) { + try { + RootShell.log("Trying to open Root Shell, attempt #" + retries); + Shell.rootShell = new Shell(cmd, ShellType.ROOT, shellContext, timeout); + } catch (IOException e) { + if (retries++ >= retry) { + RootShell.log("IOException, could not start shell"); + throw e; + } + } catch (RootDeniedException e) { + if (retries++ >= retry) { + RootShell.log("RootDeniedException, could not start shell"); + throw e; + } + } catch (TimeoutException e) { + if (retries++ >= retry) { + RootShell.log("TimeoutException, could not start shell"); + throw e; + } + } + } + } else if (Shell.rootShell.shellContext != shellContext) { + try { + RootShell.log("Context is different than open shell, switching context... " + Shell.rootShell.shellContext + " VS " + shellContext); + Shell.rootShell.switchRootShellContext(shellContext); + } catch (IOException e) { + if (retries++ >= retry) { + RootShell.log("IOException, could not switch context!"); + throw e; + } + } catch (RootDeniedException e) { + if (retries++ >= retry) { + RootShell.log("RootDeniedException, could not switch context!"); + throw e; + } + } catch (TimeoutException e) { + if (retries++ >= retry) { + RootShell.log("TimeoutException, could not switch context!"); + throw e; + } + } + } else { + RootShell.log("Using Existing Root Shell!"); + } + + return Shell.rootShell; + } + + public static Shell startCustomShell(String shellPath) throws IOException, TimeoutException, RootDeniedException { + return Shell.startCustomShell(shellPath, 0); + } + + public static Shell startCustomShell(String shellPath, int timeout) throws IOException, TimeoutException, RootDeniedException { + + if (Shell.customShell == null) { + RootShell.log("Starting Custom Shell!"); + Shell.customShell = new Shell(shellPath, ShellType.CUSTOM, ShellContext.NORMAL, timeout); + } else { + RootShell.log("Using Existing Custom Shell!"); + } + + return Shell.customShell; + } + + public static Shell startShell() throws IOException, TimeoutException { + return Shell.startShell(0); + } + + public static Shell startShell(int timeout) throws IOException, TimeoutException { + + try { + if (Shell.shell == null) { + RootShell.log("Starting Shell!"); + Shell.shell = new Shell("/system/bin/sh", ShellType.NORMAL, ShellContext.NORMAL, timeout); + } else { + RootShell.log("Using Existing Shell!"); + } + return Shell.shell; + } catch (RootDeniedException e) { + //Root Denied should never be thrown. + throw new IOException(); + } + } + + public Shell switchRootShellContext(ShellContext shellContext) throws IOException, TimeoutException, RootDeniedException { + if (this.shellType == ShellType.ROOT) { + try { + Shell.closeRootShell(); + } catch (Exception e) { + RootShell.log("Problem closing shell while trying to switch context..."); + } + + //create new root shell with new context... + + return Shell.startRootShell(this.shellTimeout, shellContext, 3); + } else { + //can only switch context on a root shell... + RootShell.log("Can only switch context on a root shell!"); + return this; + } + } + + protected static class Worker extends Thread { + + public int exit = -911; + + public Shell shell; + + private Worker(Shell shell) { + this.shell = shell; + } + + public void run() { + + /** + * Trying to open the shell. + * + * We echo "Started" and we look for it in the output. + * + * If we find the output then the shell is open and we return. + * + * If we do not find it then we determine the error and report + * it by setting the value of the variable exit + */ + try { + shell.outputStream.write("echo Started\n"); + shell.outputStream.flush(); + + while (true) { + String line = shell.inputStream.readLine(); + + if (line == null) { + throw new EOFException(); + } else if ("".equals(line)) { + continue; + } else if ("Started".equals(line)) { + this.exit = 1; + setShellOom(); + break; + } + + shell.error = "unkown error occured."; + } + } catch (IOException e) { + exit = -42; + if (e.getMessage() != null) { + shell.error = e.getMessage(); + } else { + shell.error = "RootAccess denied?."; + } + } + + } + + /* + * setOom for shell processes (sh and su if root shell) + * and discard outputs + * + */ + private void setShellOom() { + try { + Class processClass = shell.proc.getClass(); + Field field; + try { + field = processClass.getDeclaredField("pid"); + } catch (NoSuchFieldException e) { + field = processClass.getDeclaredField("id"); + } + field.setAccessible(true); + int pid = (Integer) field.get(shell.proc); + shell.outputStream.write("(echo -17 > /proc/" + pid + "/oom_adj) &> /dev/null\n"); + shell.outputStream.write("(echo -17 > /proc/$$/oom_adj) &> /dev/null\n"); + shell.outputStream.flush(); + } catch (Exception e) { + e.printStackTrace(); + } + } + } +} diff --git a/src/com/stericson/RootShellTests/NativeJavaClass.java b/src/com/stericson/RootShellTests/NativeJavaClass.java new file mode 100644 index 0000000..8fb7081 --- /dev/null +++ b/src/com/stericson/RootShellTests/NativeJavaClass.java @@ -0,0 +1,46 @@ +package com.stericson.RootShellTests; + +import com.stericson.RootShell.containers.RootClass; + +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; + +@RootClass.Candidate +public class NativeJavaClass +{ + + public NativeJavaClass(RootClass.RootArgs args) + { + System.out.println("NativeJavaClass says: oh hi there."); + String p = "/data/data/com.android.browser/cache"; + File f = new File(p); + String[] fl = f.list(); + if (fl != null) + { + System.out.println("Look at all the stuff in your browser's cache:"); + for (String af : fl) + { + System.out.println("-" + af); + } + System.out.println("Leaving my mark for posterity..."); + File f2 = new File(p + "/rootshell_was_here"); + try + { + FileWriter filewriter = new FileWriter(f2); + BufferedWriter out = new BufferedWriter(filewriter); + out.write("This is just a file created using RootShell's Sanity check tools..\n"); + out.close(); + System.out.println("Done!"); + } + catch (IOException e) + { + System.out.println("...and I failed miserably."); + e.printStackTrace(); + } + + } + } + +} diff --git a/src/com/stericson/RootShellTests/SanityCheckRootShell.java b/src/com/stericson/RootShellTests/SanityCheckRootShell.java new file mode 100644 index 0000000..356346d --- /dev/null +++ b/src/com/stericson/RootShellTests/SanityCheckRootShell.java @@ -0,0 +1,412 @@ +/* + * This file is part of the RootShell Project: http://code.google.com/p/RootShell/ + * + * Copyright (c) 2012 Stephen Erickson, Chris Ravenscroft, Dominik Schuermann, Adam Shanks + * + * This code is dual-licensed under the terms of the Apache License Version 2.0 and + * the terms of the General Public License (GPL) Version 2. + * You may use this code according to either of these licenses as is most appropriate + * for your project on a case-by-case basis. + * + * The terms of each license can be found in the root directory of this project's repository as well as at: + * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * http://www.gnu.org/licenses/gpl-2.0.txt + * + * Unless required by applicable law or agreed to in writing, software + * distributed under these Licenses is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See each License for the specific language governing permissions and + * limitations under that License. + */ + +package com.stericson.RootShellTests; + +import java.io.IOException; +import java.util.List; +import java.util.concurrent.TimeoutException; + +import android.app.Activity; +import android.app.ProgressDialog; +import android.content.Context; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.os.StrictMode; +import android.widget.ScrollView; +import android.widget.TextView; + +import com.stericson.RootShell.RootShell; +import com.stericson.RootShell.exceptions.RootDeniedException; +import com.stericson.RootShell.execution.Command; +import com.stericson.RootShell.execution.Shell; + +public class SanityCheckRootShell extends Activity +{ + private ScrollView mScrollView; + private TextView mTextView; + private ProgressDialog mPDialog; + + @Override + public void onCreate(Bundle savedInstanceState) + { + super.onCreate(savedInstanceState); + + StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder() + .detectDiskReads() + .detectDiskWrites() + .detectNetwork() // or .detectAll() for all detectable problems + .penaltyLog() + .build()); + StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder() + .detectLeakedSqlLiteObjects() + .detectLeakedClosableObjects() + .penaltyLog() + .penaltyDeath() + .build()); + + RootShell.debugMode = true; + + mTextView = new TextView(this); + mTextView.setText(""); + mScrollView = new ScrollView(this); + mScrollView.addView(mTextView); + setContentView(mScrollView); + + print("SanityCheckRootShell \n\n"); + + if (RootShell.isRootAvailable()) + { + print("Root found.\n"); + } + else + { + print("Root not found"); + } + + try + { + RootShell.getShell(true); + } + catch (IOException e2) + { + // TODO Auto-generated catch block + e2.printStackTrace(); + } + catch (TimeoutException e) + { + print("[ TIMEOUT EXCEPTION! ]\n"); + e.printStackTrace(); + } + catch (RootDeniedException e) + { + print("[ ROOT DENIED EXCEPTION! ]\n"); + e.printStackTrace(); + } + + try + { + if (!RootShell.isAccessGiven()) + { + print("ERROR: No root access to this device.\n"); + return; + } + } + catch (Exception e) + { + print("ERROR: could not determine root access to this device.\n"); + return; + } + + // Display infinite progress bar + mPDialog = new ProgressDialog(this); + mPDialog.setCancelable(false); + mPDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER); + + new SanityCheckThread(this, new TestHandler()).start(); + } + + protected void print(CharSequence text) + { + mTextView.append(text); + mScrollView.post(new Runnable() + { + public void run() + { + mScrollView.fullScroll(ScrollView.FOCUS_DOWN); + } + }); + } + + // Run our long-running tests in their separate thread so as to + // not interfere with proper rendering. + private class SanityCheckThread extends Thread + { + private Handler mHandler; + + public SanityCheckThread(Context context, Handler handler) + { + mHandler = handler; + } + + public void run() + { + visualUpdate(TestHandler.ACTION_SHOW, null); + + // First test: Install a binary file for future use + // if it wasn't already installed. + /* + visualUpdate(TestHandler.ACTION_PDISPLAY, "Installing binary if needed"); + if(false == RootShell.installBinary(mContext, R.raw.nes, "nes_binary")) { + visualUpdate(TestHandler.ACTION_HIDE, "ERROR: Failed to install binary. Please see log file."); + return; + } + */ + + boolean result; + + visualUpdate(TestHandler.ACTION_PDISPLAY, "Testing getPath"); + visualUpdate(TestHandler.ACTION_DISPLAY, "[ getPath ]\n"); + + try + { + List paths = RootShell.getPath(); + + for (String path : paths) + { + visualUpdate(TestHandler.ACTION_DISPLAY, path + " k\n\n"); + } + + } + catch (Exception e) + { + e.printStackTrace(); + } + + visualUpdate(TestHandler.ACTION_PDISPLAY, "Testing A ton of commands"); + visualUpdate(TestHandler.ACTION_DISPLAY, "[ Ton of Commands ]\n"); + + for (int i = 0; i < 100; i++) + { + RootShell.exists("/system/xbin/busybox"); + } + + visualUpdate(TestHandler.ACTION_PDISPLAY, "Testing Find Binary"); + result = RootShell.isRootAvailable(); + visualUpdate(TestHandler.ACTION_DISPLAY, "[ Checking Root ]\n"); + visualUpdate(TestHandler.ACTION_DISPLAY, result + " k\n\n"); + + visualUpdate(TestHandler.ACTION_PDISPLAY, "Testing file exists"); + visualUpdate(TestHandler.ACTION_DISPLAY, "[ Checking Exists() ]\n"); + visualUpdate(TestHandler.ACTION_DISPLAY, RootShell.exists("/system/sbin/[") + " k\n\n"); + + visualUpdate(TestHandler.ACTION_PDISPLAY, "Testing Is Access Given"); + result = RootShell.isAccessGiven(); + visualUpdate(TestHandler.ACTION_DISPLAY, "[ Checking for Access to Root ]\n"); + visualUpdate(TestHandler.ACTION_DISPLAY, result + " k\n\n"); + + + Shell shell; + + visualUpdate(TestHandler.ACTION_PDISPLAY, "Testing output capture"); + visualUpdate(TestHandler.ACTION_DISPLAY, "[ busybox ash --help ]\n"); + + try + { + shell = RootShell.getShell(true); + Command cmd = new Command( + 0, + "busybox ash --help") + { + + @Override + public void commandOutput(int id, String line) + { + visualUpdate(TestHandler.ACTION_DISPLAY, line + "\n"); + super.commandOutput(id, line); + } + }; + shell.add(cmd); + + } + catch (Exception e) + { + e.printStackTrace(); + } + + visualUpdate(TestHandler.ACTION_PDISPLAY, "Switching RootContext - SYSTEM_APP"); + visualUpdate(TestHandler.ACTION_DISPLAY, "[ Switching Root Context - SYSTEM_APP ]\n"); + + try + { + shell = RootShell.getShell(true, Shell.ShellContext.SYSTEM_APP); + Command cmd = new Command( + 0, + "id") + { + + @Override + public void commandOutput(int id, String line) + { + visualUpdate(TestHandler.ACTION_DISPLAY, line + "\n"); + super.commandOutput(id, line); + } + }; + shell.add(cmd); + + } + catch (Exception e) + { + e.printStackTrace(); + } + + visualUpdate(TestHandler.ACTION_PDISPLAY, "Switching RootContext - UNTRUSTED"); + visualUpdate(TestHandler.ACTION_DISPLAY, "[ Switching Root Context - UNTRUSTED ]\n"); + + try + { + shell = RootShell.getShell(true, Shell.ShellContext.UNTRUSTED_APP); + Command cmd = new Command( + 0, + "id") + { + + @Override + public void commandOutput(int id, String line) + { + visualUpdate(TestHandler.ACTION_DISPLAY, line + "\n"); + super.commandOutput(id, line); + } + }; + shell.add(cmd); + + } + catch (Exception e) + { + e.printStackTrace(); + } + + try + { + shell = RootShell.getShell(true); + + Command cmd = new Command(42, false, "echo done") + { + + boolean _catch = false; + + @Override + public void commandOutput(int id, String line) + { + if (_catch) + { + RootShell.log("CAUGHT!!!"); + } + + super.commandOutput(id, line); + + } + + @Override + public void commandTerminated(int id, String reason) + { + synchronized (com.stericson.RootShellTests.SanityCheckRootShell.this) + { + + _catch = true; + visualUpdate(TestHandler.ACTION_PDISPLAY, "All tests complete."); + visualUpdate(TestHandler.ACTION_HIDE, null); + + try + { + RootShell.closeAllShells(); + } + catch (IOException e) + { + // TODO Auto-generated catch block + e.printStackTrace(); + } + + } + } + + @Override + public void commandCompleted(int id, int exitCode) + { + synchronized (com.stericson.RootShellTests.SanityCheckRootShell.this) + { + _catch = true; + + visualUpdate(TestHandler.ACTION_PDISPLAY, "All tests complete."); + visualUpdate(TestHandler.ACTION_HIDE, null); + + try + { + RootShell.closeAllShells(); + } + catch (IOException e) + { + // TODO Auto-generated catch block + e.printStackTrace(); + } + + } + } + }; + + shell.add(cmd); + + } + catch (Exception e) + { + e.printStackTrace(); + } + + } + + private void visualUpdate(int action, String text) + { + Message msg = mHandler.obtainMessage(); + Bundle bundle = new Bundle(); + bundle.putInt(TestHandler.ACTION, action); + bundle.putString(TestHandler.TEXT, text); + msg.setData(bundle); + mHandler.sendMessage(msg); + } + } + + private class TestHandler extends Handler + { + static final public String ACTION = "action"; + static final public int ACTION_SHOW = 0x01; + static final public int ACTION_HIDE = 0x02; + static final public int ACTION_DISPLAY = 0x03; + static final public int ACTION_PDISPLAY = 0x04; + static final public String TEXT = "text"; + + public void handleMessage(Message msg) + { + int action = msg.getData().getInt(ACTION); + String text = msg.getData().getString(TEXT); + + switch (action) + { + case ACTION_SHOW: + mPDialog.show(); + mPDialog.setMessage("Running Root Library Tests..."); + break; + case ACTION_HIDE: + if (null != text) + { print(text); } + mPDialog.hide(); + break; + case ACTION_DISPLAY: + print(text); + break; + case ACTION_PDISPLAY: + mPDialog.setMessage(text); + break; + } + } + } +}