diff --git a/.gitmodules b/.gitmodules index e593b54d7..8e46a35a3 100644 --- a/.gitmodules +++ b/.gitmodules @@ -6,10 +6,6 @@ path = extern/MemorizingTrustManager url = https://github.com/ge0rg/MemorizingTrustManager.git ignore = dirty -[submodule "extern/libsuperuser"] - path = extern/libsuperuser - url = https://github.com/Chainfire/libsuperuser - ignore = dirty [submodule "extern/jmdns"] path = extern/jmdns url = https://gitlab.com/fdroid/jmdns.git diff --git a/extern/libsuperuser b/extern/libsuperuser deleted file mode 160000 index 7fb28c154..000000000 --- a/extern/libsuperuser +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 7fb28c15440c5ea42662b0bebdb3295ecd3cff44 diff --git a/extern/libsuperuser/LICENSE b/extern/libsuperuser/LICENSE new file mode 100644 index 000000000..ade750b16 --- /dev/null +++ b/extern/libsuperuser/LICENSE @@ -0,0 +1,177 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/extern/libsuperuser/build.gradle b/extern/libsuperuser/build.gradle new file mode 100644 index 000000000..ebc04a4e2 --- /dev/null +++ b/extern/libsuperuser/build.gradle @@ -0,0 +1,17 @@ +buildscript { + repositories { + jcenter() + } + + dependencies { + classpath 'com.android.tools.build:gradle:1.0.0' + classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.0' + classpath 'com.github.dcendents:android-maven-plugin:1.2' + } +} + +allprojects { + repositories { + jcenter() + } +} diff --git a/extern/libsuperuser/libsuperuser/.classpath b/extern/libsuperuser/libsuperuser/.classpath new file mode 100644 index 000000000..b76ec6cd4 --- /dev/null +++ b/extern/libsuperuser/libsuperuser/.classpath @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/extern/libsuperuser/libsuperuser/.project b/extern/libsuperuser/libsuperuser/.project new file mode 100644 index 000000000..8f20a24e9 --- /dev/null +++ b/extern/libsuperuser/libsuperuser/.project @@ -0,0 +1,38 @@ + + + libsuperuser + + + + + + com.android.ide.eclipse.adt.ResourceManagerBuilder + + + + + com.android.ide.eclipse.adt.PreCompilerBuilder + + + + + org.eclipse.jdt.core.javabuilder + + + + + com.saikoa.dexguard.eclipse.adt.ApkBuilder + + + + + com.android.ide.eclipse.adt.ApkBuilder + + + + + + com.android.ide.eclipse.adt.AndroidNature + org.eclipse.jdt.core.javanature + + diff --git a/extern/libsuperuser/libsuperuser/AndroidManifest.xml b/extern/libsuperuser/libsuperuser/AndroidManifest.xml new file mode 100644 index 000000000..2a7f3830e --- /dev/null +++ b/extern/libsuperuser/libsuperuser/AndroidManifest.xml @@ -0,0 +1,8 @@ + + + + + diff --git a/extern/libsuperuser/libsuperuser/build.gradle b/extern/libsuperuser/libsuperuser/build.gradle new file mode 100644 index 000000000..c3d7d2479 --- /dev/null +++ b/extern/libsuperuser/libsuperuser/build.gradle @@ -0,0 +1,105 @@ +apply plugin: 'com.android.library' +apply plugin: 'com.github.dcendents.android-maven' +apply plugin: 'com.jfrog.bintray' + +android { + compileSdkVersion 21 + buildToolsVersion "21.1.0" + + defaultConfig { + minSdkVersion 4 + targetSdkVersion 21 + } + + sourceSets { + main { + manifest.srcFile 'AndroidManifest.xml' + java.srcDirs = ['src'] + resources.srcDirs = ['src'] + aidl.srcDirs = ['src'] + renderscript.srcDirs = ['src'] + res.srcDirs = ['res'] + assets.srcDirs = ['assets'] + } + } +} + +version = "1.0.0." + (new Date()).format('yyyyMMddHHmm') +group = "eu.chainfire" + +bintray { + // in global gradle.properties: "systemProp.bintrayUser=chainfire", etc + user = System.properties['bintrayUser'] + key = System.properties['bintrayApiKey'] + + configurations = ['archives'] + dryRun = false + publish = true + pkg { + repo = 'maven' + name = 'libsuperuser' + desc = 'libsuperuser' + websiteUrl = 'https://github.com/Chainfire/libsuperuser' + issueTrackerUrl = 'https://github.com/Chainfire/libsuperuser/issues' + vcsUrl = 'https://github.com/Chainfire/libsuperuser.git' + licenses = ['Apache-2.0'] + publicDownloadNumbers = true + } + +} + +install { + repositories.mavenInstaller { + pom { + project { + packaging 'aar' + name 'libsuperuser' + url 'https://github.com/Chainfire/libsuperuser' + licenses { + license { + name 'The Apache Software License, Version 2.0' + url 'http://www.apache.org/licenses/LICENSE-2.0.txt' + } + } + developers { + developer { + id 'Chainfire' + name 'Jorrit Jongma' + } + } + scm { + connection 'https://github.com/Chainfire/libsuperuser.git' + developerConnection 'https://github.com/Chainfire/libsuperuser.git' + url 'https://github.com/Chainfire/libsuperuser.git' + } + } + } + } +} + +dependencies { +} + +task sourcesJar(type: Jar) { + from android.sourceSets.main.java.srcDirs + classifier = 'sources' +} + +task javadoc(type: Javadoc) { + source = android.sourceSets.main.java.srcDirs + classpath += project.files(android.getBootClasspath().join(File.pathSeparator)) +} + +task javadocJar(type: Jar, dependsOn: javadoc) { + classifier = 'javadoc' + from javadoc.destinationDir +} + +artifacts { + archives javadocJar + archives sourcesJar +} + +task findConventions << { + println project.getConvention() +} \ No newline at end of file diff --git a/extern/libsuperuser/libsuperuser/project.properties b/extern/libsuperuser/libsuperuser/project.properties new file mode 100644 index 000000000..93c8c3c08 --- /dev/null +++ b/extern/libsuperuser/libsuperuser/project.properties @@ -0,0 +1,15 @@ +# This file is automatically generated by Android Tools. +# Do not modify this file -- YOUR CHANGES WILL BE ERASED! +# +# This file must be checked in Version Control Systems. +# +# To customize properties used by the Ant build system edit +# "ant.properties", and override values to adapt the script to your +# project structure. +# +# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home): +#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt + +# Project target. +target=android-21 +android.library=true diff --git a/extern/libsuperuser/libsuperuser/src/eu/chainfire/libsuperuser/Application.java b/extern/libsuperuser/libsuperuser/src/eu/chainfire/libsuperuser/Application.java new file mode 100644 index 000000000..a439517fb --- /dev/null +++ b/extern/libsuperuser/libsuperuser/src/eu/chainfire/libsuperuser/Application.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2012-2014 Jorrit "Chainfire" Jongma + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package eu.chainfire.libsuperuser; + +import android.content.Context; +import android.os.Handler; +import android.widget.Toast; + +/** + * Base application class to extend from, solving some issues with + * toasts and AsyncTasks you are likely to run into + */ +public class Application extends android.app.Application { + /** + * Shows a toast message + * + * @param context Any context belonging to this application + * @param message The message to show + */ + public static void toast(Context context, String message) { + // this is a static method so it is easier to call, + // as the context checking and casting is done for you + + if (context == null) return; + + if (!(context instanceof Application)) { + context = context.getApplicationContext(); + } + + if (context instanceof Application) { + final Context c = context; + final String m = message; + + ((Application)context).runInApplicationThread(new Runnable() { + @Override + public void run() { + Toast.makeText(c, m, Toast.LENGTH_LONG).show(); + } + }); + } + } + + private static Handler mApplicationHandler = new Handler(); + + /** + * Run a runnable in the main application thread + * + * @param r Runnable to run + */ + public void runInApplicationThread(Runnable r) { + mApplicationHandler.post(r); + } + + @Override + public void onCreate() { + super.onCreate(); + + try { + // workaround bug in AsyncTask, can show up (for example) when you toast from a service + // this makes sure AsyncTask's internal handler is created from the right (main) thread + Class.forName("android.os.AsyncTask"); + } catch (ClassNotFoundException e) { + } + } +} diff --git a/extern/libsuperuser/libsuperuser/src/eu/chainfire/libsuperuser/Debug.java b/extern/libsuperuser/libsuperuser/src/eu/chainfire/libsuperuser/Debug.java new file mode 100644 index 000000000..c6f695ca0 --- /dev/null +++ b/extern/libsuperuser/libsuperuser/src/eu/chainfire/libsuperuser/Debug.java @@ -0,0 +1,240 @@ +/* + * Copyright (C) 2012-2014 Jorrit "Chainfire" Jongma + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package eu.chainfire.libsuperuser; + +import android.os.Looper; +import android.util.Log; + +/** + * Utility class for logging and debug features that (by default) does nothing when not in debug mode + */ +public class Debug { + + // ----- DEBUGGING ----- + + private static boolean debug = BuildConfig.DEBUG; + + /** + *

Enable or disable debug mode

+ * + *

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

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

Is debug mode enabled ?

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

Log a message (internal)

+ * + *

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

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

Log a "general" message

+ * + *

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

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

Log a "per-command" message

+ * + *

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

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

Log a line of stdout/stderr output

+ * + *

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

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

Enable or disable logging specific types of message

+ * + *

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

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

Is logging for specific types of messages enabled ?

+ * + *

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

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

Is logging for specific types of messages enabled ?

+ * + *

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

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

Register a custom log handler

+ * + *

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

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

Get the currently registered custom log handler

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

Enable or disable sanity checks

+ * + *

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

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

Are sanity checks enabled ?

+ * + *

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

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

Are sanity checks enabled ?

+ * + *

Takes debug mode into account for the result.

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

Are we running on the main thread ?

+ * + * @return Running on main thread ? + */ + public static boolean onMainThread() { + return ((Looper.myLooper() != null) && (Looper.myLooper() == Looper.getMainLooper())); + } + +} diff --git a/extern/libsuperuser/libsuperuser/src/eu/chainfire/libsuperuser/HideOverlaysReceiver.java b/extern/libsuperuser/libsuperuser/src/eu/chainfire/libsuperuser/HideOverlaysReceiver.java new file mode 100644 index 000000000..4b4ce5a8d --- /dev/null +++ b/extern/libsuperuser/libsuperuser/src/eu/chainfire/libsuperuser/HideOverlaysReceiver.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2012-2014 Jorrit "Chainfire" Jongma + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package eu.chainfire.libsuperuser; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; + +/** + *

+ * Base receiver to extend to catch notifications when overlays should be + * hidden. + *

+ *

+ * Tapjacking protection in SuperSU prevents some dialogs from receiving user + * input when overlays are present. For security reasons this notification is + * only sent to apps that have previously been granted root access, so even if + * your app does not require root, you still need to request + * it, and the user must grant it. + *

+ *

+ * Note that the word overlay as used here should be interpreted as "any view or + * window possibly obscuring SuperSU dialogs". + *

+ */ +public abstract class HideOverlaysReceiver extends BroadcastReceiver { + public static final String ACTION_HIDE_OVERLAYS = "eu.chainfire.supersu.action.HIDE_OVERLAYS"; + public static final String CATEGORY_HIDE_OVERLAYS = Intent.CATEGORY_INFO; + public static final String EXTRA_HIDE_OVERLAYS = "eu.chainfire.supersu.extra.HIDE"; + + @Override + public final void onReceive(Context context, Intent intent) { + if (intent.hasExtra(EXTRA_HIDE_OVERLAYS)) { + onHideOverlays(intent.getBooleanExtra(EXTRA_HIDE_OVERLAYS, false)); + } + } + + /** + * Called when overlays should be hidden or may be shown + * again. + * + * @param hide Should overlays be hidden? + */ + public abstract void onHideOverlays(boolean hide); +} diff --git a/extern/libsuperuser/libsuperuser/src/eu/chainfire/libsuperuser/Shell.java b/extern/libsuperuser/libsuperuser/src/eu/chainfire/libsuperuser/Shell.java new file mode 100644 index 000000000..ebd03cff9 --- /dev/null +++ b/extern/libsuperuser/libsuperuser/src/eu/chainfire/libsuperuser/Shell.java @@ -0,0 +1,1750 @@ +/* + * Copyright (C) 2012-2014 Jorrit "Chainfire" Jongma + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package eu.chainfire.libsuperuser; + +import java.io.DataOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +import android.os.Handler; +import android.os.Looper; + +import eu.chainfire.libsuperuser.StreamGobbler.OnLineListener; + +/** + * Class providing functionality to execute commands in a (root) shell + */ +public class Shell { + /** + *

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

+ *

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

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

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

+ *

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

+ *

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

+ *

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

+ *

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

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

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

+ *

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

+ *

+ * 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 + */ + public static synchronized String version(boolean internal) { + int idx = internal ? 0 : 1; + if (suVersion[idx] == null) { + String version = null; + + List ret = Shell.run( + internal ? "su -V" : "su -v", + new String[] { "exit" }, + null, + false + ); + + 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]; + } + + /** + * Attempts to deduce if the shell command refers to a su shell + * + * @param shell Shell command to run + * @return Shell command appears to be su + */ + public static boolean isSU(String shell) { + // Strip parameters + int pos = shell.indexOf(' '); + if (pos >= 0) { + shell = shell.substring(0, pos); + } + + // Strip path + pos = shell.lastIndexOf('/'); + if (pos >= 0) { + shell = shell.substring(pos + 1); + } + + return shell.equals("su"); + } + + /** + * Constructs a shell command to start a su shell using the supplied uid + * and SELinux context. This is can be an expensive operation, consider + * caching the result. + * + * @param uid Uid to use (0 == root) + * @param context (SELinux) context name to use or null + * @return Shell command + */ + public static String shell(int uid, String context) { + // su[ --context ][ ] + String shell = "su"; + + if ((context != null) && isSELinuxEnforcing()) { + String display = version(false); + String internal = version(true); + + // We only know the format for SuperSU v1.90+ right now + if ((display != null) && + (internal != null) && + (display.endsWith("SUPERSU")) && + (Integer.valueOf(internal) >= 190)) { + shell = String.format(Locale.ENGLISH, "%s --context %s", shell, context); + } + } + + // Most su binaries support the "su " format, but in case + // they don't, lets skip it for the default 0 (root) case + if (uid > 0) { + shell = String.format(Locale.ENGLISH, "%s %d", shell, uid); + } + + return shell; + } + + /** + * Constructs a shell command to start a su shell connected to mount + * master daemon, to perform public mounts on Android 4.3+ (or 4.2+ in + * SELinux enforcing mode) + * + * @return Shell command + */ + public static String shellMountMaster() { + if (android.os.Build.VERSION.SDK_INT >= 17) { + return "su --mount-master"; + } + return "su"; + } + + /** + * 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 static 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 + if (enforcing == null) { + 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; + } + + /** + *

+ * Clears results cached by isSELinuxEnforcing() and version(boolean + * internal) calls. + *

+ *

+ * Most apps should never need to call this, as neither enforcing status + * nor su version is likely to change on a running device - though it is + * not impossible. + *

+ */ + public static synchronized void clearCachedResults() { + isSELinuxEnforcing = null; + suVersion[0] = null; + suVersion[1] = null; + } + } + + private interface OnResult { + // for any onCommandResult callback + public static final int WATCHDOG_EXIT = -1; + public static final int SHELL_DIED = -2; + + // for Interactive.open() callbacks only + public static final int SHELL_EXEC_FAILED = -3; + public static final int SHELL_WRONG_UID = -4; + public static final int SHELL_RUNNING = 0; + } + + /** + * Command result callback, notifies the recipient of the completion of a + * command block, including the (last) exit code, and the full output + */ + public interface OnCommandResultListener extends OnResult { + /** + *

+ * Command result callback + *

+ *

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

+ *

+ * See {@link Shell.Interactive} for threading details + *

+ * + * @param commandCode Value previously supplied to addCommand + * @param exitCode Exit code of the last command in the block + * @param output All output generated by the command block + */ + public void onCommandResult(int commandCode, int exitCode, List output); + } + + /** + * Command per line callback for parsing the output line by line without + * buffering It also notifies the recipient of the completion of a command + * block, including the (last) exit code. + */ + public interface OnCommandLineListener extends OnResult, OnLineListener { + /** + *

+ * Command result callback + *

+ *

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

+ *

+ * See {@link Shell.Interactive} for threading details + *

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

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

+ *

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

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

+ * Automatically create a handler if possible ? Default to true + *

+ *

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

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

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

+ *

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

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

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

+ *

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

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

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

+ *

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

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

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

+ *

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

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

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

+ *

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

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

+ * Enable command timeout callback + *

+ *

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

+ *

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

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

+ * Enable/disable reduced logcat output + *

+ *

+ * Note that this is a global setting + *

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

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

+ *

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

+ *

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

+ *

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

+ *

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

+ *

Callbacks, threads and handlers

+ *

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

+ *

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

+ *

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

+ *

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

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

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

+ *

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

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

+ * Add a command to execute, with a callback. This callback gobbles the + * output line by line without buffering it and also returns the result + * code on completion. + *

+ *

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

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

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

+ *

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

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

+ * Add commands to execute, with a callback. This callback gobbles the + * output line by line without buffering it and also returns the result + * code on completion. + *

+ *

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

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

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

+ *

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

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

+ * Add commands to execute, with a callback. This callback gobbles the + * output line by line without buffering it and also returns the result + * code on completion. + *

+ *

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

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

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

+ *

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

+ *

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

+ *

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

+ *

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

+ * + * @return True if wait complete, false if wait interrupted + */ + public boolean waitForIdle() { + if (Debug.getSanityChecksEnabledEffective() && Debug.onMainThread()) { + Debug.log(ShellOnMainThreadException.EXCEPTION_WAIT_IDLE); + throw new ShellOnMainThreadException(ShellOnMainThreadException.EXCEPTION_WAIT_IDLE); + } + + if (isRunning()) { + synchronized (idleSync) { + while (!idle) { + try { + idleSync.wait(); + } catch (InterruptedException e) { + return false; + } + } + } + + if ((handler != null) && + (handler.getLooper() != null) && + (handler.getLooper() != Looper.myLooper())) { + // If the callbacks are posted to a different thread than + // this one, we can wait until all callbacks have called + // before returning. If we don't use a Handler at all, the + // callbacks are already called before we get here. If we do + // use a Handler but we use the same Looper, waiting here + // would actually block the callbacks from being called + + synchronized (callbackSync) { + while (callbacks > 0) { + try { + callbackSync.wait(); + } catch (InterruptedException e) { + return false; + } + } + } + } + } + + return true; + } + + /** + * Are we using a Handler to post callbacks ? + * + * @return Handler used ? + */ + public boolean hasHandler() { + return (handler != null); + } + } +} diff --git a/extern/libsuperuser/libsuperuser/src/eu/chainfire/libsuperuser/ShellNotClosedException.java b/extern/libsuperuser/libsuperuser/src/eu/chainfire/libsuperuser/ShellNotClosedException.java new file mode 100644 index 000000000..2a44083ec --- /dev/null +++ b/extern/libsuperuser/libsuperuser/src/eu/chainfire/libsuperuser/ShellNotClosedException.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2012-2014 Jorrit "Chainfire" Jongma + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package eu.chainfire.libsuperuser; + +/** + * Exception class used to notify developer that a shell was not close()d + */ +@SuppressWarnings("serial") +public class ShellNotClosedException extends RuntimeException { + public static final String EXCEPTION_NOT_CLOSED = "Application did not close() interactive shell"; + + public ShellNotClosedException() { + super(EXCEPTION_NOT_CLOSED); + } +} diff --git a/extern/libsuperuser/libsuperuser/src/eu/chainfire/libsuperuser/ShellOnMainThreadException.java b/extern/libsuperuser/libsuperuser/src/eu/chainfire/libsuperuser/ShellOnMainThreadException.java new file mode 100644 index 000000000..69732e3b3 --- /dev/null +++ b/extern/libsuperuser/libsuperuser/src/eu/chainfire/libsuperuser/ShellOnMainThreadException.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2012-2014 Jorrit "Chainfire" Jongma + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package eu.chainfire.libsuperuser; + +/** + * Exception class used to crash application when shell commands are executed + * from the main thread, and we are in debug mode. + */ +@SuppressWarnings("serial") +public class ShellOnMainThreadException extends RuntimeException { + public static final String EXCEPTION_COMMAND = "Application attempted to run a shell command from the main thread"; + public static final String EXCEPTION_NOT_IDLE = "Application attempted to wait for a non-idle shell to close on the main thread"; + public static final String EXCEPTION_WAIT_IDLE = "Application attempted to wait for a shell to become idle on the main thread"; + + public ShellOnMainThreadException(String message) { + super(message); + } +} diff --git a/extern/libsuperuser/libsuperuser/src/eu/chainfire/libsuperuser/StreamGobbler.java b/extern/libsuperuser/libsuperuser/src/eu/chainfire/libsuperuser/StreamGobbler.java new file mode 100644 index 000000000..730521545 --- /dev/null +++ b/extern/libsuperuser/libsuperuser/src/eu/chainfire/libsuperuser/StreamGobbler.java @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2012-2014 Jorrit "Chainfire" Jongma + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package eu.chainfire.libsuperuser; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.List; + +/** + * Thread utility class continuously reading from an InputStream + */ +public class StreamGobbler extends Thread { + /** + * Line callback interface + */ + public interface OnLineListener { + /** + *

Line callback

+ * + *

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

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

StreamGobbler constructor

+ * + *

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

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

StreamGobbler constructor

+ * + *

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

+ * + * @param shell Name of the shell + * @param inputStream InputStream to read from + * @param onLineListener OnLineListener callback + */ + public StreamGobbler(String shell, InputStream inputStream, OnLineListener onLineListener) { + this.shell = shell; + reader = new BufferedReader(new InputStreamReader(inputStream)); + listener = onLineListener; + } + + @Override + public void run() { + // keep reading the InputStream until it ends (or an error occurs) + try { + String line = null; + while ((line = reader.readLine()) != null) { + Debug.logOutput(String.format("[%s] %s", shell, line)); + if (writer != null) writer.add(line); + if (listener != null) listener.onLine(line); + } + } catch (IOException e) { + } + + // make sure our stream is closed and resources will be freed + try { + reader.close(); + } catch (IOException e) { + } + } +} diff --git a/extern/libsuperuser/settings.gradle b/extern/libsuperuser/settings.gradle new file mode 100644 index 000000000..32aef3ac0 --- /dev/null +++ b/extern/libsuperuser/settings.gradle @@ -0,0 +1 @@ +include ':libsuperuser', ':libsuperuser_example'