package de.robv.android.xposed.installer.util; import android.content.Context; import android.os.Handler; import android.os.HandlerThread; import android.support.annotation.IdRes; import android.support.annotation.NonNull; import android.support.annotation.StringRes; import android.text.TextUtils; import android.util.Log; import java.io.File; import java.io.IOException; import java.util.LinkedList; import java.util.List; import de.robv.android.xposed.installer.R; import de.robv.android.xposed.installer.XposedApp; import de.robv.android.xposed.installer.installation.FlashCallback; import eu.chainfire.libsuperuser.Shell; import eu.chainfire.libsuperuser.Shell.OnCommandResultListener; import static de.robv.android.xposed.installer.util.InstallZipUtil.triggerError; public class RootUtil { private Shell.Interactive mShell = null; private HandlerThread mCallbackThread = null; private boolean mCommandRunning = false; private int mLastExitCode = -1; private LineCallback mCallback = null; private static final String EMULATED_STORAGE_SOURCE; private static final String EMULATED_STORAGE_TARGET; static { EMULATED_STORAGE_SOURCE = getEmulatedStorageVariable("EMULATED_STORAGE_SOURCE"); EMULATED_STORAGE_TARGET = getEmulatedStorageVariable("EMULATED_STORAGE_TARGET"); } public interface LineCallback { void onLine(String line); void onErrorLine(String line); } public static class CollectingLineCallback implements LineCallback { protected List<String> mLines = new LinkedList<>(); @Override public void onLine(String line) { mLines.add(line); } @Override public void onErrorLine(String line) { mLines.add(line); } @Override public String toString() { return TextUtils.join("\n", mLines); } } public static class LogLineCallback implements LineCallback { @Override public void onLine(String line) { Log.i(XposedApp.TAG, line); } @Override public void onErrorLine(String line) { Log.e(XposedApp.TAG, line); } } private static String getEmulatedStorageVariable(String variable) { String result = System.getenv(variable); if (result != null) { result = getCanonicalPath(new File(result)); if (!result.endsWith("/")) { result += "/"; } } return result; } private final Shell.OnCommandResultListener mOpenListener = new Shell.OnCommandResultListener() { @Override public void onCommandResult(int commandCode, int exitCode, List<String> output) { mStdoutListener.onCommandResult(commandCode, exitCode); } }; private final Shell.OnCommandLineListener mStdoutListener = new Shell.OnCommandLineListener() { public void onLine(String line) { if (mCallback != null) { mCallback.onLine(line); } } @Override public void onCommandResult(int commandCode, int exitCode) { mLastExitCode = exitCode; synchronized (mCallbackThread) { mCommandRunning = false; mCallbackThread.notifyAll(); } } }; private final Shell.OnCommandLineListener mStderrListener = new Shell.OnCommandLineListener() { @Override public void onLine(String line) { if (mCallback != null) { mCallback.onErrorLine(line); } } @Override public void onCommandResult(int commandCode, int exitCode) { // Not called for STDERR listener. } }; private void waitForCommandFinished() { synchronized (mCallbackThread) { while (mCommandRunning) { try { mCallbackThread.wait(); } catch (InterruptedException ignored) { } } } if (mLastExitCode == OnCommandResultListener.WATCHDOG_EXIT || mLastExitCode == OnCommandResultListener.SHELL_DIED) { dispose(); } } /** * Starts an interactive shell with root permissions. Does nothing if * already running. * * @return true if root access is available, false otherwise */ public synchronized boolean startShell() { if (mShell != null) { if (mShell.isRunning()) { return true; } else { dispose(); } } mCallbackThread = new HandlerThread("su callback listener"); mCallbackThread.start(); mCommandRunning = true; mShell = new Shell.Builder().useSU() .setHandler(new Handler(mCallbackThread.getLooper())) .setOnSTDERRLineListener(mStderrListener) .open(mOpenListener); waitForCommandFinished(); if (mLastExitCode != OnCommandResultListener.SHELL_RUNNING) { dispose(); return false; } return true; } public boolean startShell(FlashCallback flashCallback) { if (!startShell()) { triggerError(flashCallback, FlashCallback.ERROR_NO_ROOT_ACCESS); return false; } return true; } /** * Closes all resources related to the shell. */ public synchronized void dispose() { if (mShell == null) { return; } try { mShell.close(); } catch (Exception ignored) { } mShell = null; mCallbackThread.quit(); mCallbackThread = null; } public synchronized int execute(String command, LineCallback callback) { if (mShell == null) { throw new IllegalStateException("shell is not running"); } mCallback = callback; mCommandRunning = true; mShell.addCommand(command, 0, mStdoutListener); waitForCommandFinished(); return mLastExitCode; } public int executeWithBusybox(String command, LineCallback callback) { AssetUtil.extractBusybox(); return execute(AssetUtil.BUSYBOX_FILE.getAbsolutePath() + " " + command, callback); } private static String getCanonicalPath(File file) { try { return file.getCanonicalPath(); } catch (IOException e) { Log.w(XposedApp.TAG, "Could not get canonical path for " + file); return file.getAbsolutePath(); } } public static String getShellPath(File file) { return getShellPath(getCanonicalPath(file)); } public static String getShellPath(String path) { if (EMULATED_STORAGE_SOURCE != null && EMULATED_STORAGE_TARGET != null && path.startsWith(EMULATED_STORAGE_TARGET)) { path = EMULATED_STORAGE_SOURCE + path.substring(EMULATED_STORAGE_TARGET.length()); } return path; } @Override protected void finalize() throws Throwable { dispose(); } public enum RebootMode { NORMAL(R.string.reboot), SOFT(R.string.soft_reboot), RECOVERY(R.string.reboot_recovery); public final int titleRes; RebootMode(@StringRes int titleRes) { this.titleRes = titleRes; } public static RebootMode fromId(@IdRes int id) { switch (id) { case R.id.reboot: return NORMAL; case R.id.soft_reboot: return SOFT; case R.id.reboot_recovery: return RECOVERY; default: throw new IllegalArgumentException(); } } } public static boolean reboot(RebootMode mode, @NonNull Context context) { RootUtil rootUtil = new RootUtil(); if (!rootUtil.startShell()) { NavUtil.showMessage(context, context.getString(R.string.root_failed)); return false; } LineCallback callback = new CollectingLineCallback(); if (!rootUtil.reboot(mode, callback)) { StringBuilder message = new StringBuilder(callback.toString()); if (message.length() > 0) { message.append("\n\n"); } message.append(context.getString(R.string.reboot_failed)); NavUtil.showMessage(context, message); return false; } return true; } public boolean reboot(RebootMode mode, LineCallback callback) { switch (mode) { case NORMAL: return reboot(callback); case SOFT: return softReboot(callback); case RECOVERY: return rebootToRecovery(callback); default: throw new IllegalArgumentException(); } } private boolean reboot(LineCallback callback) { return executeWithBusybox("reboot", callback) == 0; } private boolean softReboot(LineCallback callback) { return execute("setprop ctl.restart surfaceflinger; setprop ctl.restart zygote", callback) == 0; } private boolean rebootToRecovery(LineCallback callback) { // Create a flag used by some kernels to boot into recovery. if (execute("ls /cache/recovery", null) != 0) { executeWithBusybox("mkdir /cache/recovery", callback); } executeWithBusybox("touch /cache/recovery/boot", callback); return executeWithBusybox("reboot recovery", callback) == 0; } }