Так сложилось, что в моём текущем проекте необходимо было реализовать выполнение 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 кода, что в общем-то и хотел.

Надеюсь, кому-нибудь это может пригодиться. Если остались какие-либо вопросы, я постараюсь вам ответить.

Комментарии (6)


  1. eaa
    04.08.2015 14:52

    А если скрипт завис или выполняется достаточно долго — как эти ситуации отрабатываются?


  1. iwuvhugs
    04.08.2015 17:14

    Я не сталкивался со случаями, чтобы скрипты зависали. Они могут либо выполняться, либо нет. Проверку на то, выполнился ли скрипт, можно добавить в последний описанный метод. Другой вопрос — проверка, выполнил ли скрипт то, что мы хотели или нет. У меня есть несколько решений, в ближайшие дни напишу об этом.


    1. eaa
      04.08.2015 17:33

      Чтоб скрипт выполнялся долго — достаточно дать ему большой объем данных. Так или иначе, бывает, что скрипт выполняется, условно, час.
      И нас это не устраивает. Надо его прибить, допустим таймаут 5 минут, если больше — прибить скрипт и дальше адекватно обработать эту ситуацию.
      Как это сделать?


      1. iamironz
        04.08.2015 20:11
        +1

        Если именно прибить — то вызвать метод «destroy» объекта «Process», исполняющего вашу комманду.


      1. iwuvhugs
        04.08.2015 20:58

        Честно, не пробовал прибивать сам скрипт.
        Использовал скрипт, который прибивал системный процесс, в результате чего происходил soft reboot:

        su -c kill -9 `ps | grep system_server | awk '{print $2}'`
        

        Если каким-то образом будет известно имя процесса или его id, то можно таким скриптом прибивать другие скрипты


  1. r_ii
    05.08.2015 18:59

    Еще наверное можно (и нужно) проверять код завершения процесса.