+
+
\ 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 extends TypeElement> 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;
+ }
+ }
+ }
+}