September 26, 2018

shell команды на Android за ~100 строк кода

Мой канал: @VolfsChannel

Сегодня мы попробуем создать мини-приложение терминал, рассмотрим основные методы работы с shell-командами на Android.

Android основан на ядре Linux. Также как и на Linux в нем можно выполнять команды через терминал. Терминал в Android по своей сути просто является нескучной обоиной для выполнения функций рантайма. Чтож, заглянем глубже. Создадим простейшее приложение которое будет отдавать shell-команды и получать результат в виде строки. Ко всему прочему наше приложение сможет использовать Root.

100 строчек кода...

Создадим новый класс, я назвал его CommandExecutor.java и напишите следующий код:

import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;

public class CommandExecutor {

		// Модель для получения результата
		public static class ResultData {
				// Код результата
				private int resultCode;
				// Результат отработки команды
				private String resultData;
				// Поле для записи ошибок
				private String resultError;

				// Конструктор
				ResultData(int code, String info, String err) {
						resultCode = code;
						resultData = info == null ? "" : info;
						resultError = err == null ? "" : err;
					}
				// Геттеры полей
				public int getResultCode() {
						return resultCode;
					}
				public String getResultData() {
						return resultData;
					}
				public String getResultError() {
						return resultError;
					}
				public String getResult() {
						return toString();
					}
				public String toString() {
						return resultData + "\n" + resultError;
					}
			}
		// Выполнение простой sh-команды
		public static ResultData execute(String command) {
				return execute(false, command);
			}
		// То же что и выше, но из под рута
		public static ResultData executeSu(String command) {
				return execute(true, command);
			}
		// Если нужно изменять значение su динамически
		public static ResultData execute(boolean su, String command) {
				return execPool(su, rmSlashN(command).split("\n"));
			}
		// Выполняем массив команд
		private static ResultData execPool(boolean su, String[] commands) {
				// Обьявление переменных
				Process exec = null;
				InputStream execIn = null;
				InputStream execErr = null;
				OutputStream execOs = null;

				ResultData resultData;
				try {
						// В зависимости от значения su меняем параметр запуска команды
						// От рута или же от юзера
						exec = Runtime.getRuntime().exec(su ? "su" : "sh");
						execIn = exec.getInputStream();
						execErr = exec.getErrorStream();
						execOs = exec.getOutputStream();
						DataOutputStream dos = new DataOutputStream(execOs);
						// В цикле проходимся по всем отосланным командам
						for (String com : commands)
							if (!com.isEmpty()) {
									dos.writeBytes(com + "\n");
									dos.flush();
								}
						dos.close();
						// Посылаем команду и ждём ответа от оболочки
						resultData = new ResultData(exec.waitFor(), inputStream2String(execIn, "utf-8"), inputStream2String(execErr, "utf-8"));
					} catch (Exception e) {
						resultData = new ResultData(-1, "", e.toString());
					} finally {
						try {
								// Не забываем закрыть все стримы
								if (execIn != null) execIn.close();
								if (execErr != null) execErr.close();
								if (execOs != null) execOs.close();
								// Завершаем процесс выполнения
								if (exec != null) exec.destroy();
							} catch (Exception ignored) {}
					}
				return resultData;
			}

		// Преобразование стрима в строку
		public static String inputStream2String(InputStream in, String encoding) throws Exception {
				StringBuilder out = new StringBuilder();
				InputStreamReader inread = new InputStreamReader(in, encoding);
				char[] b = new char[1024];
				int n;
				while ((n = inread.read(b)) != -1) {
						String s = new String(b, 0, n);
						out.append(s);
					}
				return out.toString();
			}

		// Чтение стрима в строку
		public static String read(InputStream is) {
				try {
						ByteArrayOutputStream baos = new ByteArrayOutputStream();
						byte[] buffer = new byte[1024];
						int length;
						while ((length = is.read(buffer)) != -1)
							baos.write(buffer, 0, length);
						return baos.toString("UTF-8");
					} catch (Exception e) { e.printStackTrace(); return e.toString(); }
			}

		// Удаляем лишние переносы строки
		public static String rmSlashN(String text) {
				while (text.contains("\n\n")) text = text.replace("\n\n", "\n");
				if (text.startsWith("\n"))
					text = text.substring(1);
				if (text.endsWith("\n"))
					text = text.substring(0, text.length() - 1);

				return text;
			}
}

Я добавил комментарии к коду. Если у вас возникнут вопросы - пишите их в бота.

Грубо говоря: мы шлём команду, ждём пока система ответит, и выполняем другие команды дальше.

В примере приложения я использовал метод execute(boolean su, String command) т.к. значение su у меня зависит от чекбокса.

Пример вызова

String out = CommandExecutor.execute(true, "magiskhide --ls").getResult();

Выведет список приложений от которых скрывается рут в MagiskHide.

Log.d("SU-TEST", out);

Создаем приложение

Создайте новый проект в IDE. Откройте MainActivity.java и напишите следующий код:

import android.app.Activity;
import android.os.Bundle;
import android.widget.Button;
import android.widget.EditText;
import android.widget.CheckBox;
import android.widget.TextView;
import android.view.View.OnClickListener;
import android.view.View;

public class MainActivity extends Activity {
	private Button btnExec;
	private EditText fieldCommand;
	private CheckBox boxSu;
	private TextView textOut;
	
		@Override
		protected void onCreate(Bundle savedInstanceState) {
				super.onCreate(savedInstanceState);
				setContentView(R.layout.main);
				
				fieldCommand = findViewById(R.id.field_exec);
				boxSu = findViewById(R.id.checbox_su);
				btnExec = findViewById(R.id.btn_execute);
				textOut = findViewById(R.id.command_output);
				
				btnExec.setOnClickListener(new OnClickListener(){

							@Override
							public void onClick(View p1) {
									String out = CommandExecutor.execute(boxSu.isChecked(), fieldCommand.getText().toString()).getResult();
									textOut.setText(out);
								}
							
					
				});
			}
	}

Измените разметку главного экрана (в моем случае main.xml)

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
	xmlns:android="http://schemas.android.com/apk/res/android"
	android:layout_width="match_parent"
	android:layout_height="match_parent"
	android:orientation="vertical"
	android:padding="16dp">

	<EditText
		android:layout_width="match_parent"
		android:maxLines="2"
		android:layout_height="wrap_content"
		android:hint="command"
		android:id="@+id/field_exec"/>

	<CheckBox
		android:layout_marginTop="16dp"
		android:text="Super User"
		android:layout_width="wrap_content"
		android:layout_height="wrap_content"
		android:id="@+id/checbox_su"/>

	<Button
		android:layout_width="wrap_content"
		android:layout_height="wrap_content"
		android:text="Execute"
		android:textAllCaps="false"
		android:layout_gravity="end"
		android:id="@+id/btn_execute"/>

	<ScrollView
		android:layout_width="wrap_content"
		android:layout_height="wrap_content">

		<TextView
			android:layout_width="wrap_content"
			android:layout_height="wrap_content"
			android:typeface="monospace"
			android:textSize="12sp"
			android:id="@+id/command_output"/>

	</ScrollView>

</LinearLayout>

Скопируйте класс CommandExecutor.java к себе в проект.

Все ошибки должны пропасть. Запустите приложение и попробуйте выполнить любую shell-команду.

Результат

Выполнение команды без рута

То же самое с рутом

Приложение и его исходный код можно найти на канале: @VolfsChannel