feat(android): add libshell and list devices

This commit is contained in:
John Zhao 2019-01-16 10:41:12 +08:00
parent 5105b5ea82
commit d531112075
22 changed files with 2764 additions and 23 deletions

View File

@ -37,6 +37,7 @@ dependencies {
annotationProcessor 'com.jakewharton:butterknife-compiler:10.0.0'
implementation project(':libmynteye')
implementation project(':libshell')
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test:runner:1.1.1'

View File

@ -3,6 +3,10 @@
xmlns:tools="http://schemas.android.com/tools"
package="com.slightech.mynteye.demo">
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<application
android:allowBackup="false"
android:label="@string/app_name"
@ -10,7 +14,7 @@
android:supportsRtl="true"
android:theme="@style/AppTheme"
tools:ignore="GoogleAppIndexingWarning">
<activity android:name=".MainActivity">
<activity android:name=".ui.MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>

View File

@ -1,20 +0,0 @@
package com.slightech.mynteye.demo;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import com.slightech.mynteye.Device;
import com.slightech.mynteye.DeviceUsbInfo;
import timber.log.Timber;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
for (DeviceUsbInfo info : Device.query()) {
Timber.i(info.toString());
}
}
}

View File

@ -0,0 +1,63 @@
package com.slightech.mynteye.demo.ui;
import android.annotation.SuppressLint;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import static android.Manifest.permission.CAMERA;
import static android.Manifest.permission.WRITE_EXTERNAL_STORAGE;
@SuppressLint("Registered")
public class BaseActivity extends AppCompatActivity {
private final int REQ_PERMISSIONS = 1;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestPermissions();
}
private void requestPermissions() {
final String[] permissions = new String[]{WRITE_EXTERNAL_STORAGE, CAMERA};
boolean granted = true;
for (String permission : permissions) {
if (ContextCompat.checkSelfPermission(this, permission)
!= PackageManager.PERMISSION_GRANTED) {
granted = false;
}
}
if (granted) return;
ActivityCompat.requestPermissions(this, permissions, REQ_PERMISSIONS);
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
@NonNull int[] grantResults) {
if (requestCode == REQ_PERMISSIONS) {
boolean granted = true;
if (grantResults.length < 1) {
granted = false;
} else {
for (int result : grantResults) {
if (result != PackageManager.PERMISSION_GRANTED) {
granted = false;
}
}
}
if (!granted) {
Toast.makeText(this, "Permission denied :(", Toast.LENGTH_LONG).show();
}
} else {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
}
}

View File

@ -0,0 +1,105 @@
package com.slightech.mynteye.demo.ui;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.Toast;
import androidx.appcompat.app.AlertDialog;
import butterknife.ButterKnife;
import com.slightech.mynteye.Device;
import com.slightech.mynteye.DeviceUsbInfo;
import com.slightech.mynteye.demo.R;
import com.slightech.mynteye.demo.util.RootUtils;
import java.util.ArrayList;
import java.util.Locale;
public class MainActivity extends BaseActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ButterKnife.bind(this);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.menu_main, menu);
MenuItem item = menu.findItem(R.id.action_open);
if (item != null) {
item.setEnabled(false);
actionOpen(() -> item.setEnabled(true));
}
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.action_open:
item.setEnabled(false);
actionOpen(() -> item.setEnabled(true));
return true;
default:
return super.onOptionsItemSelected(item);
}
}
private void actionOpen(final Runnable completeEvent) {
if (!RootUtils.isRooted()) {
alert("Warning", "Root denied :(");
return;
}
RootUtils.requestAccessible(ok -> {
if (completeEvent != null) completeEvent.run();
if (ok) {
toast("Root granted :)");
showDevices();
} else {
alert("Warning", "There are no devices accessible.");
}
});
}
private void showDevices() {
ArrayList<DeviceUsbInfo> infos = Device.query();
if (infos.isEmpty()) {
alert("Warning", "There are no devices :(");
} else {
ArrayList<String> items = new ArrayList<>();
for (DeviceUsbInfo info : infos) {
items.add(String.format(Locale.getDefault(), "%d, %s, SN: %s",
info.getIndex(), info.getName(), info.getSn()));
}
AlertDialog dialog = new AlertDialog.Builder(this)
.setTitle("Devices")
.create();
ListView listView = new ListView(this);
listView.setAdapter(new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, items));
listView.setOnItemClickListener((parent, view, position, id) -> {
dialog.dismiss();
openDevice(infos.get(position));
});
dialog.setView(listView);
dialog.show();
}
}
private void openDevice(DeviceUsbInfo info) {
}
private void toast(CharSequence text) {
Toast.makeText(this, text, Toast.LENGTH_LONG).show();
}
private void alert(CharSequence title, CharSequence message) {
new AlertDialog.Builder(this)
.setTitle(title)
.setMessage(message)
.setPositiveButton(android.R.string.ok, null)
.show();
}
}

View File

@ -0,0 +1,76 @@
package com.slightech.mynteye.demo.util;
import com.stericson.RootShell.RootShell;
import com.stericson.RootShell.exceptions.RootDeniedException;
import com.stericson.RootShell.execution.Command;
import com.stericson.RootShell.execution.Shell;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
import timber.log.Timber;
public final class RootUtils {
public interface OnRequestAccessibleListener {
void onRequestAccessible(boolean ok);
}
public static boolean isRooted() {
if (!RootShell.isRootAvailable()) {
Timber.e("Root not found");
return false;
}
try {
RootShell.getShell(true);
} catch (IOException e) {
e.printStackTrace();
return false;
} catch (TimeoutException e) {
Timber.e("TIMEOUT EXCEPTION!");
e.printStackTrace();
return false;
} catch (RootDeniedException e) {
Timber.e("ROOT DENIED EXCEPTION!");
e.printStackTrace();
return false;
}
try {
if (!RootShell.isAccessGiven()) {
Timber.e("ERROR: No root access to this device.");
return false;
}
} catch (Exception e) {
Timber.e("ERROR: could not determine root access to this device.");
return false;
}
return true;
}
public static void requestAccessible(OnRequestAccessibleListener l) {
try {
Shell sh = RootShell.getShell(true);
sh.add(new Command(1, "chmod 666 /dev/video*") {
@Override
public void commandOutput(int id, String line) {
Timber.d("commandOutput: %s", line);
super.commandOutput(id, line);
}
@Override
public void commandTerminated(int id, String reason) {
Timber.d("commandTerminated: %s", reason);
}
@Override
public void commandCompleted(int id, int exitcode) {
Timber.d("commandCompleted: %s", ((exitcode == 0) ? "ok" : "fail"));
if (l != null) l.onRequestAccessible(exitcode == 0);
}
});
sh.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}

View File

@ -5,10 +5,11 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity"
tools:context=".ui.MainActivity"
>
<TextView
android:id="@+id/text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_open"
android:orderInCategory="0"
android:title="@string/open"
app:showAsAction="ifRoom|withText" />
</menu>

View File

@ -1,3 +1,5 @@
<resources>
<string name="app_name">mynteye</string>
<string name="open">Open</string>
</resources>

View File

@ -2,6 +2,7 @@
#include "mynteye/device/context.h"
#include "mynteye/device/device.h"
#include "mynteye/logger.h"
#include "device_usb_info.hpp"
#include "stream_request.hpp"
@ -12,6 +13,7 @@ MYNTEYE_USE_NAMESPACE
namespace mynteye_jni {
std::vector<DeviceUsbInfo> Device::Query() {
VLOG(2) << __func__;
std::vector<DeviceUsbInfo> infos;
Context context;
@ -27,6 +29,7 @@ std::vector<DeviceUsbInfo> Device::Query() {
}
std::shared_ptr<Device> Device::Create(const DeviceUsbInfo & info) {
VLOG(2) << __func__;
Context context;
int32_t i = 0;
for (auto&& d : context.devices()) {
@ -39,12 +42,15 @@ std::shared_ptr<Device> Device::Create(const DeviceUsbInfo & info) {
}
DeviceImpl::DeviceImpl(const device_t & device) : Device(), device_(device) {
VLOG(2) << __func__;
}
DeviceImpl::~DeviceImpl() {
VLOG(2) << __func__;
}
std::vector<StreamRequest> DeviceImpl::GetStreamRequests() {
VLOG(2) << __func__;
std::vector<StreamRequest> requests;
int32_t i = 0;
@ -58,6 +64,7 @@ std::vector<StreamRequest> DeviceImpl::GetStreamRequests() {
}
void DeviceImpl::ConfigStreamRequest(const StreamRequest & request) {
VLOG(2) << __func__;
int32_t i = 0;
for (auto&& req : device_->GetStreamRequests()) {
if (i == request.index) {
@ -69,9 +76,11 @@ void DeviceImpl::ConfigStreamRequest(const StreamRequest & request) {
}
void DeviceImpl::Start() {
VLOG(2) << __func__;
}
void DeviceImpl::Stop() {
VLOG(2) << __func__;
}
} // namespace mynteye_jni

View File

@ -0,0 +1 @@
/build

View File

@ -0,0 +1,2 @@
* [RootShell](https://github.com/Stericson/RootShell)

View File

@ -0,0 +1,29 @@
apply plugin: 'com.android.library'
android {
compileSdkVersion xversions.compileSdk
defaultConfig {
minSdkVersion xversions.minSdk
targetSdkVersion xversions.targetSdk
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test:runner:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1'
}

View File

@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View File

@ -0,0 +1,2 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.stericson.RootShell"/>

View File

@ -0,0 +1,610 @@
/*
* 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.6";
/**
* Setting this to false will disable the handler that is used
* by default for the 3 callback methods for Command.
* <p/>
* 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.
* <p/>
* 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 <code>boolean</code> 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<String> result = new ArrayList<String>();
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) {
RootShell.log("Exception: " + e);
return false;
}
for (String line : result) {
if (line.trim().equals(file)) {
return true;
}
}
result.clear();
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 {
RootShell.getShell(true).add(command);
commandWait(RootShell.getShell(true), command);
} catch (Exception e) {
RootShell.log("Exception: " + e);
return false;
}
//Avoid concurrent modification...
List<String> final_result = new ArrayList<String>();
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.
* @param singlePath boolean that represents whether to return a single path or multiple.
*
* @return <code>List<String></code> containing the locations the binary was found at.
*/
public static List<String> findBinary(String binaryName, boolean singlePath) {
return findBinary(binaryName, null, singlePath);
}
/**
* @param binaryName <code>String</code> that represent the binary to find.
* @param searchPaths <code>List<String></code> which contains the paths to search for this binary in.
* @param singlePath boolean that represents whether to return a single path or multiple.
*
* @return <code>List<String></code> containing the locations the binary was found at.
*/
public static List<String> findBinary(final String binaryName, List<String> searchPaths, boolean singlePath) {
final List<String> foundPaths = new ArrayList<String>();
boolean found = false;
if(searchPaths == null)
{
searchPaths = RootShell.getPath();
}
RootShell.log("Checking for " + binaryName);
//Try to use stat first
try {
for (String path : searchPaths) {
if(!path.endsWith("/"))
{
path += "/";
}
final String currentPath = path;
Command cc = new Command(0, false, "stat " + path + binaryName) {
@Override
public void commandOutput(int id, String line) {
if (line.contains("File: ") && line.contains(binaryName)) {
foundPaths.add(currentPath);
RootShell.log(binaryName + " was found here: " + currentPath);
}
RootShell.log(line);
super.commandOutput(id, line);
}
};
cc = RootShell.getShell(false).add(cc);
commandWait(RootShell.getShell(false), cc);
if(foundPaths.size() > 0 && singlePath) {
break;
}
}
found = !foundPaths.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 path : searchPaths) {
if(!path.endsWith("/"))
{
path += "/";
}
if (RootShell.exists(path + binaryName)) {
RootShell.log(binaryName + " was found here: " + path);
foundPaths.add(path);
if(foundPaths.size() > 0 && singlePath) {
break;
}
} else {
RootShell.log(binaryName + " was NOT found here: " + path);
}
}
}
Collections.reverse(foundPaths);
return foundPaths;
}
/**
* 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 <code>String</code> to Indicate the path to the shell that you want to open.
* @param timeout an <code>int</code> 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 Shell.startCustomShell(shellPath, timeout);
}
/**
* This will return the environment variable PATH
*
* @return <code>List<String></code> A List of Strings representing the environment variable $PATH
*/
public static List<String> 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 <code>boolean</code> to Indicate whether or not you want to open a root shell or a standard shell
* @param timeout an <code>int</code> 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 <code>int</code> 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 <code>boolean</code> to Indicate whether or not you want to open a root shell or a standard shell
* @param timeout an <code>int</code> 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 <code>boolean</code> 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 <code>boolean</code> to Indicate whether or not you want to open a root shell or a standard shell
* @param timeout an <code>int</code> 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 <code>boolean</code> 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 <code>true</code> 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() {
return isAccessGiven(0, 3);
}
/**
* Control how many time of retries should request
*
* @param timeout The timeout
* @param retries The number of retries
*
* @return <code>true</code> 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(int timeout, int retries) {
final Set<String> ID = new HashSet<String>();
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 shell = Shell.startRootShell(timeout, retries);
shell.add(command);
commandWait(shell, 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 <code>true</code> if BusyBox was found.
*/
public static boolean isBusyboxAvailable()
{
return isBusyboxAvailable(false);
}
/**
* @return <code>true</code> if BusyBox or Toybox was found.
*/
public static boolean isBusyboxAvailable(boolean includeToybox)
{
if(includeToybox) {
return (findBinary("busybox", true)).size() > 0 || (findBinary("toybox", true)).size() > 0;
} else {
return (findBinary("busybox", true)).size() > 0;
}
}
/**
* @return <code>true</code> if su was found.
*/
public static boolean isRootAvailable() {
return (findBinary("su", true)).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.
* <p/>
* 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.
* <p/>
* 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.
* <p/>
* 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)
* <p/>
* 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.
* <p/>
* 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));
RootShell.log(version, "Processed " + cmd.totalOutputProcessed + " of " + cmd.totalOutput + " output from command.");
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();
}
}
}
}
}

View File

@ -0,0 +1,348 @@
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 + "RootShell" + File.separator;
private List<File> classFiles;
public AnnotationsFinder() throws IOException {
System.out.println("Discovering root class annotations...");
classFiles = new ArrayList<File>();
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<String> al = new ArrayList<String>();
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) {
}
String strRawFolder = "res" + File.separator + "raw";
if (builtPath.toString().startsWith("build")); //Check if running in AndroidStudio
strRawFolder = "src" + File.separator + "main" + File.separator + "res" + File.separator + "raw";
File rawFolder = new File(strRawFolder);
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=" + strRawFolder + File.separator + "anbuild.dex "
+ builtPath + File.separator + "anbuild.jar"
};
} else {
cmd = new String[]{
getPathToDx(),
"--dex",
"--output=" + strRawFolder + File.separator + "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 src" + File.separator + "main" + File.separator + "res" + File.separator + "raw" + File.separator + " folder :::");
}
protected void lookup(File path, List<File> fileList) {
String desourcedPath = path.toString().replace("src" + File.separator, "").replace("main" + File.separator + "java" + File.separator, "");
File[] files = path.listFiles(new FileFilter() {
@Override
public boolean accept(File file) {
return true;
}
});
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 if (splitFileName[1].contains("rc")) {
continue; //Do not use release candidates
} 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;
}
}
if (null == foundPath) {
File androidStudioPath = new File("build" + File.separator + "intermediates" + File.separator + "classes" + File.separator + "debug"); // Android Studio
if (androidStudioPath.isDirectory()) {
foundPath = androidStudioPath;
}
}
return foundPath;
}
}
public static void main(String[] args) {
try {
if (args.length == 0) {
new AnnotationsFinder();
} else {
new RootClass(args);
}
} catch (Exception e) {
displayError(e);
}
}
}

View File

@ -0,0 +1,32 @@
/*
* This file is part of the RootShell Project: https://github.com/Stericson/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.exceptions;
public class RootDeniedException extends Exception {
private static final long serialVersionUID = -8713947214162841310L;
public RootDeniedException(String error) {
super(error);
}
}

View File

@ -0,0 +1,350 @@
/*
* 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.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import java.io.IOException;
public class Command {
//directly modified by JavaCommand
protected boolean javaCommand = false;
protected Context context = null;
public int totalOutput = 0;
public int totalOutputProcessed = 0;
ExecutionMonitor executionMonitor = null;
Handler mHandler = null;
//Has this command already been used?
protected boolean used = false;
boolean executing = false;
String[] command = {};
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);
}
//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 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 void finish()
{
RootShell.log("Command finished at users request!");
commandFinished();
}
protected final void finishCommand() {
this.executing = false;
this.finished = true;
this.notifyAll();
}
public final String getCommand() {
StringBuilder sb = new StringBuilder();
if(javaCommand) {
String filePath = context.getFilesDir().getPath();
for (int i = 0; i < command.length; i++) {
/*
* TODO Make withFramework optional for applications
* that do not require access to the fw. -CFR
*/
//export CLASSPATH=/data/user/0/ch.masshardt.emailnotification/files/anbuild.dex ; app_process /system/bin
if (Build.VERSION.SDK_INT > 22) {
//dalvikvm command is not working in Android Marshmallow
sb.append(
"export CLASSPATH=" + filePath + "/anbuild.dex;"
+ " app_process /system/bin "
+ command[i]);
} else {
sb.append(
"dalvikvm -cp " + filePath + "/anbuild.dex"
+ " com.android.internal.util.WithFramework"
+ " com.stericson.RootTools.containers.RootClass "
+ command[i]);
}
sb.append('\n');
}
}
else {
for (int i = 0; i < command.length; i++) {
sb.append(command[i]);
sb.append('\n');
}
}
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() {
this.used = true;
executionMonitor = new ExecutionMonitor(this);
executionMonitor.setPriority(Thread.MIN_PRIORITY);
executionMonitor.start();
executing = true;
}
public final void terminate()
{
RootShell.log("Terminating command at users request!");
terminated("Terminated at users request!");
}
protected 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 {
private final Command command;
public ExecutionMonitor(Command command) {
this.command = command;
}
public void run() {
if(command.timeout > 0)
{
synchronized (command) {
try {
RootShell.log("Command " + command.id + " is waiting for: " + command.timeout);
command.wait(command.timeout);
} catch (InterruptedException e) {
RootShell.log("Exception: " + e);
}
if (!command.isFinished()) {
RootShell.log("Timeout Exception has occurred for command: " + command.id + ".");
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;
}
}
}
}

View File

@ -0,0 +1,58 @@
package com.stericson.RootShell.execution;
import android.content.Context;
public class JavaCommand extends Command
{
/**
* Constructor for executing Java commands rather than binaries
*
* @param context needed to execute java command.
*/
public JavaCommand(int id, Context context, String... command) {
super(id, command);
this.context = context;
this.javaCommand = true;
}
/**
* Constructor for executing Java commands rather than binaries
*
* @param context needed to execute java command.
*/
public JavaCommand(int id, boolean handlerEnabled, Context context, String... command) {
super(id, handlerEnabled, command);
this.context = context;
this.javaCommand = true;
}
/**
* Constructor for executing Java commands rather than binaries
*
* @param context needed to execute java command.
*/
public JavaCommand(int id, int timeout, Context context, String... command) {
super(id, timeout, command);
this.context = context;
this.javaCommand = true;
}
@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
}
}

View File

@ -1 +1 @@
include ':app', ':libmynteye'
include ':app', ':libmynteye', ':libshell'