package com.rincliu.library.common.persistence.rootmanager;
import java.io.File;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
import android.text.TextUtils;
import com.rincliu.library.common.persistence.rootmanager.container.Command;
import com.rincliu.library.common.persistence.rootmanager.container.Result;
import com.rincliu.library.common.persistence.rootmanager.container.Shell;
import com.rincliu.library.common.persistence.rootmanager.container.Result.ResultBuilder;
import com.rincliu.library.common.persistence.rootmanager.exception.PermissionException;
import com.rincliu.library.common.persistence.rootmanager.utils.Remounter;
import com.rincliu.library.common.persistence.rootmanager.utils.RootUtils;
/**
* This class is the main interface of RootManager.
*
* @author Chris Jiang
*/
public class RootManager {
private static RootManager instance;
/* The useful members */
private Boolean hasRooted = null;
private boolean hasGivenPermission = false;
private long lastPermissionCheck = -1;
private RootManager() {
}
public static synchronized RootManager getInstance() {
if (instance == null) {
instance = new RootManager();
}
return instance;
}
/**
* Try to check if the device has been rooted.
* <p>
* Generally speaking, a device which has been rooted successfully must
* have a binary file named SU. However, SU may not work well for those
* devices rooted unfinished.
* </p>
*
* @return the result whether this device has been rooted.
*/
public boolean hasRooted() {
if (hasRooted == null) {
for (String path : Constants.SU_BINARY_DIRS) {
File su = new File(path + "/su");
if (su.exists()) {
hasRooted = true;
break;
} else {
hasRooted = false;
}
}
}
return hasRooted;
}
/**
* Try to grant root permission.
* <p>
* This function may result in a popup dialog to users, wait for the
* user's choice and operation, return the result then.
* </p>
*
* @return whether your app has been given the root permission by user.
*/
public boolean grantPermission() {
if (!hasGivenPermission) {
hasGivenPermission = accessRoot();
lastPermissionCheck = System.currentTimeMillis();
} else {
if (lastPermissionCheck < 0
|| System.currentTimeMillis() - lastPermissionCheck > Constants.PERMISSION_EXPIRE_TIME) {
hasGivenPermission = accessRoot();
lastPermissionCheck = System.currentTimeMillis();
}
}
return hasGivenPermission;
}
/**
* Install a user app on the device.
* <p>
* For performance, do NOT call this function on UI thread,
* {@link IllegalStateException} will be thrown if you do so.
* </p>
*
* @param apkPath the file path of APK, do not start with
* <I>"file://"</I>. For example, <I>"/sdcard/Tech_test.apk"<I>
* is good. Please do NOT contain non-ASCII chars.
* @return The result of run command operation or install operation.
*/
public Result installPackage(String apkPath) {
return installPackage(apkPath, "a");
}
/**
* Install a user app on the device.
* <p>
* For performance, do NOT call this function on UI thread,
* {@link IllegalStateException} will be thrown if you do so.
* </p>
*
* @param apkPath the file path of apk, do not start with
* <I>"file://"</I>.For example, <I>"/sdcard/Tech_test.apk"<I>
* is good. Please do NOT contain non-ASCII chars.
* @param installLocation the location of install.
* <ul>
* <li>auto means chooing the install location automatic.</li>
* <li>ex means install the app on sdcard.</li>
* <li>in means install the app in phone ram</li>
* </ul>
* @return The result of run command operation or install operation.
*/
public Result installPackage(String apkPath, String installLocation) {
RootUtils.checkUIThread();
final ResultBuilder builder = Result.newBuilder();
if (TextUtils.isEmpty(apkPath)) {
return builder.setFailed().build();
}
String command = Constants.COMMAND_INSTALL;
if (RootUtils.isNeedPathSDK()) {
command = Constants.COMMAND_INSTALL_PATCH + command;
}
command = command + apkPath;
if (TextUtils.isEmpty(installLocation)) {
if (installLocation.equalsIgnoreCase("ex")) {
command = command + Constants.COMMAND_INSTALL_LOCATION_EXTERNAL;
} else if (installLocation.equalsIgnoreCase("in")) {
command = command + Constants.COMMAND_INSTALL_LOCATION_INTERNAL;
}
}
final StringBuilder infoSb = new StringBuilder();
Command commandImpl = new Command(command) {
@Override
public void onUpdate(int id, String message) {
infoSb.append(message + "\n");
}
@Override
public void onFinished(int id) {
String finalInfo = infoSb.toString();
if (TextUtils.isEmpty(finalInfo)) {
builder.setInstallFailed();
} else {
if (finalInfo.contains("success") || finalInfo.contains("Success")) {
builder.setInstallSuccess();
} else if (finalInfo.contains("failed") || finalInfo.contains("FAILED")) {
if (finalInfo.contains("FAILED_INSUFFICIENT_STORAGE")) {
builder.setInsallFailedNoSpace();
} else if (finalInfo.contains("FAILED_INCONSISTENT_CERTIFICATES")) {
builder.setInstallFailedWrongCer();
} else if (finalInfo.contains("FAILED_CONTAINER_ERROR")) {
builder.setInstallFailedWrongCer();
} else {
builder.setInstallFailed();
}
} else {
builder.setInstallFailed();
}
}
}
};
try {
Shell.startRootShell().add(commandImpl).waitForFinish();
} catch (InterruptedException e) {
e.printStackTrace();
builder.setCommandFailedInterrupted();
} catch (IOException e) {
e.printStackTrace();
builder.setCommandFailed();
} catch (TimeoutException e) {
e.printStackTrace();
builder.setCommandFailedTimeout();
} catch (PermissionException e) {
e.printStackTrace();
builder.setCommandFailedDenied();
}
return builder.build();
}
/**
* Uninstall a user app using its package name.
* <p>
* For performance, do NOT call this function on UI thread,
* {@link IllegalStateException} will be thrown if you do so.
* </p>
*
* @param packageName the app's package name you want to uninstall.
* @return The result of run command operation or uninstall operation.
*/
public Result uninstallPackage(String packageName) {
RootUtils.checkUIThread();
final ResultBuilder builder = Result.newBuilder();
if (TextUtils.isEmpty(packageName)) {
return builder.setFailed().build();
}
String command = Constants.COMMAND_UNINSTALL + packageName;
final StringBuilder infoSb = new StringBuilder();
Command commandImpl = new Command(command) {
@Override
public void onUpdate(int id, String message) {
infoSb.append(message + "\n");
}
@Override
public void onFinished(int id) {
String finalInfo = infoSb.toString();
if (TextUtils.isEmpty(finalInfo)) {
builder.setUninstallFailed();
} else {
if (finalInfo.contains("Success") || finalInfo.contains("success")) {
builder.setUninstallSuccess();
} else {
builder.setUninstallFailed();
}
}
}
};
try {
Shell.startRootShell().add(commandImpl).waitForFinish();
} catch (InterruptedException e) {
e.printStackTrace();
builder.setCommandFailedInterrupted();
} catch (IOException e) {
e.printStackTrace();
builder.setCommandFailed();
} catch (TimeoutException e) {
e.printStackTrace();
builder.setCommandFailedTimeout();
} catch (PermissionException e) {
e.printStackTrace();
builder.setCommandFailedDenied();
}
return builder.build();
}
/**
* Uninstall a system app.
* <p>
* For performance, do NOT call this function on UI thread,
* {@link IllegalStateException} will be thrown if you do so.
* </p>
*
* @param apkPath the source apk path of system app.
* @return The result of run command operation or uninstall operation.
*/
public Result uninstallSystemApp(String apkPath) {
RootUtils.checkUIThread();
ResultBuilder builder = Result.newBuilder();
if (TextUtils.isEmpty(apkPath)) {
return builder.setFailed().build();
}
if (remount(Constants.PATH_SYSTEM, "rw")) {
File apkFile = new File(apkPath);
if (apkFile.exists()) {
return runCommand("rm '" + apkPath + "'");
}
}
return builder.setFailed().build();
}
/**
* Install a binary file into <I>"/system/bin/"</I>
*
* @param filePath The target of the binary file.
* @return the operation result.
*/
public boolean installBinary(String filePath) {
if (TextUtils.isEmpty(filePath)) {
return false;
}
File file = new File(filePath);
if (!file.exists()) {
return false;
}
return copyFile(filePath, Constants.PATH_SYSTEM_BIN);
}
/**
* Uninstall a binary from <I>"/system/bin/"</I>
*
* @param fileName, the name of target file.
* @return the operation result.
*/
public boolean removeBinary(String fileName) {
if (TextUtils.isEmpty(fileName)) {
return false;
}
File file = new File(Constants.PATH_SYSTEM_BIN + fileName);
if (!file.exists()) {
return false;
}
if (remount(Constants.PATH_SYSTEM, "rw")) {
return runCommand("rm '" + Constants.PATH_SYSTEM_BIN + fileName + "'").getResult();
} else {
return false;
}
}
/**
* Copy a file into destination dir.
* <p>
* Because Android do not support <I>"cp"</I> command by default,
* <i>"cat source > destination"</i> will be used.
* </p>
*
* @param source the source file path.
* @param destinationDir the destination dir path.
* @return the operation result.
*/
public boolean copyFile(String source, String destinationDir) {
if (TextUtils.isEmpty(destinationDir) || TextUtils.isEmpty(source)) {
return false;
}
File sourceFile = new File(source);
File desFile = new File(destinationDir);
if (!sourceFile.exists() || !desFile.isDirectory()) {
return false;
}
if (remount(destinationDir, "rw")) {
return runCommand("cat '" + source + "' > " + destinationDir).getResult();
} else {
return false;
}
}
/**
* Remount a path file as the type.
*
* @param path the path you want to remount
* @param mountType the mount type, including, <i>"ro" means read only,
* "rw" means read and write</i>
* @return the operation result.
*/
public boolean remount(String path, String mountType) {
if (TextUtils.isEmpty(path) || TextUtils.isEmpty(mountType)) {
return false;
}
if (mountType.equalsIgnoreCase("rw") || mountType.equalsIgnoreCase("ro")) {
return Remounter.remount(path, mountType);
} else {
return false;
}
}
/**
* Run a binary which exit in <i>"/system/bin/"</i>
*
* @param binaryName the file name of binary, containing params if
* necessary.
* @return the operation result.
*/
public Result runBinBinary(String binaryName) {
ResultBuilder builder = Result.newBuilder();
if (TextUtils.isEmpty(binaryName)) {
return builder.setFailed().build();
}
return runBinary(Constants.PATH_SYSTEM_BIN + binaryName);
}
/**
* Run a binary file.
*
* @param path the file path of binary, containing params if necessary.
* @return the operation result.
*/
public Result runBinary(String path) {
return runCommand(path);
}
/**
* Run a command in default shell.
*
* @param command the command string.
* @return the operation result.
*/
public Result runCommand(String command) {
final ResultBuilder builder = Result.newBuilder();
if (TextUtils.isEmpty(command)) {
return builder.setFailed().build();
}
final StringBuilder infoSb = new StringBuilder();
Command commandImpl = new Command(command) {
@Override
public void onUpdate(int id, String message) {
infoSb.append(message + "\n");
}
@Override
public void onFinished(int id) {
builder.setCustomMessage(infoSb.toString());
}
};
try {
Shell.startRootShell().add(commandImpl).waitForFinish();
} catch (InterruptedException e) {
e.printStackTrace();
builder.setCommandFailedInterrupted();
} catch (IOException e) {
e.printStackTrace();
builder.setCommandFailed();
} catch (TimeoutException e) {
e.printStackTrace();
builder.setCommandFailedTimeout();
} catch (PermissionException e) {
e.printStackTrace();
builder.setCommandFailedDenied();
}
return builder.build();
}
/**
* Get a screen cap and save into the specific path.
*
* @param path the path with file name and extend name.
* @return the operation result.
*/
public boolean screenCap(String path) {
if (TextUtils.isEmpty(path)) {
return false;
}
Result res = runCommand(Constants.COMMAND_SCREENCAP + path);
RootUtils.Log((res == null) + "");
return res.getResult();
}
/**
* Check whether a process is running.
*
* @param processName the name of process. For user app, the process name
* is its package name.
* @return whether this process is currently running.
*/
public boolean isProcessRunning(String processName) {
if (TextUtils.isEmpty(processName)) {
return false;
}
Result infos = runCommand(Constants.COMMAND_PS);
return infos.getMessage().contains(processName);
}
/**
* Kill a process by its name.
*
* @param processName the name of this process. For user app, the process
* name is its package name.
* @return the result of operation.
*/
public boolean killProcessByName(String processName) {
if (TextUtils.isEmpty(processName)) {
return false;
}
Result res = runCommand(Constants.COMMAND_PIDOF + processName);
if (!TextUtils.isEmpty(res.getMessage())) {
return killProcessById(res.getMessage());
} else {
return false;
}
}
/**
* Kill a process by its process id, hence pid.
*
* @param processID the PID of this process.
* @return the result of this operation.
*/
public boolean killProcessById(String processID) {
if (TextUtils.isEmpty(processID)) {
return false;
}
Result res = runCommand(Constants.COMMAND_KILL + processID);
return res.getResult();
}
/**
* Restart the device.
*/
public void restartDevice() {
killProcessByName("zygote");
}
private static boolean accessRoot = false;
private boolean accessRoot() {
boolean result = false;
accessRoot = false;
Command commandImpl = new Command("id") {
@Override
public void onUpdate(int id, String message) {
if (message != null && message.toLowerCase().contains("uid=0")) {
accessRoot = true;
}
}
@Override
public void onFinished(int id) {
}
};
try {
Shell.startRootShell().add(commandImpl).waitForFinish();
result = accessRoot;
} catch (InterruptedException e) {
e.printStackTrace();
result = false;
} catch (IOException e) {
e.printStackTrace();
result = false;
} catch (TimeoutException e) {
e.printStackTrace();
result = false;
} catch (PermissionException e) {
e.printStackTrace();
result = false;
}
return result;
}
}