Взаимодейтсвие Java и Shell-скриптов в Android

Так сложилось, что в моём текущем проекте необходимо было реализовать выполнение shell-скриптов прямиком из кода. Для того, чтобы войти в курс дела, советую вам прочитать эту статью: Shell-скриптинг в среде Android В ней очень хорошо описаны возможности языка Shell, однако мне помимо самих скриптов нужно было выполнять методы Java. В процессе разработки был использован образ Android_x86. Однако можно использовать рутованный телефон (наличие прав суперпользователя обязательно). Сами скрипты из Java выполняются довольно просто:

/** 
* Метод выполняет скрипты shell в отдельном потоке.
*
* @param command shell скрипт.
*/
public void runCommand(final String command) {
// Чтобы не вис интерфейс, запускаем в другом потоке
new Thread(new Runnable() {
public void run() {
OutputStream out = null;
InputStream in = null;
try {
// Отправляем скрипт в рантайм процесс
Process child = Runtime.getRuntime().exec(command);
// Выходной и входной потоки
out = child.getOutputStream();
in = child.getInputStream();
//Входной поток может что-нибудь вернуть
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(in));
String line;
String result = "";
while ((line = bufferedReader.readLine()) != null)
result += line;
//Обработка того, что он вернул
handleBashCommandsResult(result);
} catch (IOException e) {
e.printStackTrace();
} finally {
if (in != null) {
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
} if (out != null) {
try {
out.flush();
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}).start();
}

В Android_x86 можно отправлять скрипты сразу в процесс, и они буду выполняться. Если использовать телефон, то вот эту строчку:

Process child = Runtime.getRuntime().exec(command);

нужно заменить на вот эти:

Process child = Runtime.getRuntime().exec(new String[] { "su", "-c", "system/bin/sh" }); 
DataOutputStream stdin = new DataOutputStream(child.getOutputStream());
//Скрипт
stdin.writeBytes(command);

Я не знаю, чем можно объяснить это, но на телефоне перед выполнением скриптов нужно запустить командную оболочку. В остальном всё одинаково. Для примера напишем несколько shell-команд, которые потом можно будет собирать в скрипты:

/** 
* Пауза между командами
*
* @param i секунд
* @return команда
*/
public static String doSleep(int i) {
return "adb shell 'sleep " + i + "' ;";
}
/**
* Жест свайп
*
* @param x1 откуда
* @param y1 откуда
* @param x2 куда
* @param y2 куда
* @return команда
*/
public static String doSwipe(int x1, int y1, int x2, int y2) {
return "adb shell input swipe " + x1 + " " + y1 + " " + x2 + " " + y2 + " ;";
}
/**
* Жест тап
*
* @param x
* @param y
* @return команда
*/
public static String doTap(int x, int y) {
return "adb shell input tap " + x + " " + y + " ;";
}
/**
* Ввод текста в поле ввода
*
* @param text текст
* @return команда
*/
public static String doInputText(String text) {
return "adb shell input text " + text + " ;";
}
/**
* Нажатие кнопки
*
* @param keycode код кнопки
* @return команда
*/
public static String doInputKeyevent(int keycode) {
return "adb shell input keyevent " + keycode + " ;";
}
/**
* Вывод сообщения (которое потом во входном потоке ловится)
*
* @param message сообщение
* @return команда
*/
public static String echo(String message) {
return "echo '" + message + "' ;";
}

Данные команды можно также комбинировать в различные скрипты:

/** 
* Пример скрипта
*
* @return скрипт
*/
public static String sampleScript(){
String command = "";
command = command.concat(doSwipe(100, 200, 100, 500))
.concat(doSleep(1))
.concat(doTap(100, 150))
.concat(doSleep(1))
.concat(doInputText("Я скрипт"))
.concat(doSleep(1))
.concat(doInputKeyevent(KeyEvent.KEYCODE_ENTER))
.concat(doSleep(1))
.concat(echo("SCRIPT_FINISHED"));
return command;
}

Вызвать данный скрипт очень просто:

runCommand(sampleScript());

Данный скрипт с интервалом в 1 секунду сначала свайпает, затем тапает, затем вводит текст, эмулирует нажатие клавиши Enter и затем посылает сообщение с сигналом о завершении. Теперь самое интересное. Данный скрипт будет выполняться в фоне несколько секунд, поскольку в нём выставлены задержки. По его завершению во входном потоке будет это сообщение. В моём примере я преобразовываю его в строку, и после этого вызываю метод handleBashCommandsResult(result), который в качестве входного параметра и принимает эту строку. В этом методе результат можно сравнить

/** 
* Обработка того, что вернул скрипт (возвращает он обычно ключевое слово командой echo)
*
* @param result ответ скрипта.
*/
private void handleBashCommandsResult(String result) {
if (result.contains("SCRIPT_FINISHED")) {
//Здесь делаем всё что хотели сделать после завершение скрипта
} else if (.....){
//А вот здесь можно сделать что-нибудь после другого скрипта
} else {
//А вот здесь можно сделать всё остальное
}
}

На этом в общем то и всё. В методе handleBashCommandsResult можно выполнить, например, какие-нибудь проверки и запустить выполнение одного из нескольких других скриптов, в зависимости от результатов этой проверки. Так или иначе, я кратко описал, как наладить взаимодейтвие shell-скриптов и Java кода, что в общем-то и хотел. Надеюсь, кому-нибудь это может пригодиться. Если остались какие-либо вопросы, я постараюсь вам ответить.

Originally published at habrahabr.ru.