getPids() {
- return pids;
- }
-
- public String getPidsString() {
- StringBuilder sb = new StringBuilder();
- for (String s : pids) {
- sb.append(s);
- sb.append(" ");
- }
-
- return sb.toString();
- }
-
- @Override
- public void output(int id, String line) {
- // general check if line contains processName
- if (line.contains(processName)) {
- Matcher psMatcher = psPattern.matcher(line);
-
- // try to match line exactly
- try {
- if (psMatcher.find()) {
- String pid = psMatcher.group(1);
- // add to pids list
- pids.add(pid);
- Log.d(RootCommands.TAG, "Found pid: " + pid);
- } else {
- Log.d(RootCommands.TAG, "Matching in ps command failed!");
- }
- } catch (Exception e) {
- Log.e(RootCommands.TAG, "Error with regex!", e);
- }
- }
- }
-
- @Override
- public void afterExecution(int id, int exitCode) {
- }
-
- }
-
- /**
- * This method can be used to kill a running process
- *
- * (commands: ps, kill)
- *
- * @param processName
- * name of process to kill
- * @return true
if process was found and killed successfully
- * @throws java.io.IOException
- * @throws java.util.concurrent.TimeoutException
- * @throws BrokenBusyboxException
- */
- public boolean killAll(String processName) throws BrokenBusyboxException, TimeoutException,
- IOException {
- Log.d(RootCommands.TAG, "Killing process " + processName);
-
- PsCommand psCommand = new PsCommand(processName);
- shell.add(psCommand).waitForFinish();
-
- // kill processes
- if (!psCommand.getPids().isEmpty()) {
- // example: kill -9 1234 1222 5343
- SimpleCommand killCommand = new SimpleCommand("kill -9 "
- + psCommand.getPidsString());
- shell.add(killCommand).waitForFinish();
-
- if (killCommand.getExitCode() == 0) {
- return true;
- } else {
- return false;
- }
- } else {
- Log.d(RootCommands.TAG, "No pid found! Nothing was killed!");
- return false;
- }
- }
-
- /**
- * Kill a running executable
- *
- * See README for more information how to use your own executables!
- *
- * @param executableName
- * @return
- * @throws BrokenBusyboxException
- * @throws java.util.concurrent.TimeoutException
- * @throws java.io.IOException
- */
- public boolean killAllExecutable(String executableName) throws BrokenBusyboxException,
- TimeoutException, IOException {
- return killAll(ExecutableCommand.EXECUTABLE_PREFIX + executableName + ExecutableCommand.EXECUTABLE_SUFFIX);
- }
-
- /**
- * This method can be used to to check if a process is running
- *
- * @param processName
- * name of process to check
- * @return true
if process was found
- * @throws java.io.IOException
- * @throws BrokenBusyboxException
- * @throws java.util.concurrent.TimeoutException
- * (Could not determine if the process is running)
- */
- public boolean isProcessRunning(String processName) throws BrokenBusyboxException,
- TimeoutException, IOException {
- PsCommand psCommand = new PsCommand(processName);
- shell.add(psCommand).waitForFinish();
-
- // if pids are available process is running!
- if (!psCommand.getPids().isEmpty()) {
- return true;
- } else {
- return false;
- }
- }
-
- /**
- * Checks if binary is running
- *
- * @param binaryName
- * @return
- * @throws BrokenBusyboxException
- * @throws java.util.concurrent.TimeoutException
- * @throws java.io.IOException
- */
- public boolean isBinaryRunning(String binaryName) throws BrokenBusyboxException,
- TimeoutException, IOException {
- return isProcessRunning(ExecutableCommand.EXECUTABLE_PREFIX + binaryName
- + ExecutableCommand.EXECUTABLE_SUFFIX);
- }
-
- /**
- * Ls command to get permissions or symlinks
- */
- private class LsCommand extends Command {
- private String fileName;
- private String permissionRegex;
- private Pattern permissionPattern;
- private String symlinkRegex;
- private Pattern symlinkPattern;
-
- private String symlink;
- private String permissions;
-
- public String getSymlink() {
- return symlink;
- }
-
- public String getPermissions() {
- return permissions;
- }
-
- public LsCommand(String file) {
- super("ls -l " + file);
-
- // get only filename:
- this.fileName = (new File(file)).getName();
- Log.d(RootCommands.TAG, "fileName: " + fileName);
-
- /**
- * regex to get pid out of ps line, example:
- *
- *
- * with busybox:
- * lrwxrwxrwx 1 root root 15 Aug 13 12:14 dev/stdin -> /proc/self/fd/0
- *
- * with toolbox:
- * lrwxrwxrwx root root 15 Aug 13 12:14 stdin -> /proc/self/fd/0
- *
- * Regex:
- * ^.*?(\\S{10}) .* $
- *
- */
- permissionRegex = "^.*?(\\S{10}).*$";
- permissionPattern = Pattern.compile(permissionRegex);
-
- /**
- * regex to get symlink
- *
- *
- * -> /proc/self/fd/0
- * ^.*?\\-\\> \\s+ (.*) $
- *
- */
- symlinkRegex = "^.*?\\-\\>\\s+(.*)$";
- symlinkPattern = Pattern.compile(symlinkRegex);
- }
-
- /**
- * Converts permission string from ls command to numerical value. Example: -rwxrwxrwx gets
- * to 777
- *
- * @param permissions
- * @return
- */
- private String convertPermissions(String permissions) {
- int owner = getGroupPermission(permissions.substring(1, 4));
- int group = getGroupPermission(permissions.substring(4, 7));
- int world = getGroupPermission(permissions.substring(7, 10));
-
- return "" + owner + group + world;
- }
-
- /**
- * Calculates permission for one group
- *
- * @param permission
- * @return value of permission string
- */
- private int getGroupPermission(String permission) {
- int value = 0;
-
- if (permission.charAt(0) == 'r') {
- value += 4;
- }
- if (permission.charAt(1) == 'w') {
- value += 2;
- }
- if (permission.charAt(2) == 'x') {
- value += 1;
- }
-
- return value;
- }
-
- @Override
- public void output(int id, String line) {
- // general check if line contains file
- if (line.contains(fileName)) {
-
- // try to match line exactly
- try {
- Matcher permissionMatcher = permissionPattern.matcher(line);
- if (permissionMatcher.find()) {
- permissions = convertPermissions(permissionMatcher.group(1));
-
- Log.d(RootCommands.TAG, "Found permissions: " + permissions);
- } else {
- Log.d(RootCommands.TAG, "Permissions were not found in ls command!");
- }
-
- // try to parse for symlink
- Matcher symlinkMatcher = symlinkPattern.matcher(line);
- if (symlinkMatcher.find()) {
- /*
- * TODO: If symlink points to a file in the same directory the path is not
- * absolute!!!
- */
- symlink = symlinkMatcher.group(1);
- Log.d(RootCommands.TAG, "Symlink found: " + symlink);
- } else {
- Log.d(RootCommands.TAG, "No symlink found!");
- }
- } catch (Exception e) {
- Log.e(RootCommands.TAG, "Error with regex!", e);
- }
- }
- }
-
- @Override
- public void afterExecution(int id, int exitCode) {
- }
-
- }
-
- /**
- * @param file
- * String that represent the file, including the full path to the file and its name.
- * @param followSymlinks
- * @return File permissions as String, for example: 777, returns null on error
- * @throws java.io.IOException
- * @throws java.util.concurrent.TimeoutException
- * @throws BrokenBusyboxException
- *
- */
- public String getFilePermissions(String file) throws BrokenBusyboxException, TimeoutException,
- IOException {
- Log.d(RootCommands.TAG, "Checking permissions for " + file);
-
- String permissions = null;
-
- if (fileExists(file)) {
- Log.d(RootCommands.TAG, file + " was found.");
-
- LsCommand lsCommand = new LsCommand(file);
- shell.add(lsCommand).waitForFinish();
-
- permissions = lsCommand.getPermissions();
- }
-
- return permissions;
- }
-
- /**
- * Sets permission of file
- *
- * @param file
- * absolute path to file
- * @param permissions
- * String like 777
- * @return true if command worked
- * @throws BrokenBusyboxException
- * @throws java.util.concurrent.TimeoutException
- * @throws java.io.IOException
- */
- public boolean setFilePermissions(String file, String permissions)
- throws BrokenBusyboxException, TimeoutException, IOException {
- Log.d(RootCommands.TAG, "Set permissions of " + file + " to " + permissions);
-
- SimpleCommand chmodCommand = new SimpleCommand("chmod " + permissions + " " + file);
- shell.add(chmodCommand).waitForFinish();
-
- if (chmodCommand.getExitCode() == 0) {
- return true;
- } else {
- return false;
- }
- }
-
- /**
- * This will return a String that represent the symlink for a specified file.
- *
- * @param file
- * The path to the file to get the Symlink for. (must have absolute path)
- *
- * @return A String that represent the symlink for a specified file or null if no symlink
- * exists.
- * @throws java.io.IOException
- * @throws java.util.concurrent.TimeoutException
- * @throws BrokenBusyboxException
- */
- public String getSymlink(String file) throws BrokenBusyboxException, TimeoutException,
- IOException {
- Log.d(RootCommands.TAG, "Find symlink for " + file);
-
- String symlink = null;
-
- LsCommand lsCommand = new LsCommand(file);
- shell.add(lsCommand).waitForFinish();
-
- symlink = lsCommand.getSymlink();
-
- return symlink;
- }
-
- /**
- * Copys a file to a destination. Because cp is not available on all android devices, we use dd
- * or cat.
- *
- * @param source
- * example: /data/data/org.adaway/files/hosts
- * @param destination
- * example: /system/etc/hosts
- * @param remountAsRw
- * remounts the destination as read/write before writing to it
- * @param preserveFileAttributes
- * tries to copy file attributes from source to destination, if only cat is available
- * only permissions are preserved
- * @return true if it was successfully copied
- * @throws BrokenBusyboxException
- * @throws java.io.IOException
- * @throws java.util.concurrent.TimeoutException
- */
- public boolean copyFile(String source, String destination, boolean remountAsRw,
- boolean preservePermissions) throws BrokenBusyboxException, IOException,
- TimeoutException {
-
- /*
- * dd can only copy files, but we can not check if the source is a file without invoking
- * shell commands, because from Java we probably have no read access, thus we only check if
- * they are ending with trailing slashes
- */
- if (source.endsWith("/") || destination.endsWith("/")) {
- throw new FileNotFoundException("dd can only copy files!");
- }
-
- // remount destination as read/write before copying to it
- if (remountAsRw) {
- if (!remount(destination, "RW")) {
- Log.d(RootCommands.TAG,
- "Remounting failed! There is probably no need to remount this partition!");
- }
- }
-
- // get permissions of source before overwriting
- String permissions = null;
- if (preservePermissions) {
- permissions = getFilePermissions(source);
- }
-
- boolean commandSuccess = false;
-
- SimpleCommand ddCommand = new SimpleCommand("dd if=" + source + " of="
- + destination);
- shell.add(ddCommand).waitForFinish();
-
- if (ddCommand.getExitCode() == 0) {
- commandSuccess = true;
- } else {
- // try cat if dd fails
- SimpleCommand catCommand = new SimpleCommand("cat " + source + " > "
- + destination);
- shell.add(catCommand).waitForFinish();
-
- if (catCommand.getExitCode() == 0) {
- commandSuccess = true;
- }
- }
-
- // set back permissions from source to destination
- if (preservePermissions) {
- setFilePermissions(destination, permissions);
- }
-
- // remount destination back to read only
- if (remountAsRw) {
- if (!remount(destination, "RO")) {
- Log.d(RootCommands.TAG,
- "Remounting failed! There is probably no need to remount this partition!");
- }
- }
-
- return commandSuccess;
- }
-
- public static final int REBOOT_HOTREBOOT = 1;
- public static final int REBOOT_REBOOT = 2;
- public static final int REBOOT_SHUTDOWN = 3;
- public static final int REBOOT_RECOVERY = 4;
-
- /**
- * Shutdown or reboot device. Possible actions are REBOOT_HOTREBOOT, REBOOT_REBOOT,
- * REBOOT_SHUTDOWN, REBOOT_RECOVERY
- *
- * @param action
- * @throws java.io.IOException
- * @throws java.util.concurrent.TimeoutException
- * @throws BrokenBusyboxException
- */
- public void reboot(int action) throws BrokenBusyboxException, TimeoutException, IOException {
- if (action == REBOOT_HOTREBOOT) {
- killAll("system_server");
- // or: killAll("zygote");
- } else {
- String command;
- switch (action) {
- case REBOOT_REBOOT:
- command = "reboot";
- break;
- case REBOOT_SHUTDOWN:
- command = "reboot -p";
- break;
- case REBOOT_RECOVERY:
- command = "reboot recovery";
- break;
- default:
- command = "reboot";
- break;
- }
-
- SimpleCommand rebootCommand = new SimpleCommand(command);
- shell.add(rebootCommand).waitForFinish();
-
- if (rebootCommand.getExitCode() == -1) {
- Log.e(RootCommands.TAG, "Reboot failed!");
- }
- }
- }
-
- /**
- * This command checks if a file exists
- */
- private class FileExistsCommand extends Command {
- private String file;
- private boolean fileExists = false;
-
- public FileExistsCommand(String file) {
- super("ls " + file);
- this.file = file;
- }
-
- public boolean isFileExists() {
- return fileExists;
- }
-
- @Override
- public void output(int id, String line) {
- if (line.trim().equals(file)) {
- fileExists = true;
- }
- }
-
- @Override
- public void afterExecution(int id, int exitCode) {
- }
-
- }
-
- /**
- * Use this to check whether or not a file exists on the filesystem.
- *
- * @param file
- * String that represent the file, including the full path to the file and its name.
- *
- * @return a boolean that will indicate whether or not the file exists.
- * @throws java.io.IOException
- * @throws java.util.concurrent.TimeoutException
- * @throws BrokenBusyboxException
- *
- */
- public boolean fileExists(String file) throws BrokenBusyboxException, TimeoutException,
- IOException {
- FileExistsCommand fileExistsCommand = new FileExistsCommand(file);
- shell.add(fileExistsCommand).waitForFinish();
-
- if (fileExistsCommand.isFileExists()) {
- return true;
- } else {
- return false;
- }
- }
-
- public abstract class WithPermissions {
- abstract void whileHavingPermissions();
- }
-
- /**
- * Execute user defined Java code while having temporary permissions on a file
- *
- * @param file
- * @param withPermissions
- * @throws BrokenBusyboxException
- * @throws java.util.concurrent.TimeoutException
- * @throws java.io.IOException
- */
- public void withPermission(String file, String permission, WithPermissions withPermissions)
- throws BrokenBusyboxException, TimeoutException, IOException {
- String oldPermissions = getFilePermissions(file);
-
- // set permissions (If set to 666, then Dalvik VM can also write to that file!)
- setFilePermissions(file, permission);
-
- // execute user defined code
- withPermissions.whileHavingPermissions();
-
- // set back to old permissions
- setFilePermissions(file, oldPermissions);
- }
-
- /**
- * Execute user defined Java code while having temporary write permissions on a file using chmod
- * 666
- *
- * @param file
- * @param withWritePermissions
- * @throws BrokenBusyboxException
- * @throws java.util.concurrent.TimeoutException
- * @throws java.io.IOException
- */
- public void withWritePermissions(String file, WithPermissions withWritePermissions)
- throws BrokenBusyboxException, TimeoutException, IOException {
- withPermission(file, "666", withWritePermissions);
- }
-
- /**
- * Sets system clock using /dev/alarm
- *
- * @param millis
- * @throws BrokenBusyboxException
- * @throws java.util.concurrent.TimeoutException
- * @throws java.io.IOException
- */
- public void setSystemClock(final long millis) throws BrokenBusyboxException, TimeoutException,
- IOException {
- withWritePermissions("/dev/alarm", new WithPermissions() {
-
- @Override
- void whileHavingPermissions() {
- SystemClock.setCurrentTimeMillis(millis);
- }
- });
- }
-
- /**
- * Adjust system clock by offset using /dev/alarm
- *
- * @param offset
- * @throws BrokenBusyboxException
- * @throws java.util.concurrent.TimeoutException
- * @throws java.io.IOException
- */
- public void adjustSystemClock(final long offset) throws BrokenBusyboxException,
- TimeoutException, IOException {
- withWritePermissions("/dev/alarm", new WithPermissions() {
-
- @Override
- void whileHavingPermissions() {
- SystemClock.setCurrentTimeMillis(System.currentTimeMillis() + offset);
- }
- });
- }
-
- /**
- * This will take a path, which can contain the file name as well, and attempt to remount the
- * underlying partition.
- *
- * For example, passing in the following string:
- * "/system/bin/some/directory/that/really/would/never/exist" will result in /system ultimately
- * being remounted. However, keep in mind that the longer the path you supply, the more work
- * this has to do, and the slower it will run.
- *
- * @param file
- * file path
- * @param mountType
- * mount type: pass in RO (Read only) or RW (Read Write)
- * @return a boolean
which indicates whether or not the partition has been
- * remounted as specified.
- */
- public boolean remount(String file, String mountType) {
- // Recieved a request, get an instance of Remounter
- Remounter remounter = new Remounter(shell);
- // send the request
- return (remounter.remount(file, mountType));
- }
-
- /**
- * This will tell you how the specified mount is mounted. rw, ro, etc...
- *
- * @param The
- * mount you want to check
- *
- * @return String
What the mount is mounted as.
- * @throws Exception
- * if we cannot determine how the mount is mounted.
- */
- public String getMountedAs(String path) throws Exception {
- ArrayList mounts = Remounter.getMounts();
- if (mounts != null) {
- for (Mount mount : mounts) {
- if (path.contains(mount.getMountPoint().getAbsolutePath())) {
- Log.d(RootCommands.TAG, (String) mount.getFlags().toArray()[0]);
- return (String) mount.getFlags().toArray()[0];
- }
- }
-
- throw new Exception();
- } else {
- throw new Exception();
- }
- }
-
- /**
- * Check if there is enough space on partition where target is located
- *
- * @param size
- * size of file to put on partition
- * @param target
- * path where to put the file
- *
- * @return true if it will fit on partition of target, false if it will not fit.
- */
- public boolean hasEnoughSpaceOnPartition(String target, long size) {
- try {
- // new File(target).getFreeSpace() (API 9) is not working on data partition
-
- // get directory without file
- String directory = new File(target).getParent().toString();
-
- StatFs stat = new StatFs(directory);
- long blockSize = stat.getBlockSize();
- long availableBlocks = stat.getAvailableBlocks();
- long availableSpace = availableBlocks * blockSize;
-
- Log.i(RootCommands.TAG, "Checking for enough space: Target: " + target
- + ", directory: " + directory + " size: " + size + ", availableSpace: "
- + availableSpace);
-
- if (size < availableSpace) {
- return true;
- } else {
- Log.e(RootCommands.TAG, "Not enough space on partition!");
- return false;
- }
- } catch (Exception e) {
- // if new StatFs(directory) fails catch IllegalArgumentException and just return true as
- // workaround
- Log.e(RootCommands.TAG, "Problem while getting available space on partition!", e);
- return true;
- }
- }
-
- /**
- * TODO: Not tested!
- *
- * @param toggle
- * @throws java.io.IOException
- * @throws java.util.concurrent.TimeoutException
- * @throws BrokenBusyboxException
- */
- public void toggleAdbDaemon(boolean toggle) throws BrokenBusyboxException, TimeoutException,
- IOException {
- SimpleCommand disableAdb = new SimpleCommand("setprop persist.service.adb.enable 0",
- "stop adbd");
- SimpleCommand enableAdb = new SimpleCommand("setprop persist.service.adb.enable 1",
- "stop adbd", "sleep 1", "start adbd");
-
- if (toggle) {
- shell.add(enableAdb).waitForFinish();
- } else {
- shell.add(disableAdb).waitForFinish();
- }
- }
-
-}
diff --git a/libraries/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/command/Command.java b/libraries/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/command/Command.java
deleted file mode 100644
index 488cbae..0000000
--- a/libraries/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/command/Command.java
+++ /dev/null
@@ -1,155 +0,0 @@
-
-
-package org.sufficientlysecure.rootcommands.command;
-
-import java.io.IOException;
-import java.io.OutputStream;
-import java.util.concurrent.TimeoutException;
-
-import org.sufficientlysecure.rootcommands.RootCommands;
-import org.sufficientlysecure.rootcommands.Shell;
-import org.sufficientlysecure.rootcommands.util.BrokenBusyboxException;
-import org.sufficientlysecure.rootcommands.util.Log;
-
-public abstract class Command {
- final String command[];
- boolean finished = false;
- boolean brokenBusyboxDetected = false;
- int exitCode;
- int id;
- int timeout = RootCommands.DEFAULT_TIMEOUT;
- Shell shell = null;
-
- public Command(String... command) {
- this.command = command;
- }
-
- public Command(int timeout, String... command) {
- this.command = command;
- this.timeout = timeout;
- }
-
- /**
- * This is called from Shell after adding it
- *
- * @param shell
- * @param id
- */
- public void addedToShell(Shell shell, int id) {
- this.shell = shell;
- this.id = id;
- }
-
- /**
- * Gets command string executed on the shell
- *
- * @return
- */
- public String getCommand() {
- StringBuilder sb = new StringBuilder();
- for (int i = 0; i < command.length; i++) {
- // redirect stderr to stdout
- sb.append(command[i] + " 2>&1");
- sb.append('\n');
- }
- Log.d(RootCommands.TAG, "Sending command(s): " + sb.toString());
- return sb.toString();
- }
-
- public void writeCommand(OutputStream out) throws IOException {
- out.write(getCommand().getBytes());
- }
-
- public void processOutput(String line) {
- Log.d(RootCommands.TAG, "ID: " + id + ", Output: " + line);
-
- /*
- * Try to detect broken toolbox/busybox binaries (see
- * https://code.google.com/p/busybox-android/issues/detail?id=1)
- *
- * It is giving "Value too large for defined data type" on certain file operations (e.g. ls
- * and chown) in certain directories (e.g. /data/data)
- */
- if (line.contains("Value too large for defined data type")) {
- Log.e(RootCommands.TAG, "Busybox is broken with high probability due to line: " + line);
- brokenBusyboxDetected = true;
- }
-
- // now execute specific output parsing
- output(id, line);
- }
-
- public abstract void output(int id, String line);
-
- public void processAfterExecution(int exitCode) {
- Log.d(RootCommands.TAG, "ID: " + id + ", ExitCode: " + exitCode);
-
- afterExecution(id, exitCode);
- }
-
- public abstract void afterExecution(int id, int exitCode);
-
- public void commandFinished(int id) {
- Log.d(RootCommands.TAG, "Command " + id + " finished.");
- }
-
- public void setExitCode(int code) {
- synchronized (this) {
- exitCode = code;
- finished = true;
- commandFinished(id);
- this.notifyAll();
- }
- }
-
- /**
- * Close the shell
- *
- * @param reason
- */
- public void terminate(String reason) {
- try {
- shell.close();
- Log.d(RootCommands.TAG, "Terminating the shell.");
- terminated(reason);
- } catch (IOException e) {
- }
- }
-
- public void terminated(String reason) {
- setExitCode(-1);
- Log.d(RootCommands.TAG, "Command " + id + " did not finish, because of " + reason);
- }
-
- /**
- * Waits for this command to finish and forwards exitCode into afterExecution method
- *
- * @param timeout
- * @throws java.util.concurrent.TimeoutException
- * @throws BrokenBusyboxException
- */
- public void waitForFinish() throws TimeoutException, BrokenBusyboxException {
- synchronized (this) {
- while (!finished) {
- try {
- this.wait(timeout);
- } catch (InterruptedException e) {
- Log.e(RootCommands.TAG, "InterruptedException in waitForFinish()", e);
- }
-
- if (!finished) {
- finished = true;
- terminate("Timeout");
- throw new TimeoutException("Timeout has occurred.");
- }
- }
-
- if (brokenBusyboxDetected) {
- throw new BrokenBusyboxException();
- }
-
- processAfterExecution(exitCode);
- }
- }
-
-}
\ No newline at end of file
diff --git a/libraries/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/command/ExecutableCommand.java b/libraries/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/command/ExecutableCommand.java
deleted file mode 100644
index 0bd92d7..0000000
--- a/libraries/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/command/ExecutableCommand.java
+++ /dev/null
@@ -1,51 +0,0 @@
-
-
-package org.sufficientlysecure.rootcommands.command;
-
-import java.io.File;
-
-import android.annotation.SuppressLint;
-import android.content.Context;
-import android.os.Build;
-
-public abstract class ExecutableCommand extends Command {
- public static final String EXECUTABLE_PREFIX = "lib";
- public static final String EXECUTABLE_SUFFIX = "_exec.so";
-
- /**
- * This class provides a way to use your own binaries!
- *
- * Include your own executables, renamed from * to lib*_exec.so, in your libs folder under the
- * architecture directories. Now they will be deployed by Android the same way libraries are
- * deployed!
- *
- * See README for more information how to use your own executables!
- *
- * @param context
- * @param executableName
- * @param parameters
- */
- public ExecutableCommand(Context context, String executableName, String parameters) {
- super(getLibDirectory(context) + File.separator + EXECUTABLE_PREFIX + executableName
- + EXECUTABLE_SUFFIX + " " + parameters);
- }
-
- /**
- * Get full path to lib directory of app
- *
- * @return dir as String
- */
- @SuppressLint("NewApi")
- private static String getLibDirectory(Context context) {
- if (Build.VERSION.SDK_INT >= 9) {
- return context.getApplicationInfo().nativeLibraryDir;
- } else {
- return context.getApplicationInfo().dataDir + File.separator + "lib";
- }
- }
-
- public abstract void output(int id, String line);
-
- public abstract void afterExecution(int id, int exitCode);
-
-}
\ No newline at end of file
diff --git a/libraries/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/command/SimpleCommand.java b/libraries/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/command/SimpleCommand.java
deleted file mode 100644
index b2091c8..0000000
--- a/libraries/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/command/SimpleCommand.java
+++ /dev/null
@@ -1,29 +0,0 @@
-
-
-package org.sufficientlysecure.rootcommands.command;
-
-public class SimpleCommand extends Command {
- private StringBuilder sb = new StringBuilder();
-
- public SimpleCommand(String... command) {
- super(command);
- }
-
- @Override
- public void output(int id, String line) {
- sb.append(line).append('\n');
- }
-
- @Override
- public void afterExecution(int id, int exitCode) {
- }
-
- public String getOutput() {
- return sb.toString();
- }
-
- public int getExitCode() {
- return exitCode;
- }
-
-}
\ No newline at end of file
diff --git a/libraries/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/command/SimpleExecutableCommand.java b/libraries/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/command/SimpleExecutableCommand.java
deleted file mode 100644
index 78fd2be..0000000
--- a/libraries/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/command/SimpleExecutableCommand.java
+++ /dev/null
@@ -1,31 +0,0 @@
-
-
-package org.sufficientlysecure.rootcommands.command;
-
-import android.content.Context;
-
-public class SimpleExecutableCommand extends ExecutableCommand {
- private StringBuilder sb = new StringBuilder();
-
- public SimpleExecutableCommand(Context context, String executableName, String parameters) {
- super(context, executableName, parameters);
- }
-
- @Override
- public void output(int id, String line) {
- sb.append(line).append('\n');
- }
-
- @Override
- public void afterExecution(int id, int exitCode) {
- }
-
- public String getOutput() {
- return sb.toString();
- }
-
- public int getExitCode() {
- return exitCode;
- }
-
-}
\ No newline at end of file
diff --git a/libraries/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/util/BrokenBusyboxException.java b/libraries/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/util/BrokenBusyboxException.java
deleted file mode 100644
index bf40bf4..0000000
--- a/libraries/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/util/BrokenBusyboxException.java
+++ /dev/null
@@ -1,18 +0,0 @@
-
-
-package org.sufficientlysecure.rootcommands.util;
-
-import java.io.IOException;
-
-public class BrokenBusyboxException extends IOException {
- private static final long serialVersionUID = 8337358201589488409L;
-
- public BrokenBusyboxException() {
- super();
- }
-
- public BrokenBusyboxException(String detailMessage) {
- super(detailMessage);
- }
-
-}
diff --git a/libraries/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/util/Log.java b/libraries/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/util/Log.java
deleted file mode 100644
index e55da98..0000000
--- a/libraries/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/util/Log.java
+++ /dev/null
@@ -1,69 +0,0 @@
-
-
-package org.sufficientlysecure.rootcommands.util;
-
-import org.sufficientlysecure.rootcommands.RootCommands;
-
-/**
- * Wraps Android Logging to enable or disable debug output using Constants
- *
- */
-public final class Log {
-
- public static void v(String tag, String msg) {
- if (RootCommands.DEBUG) {
- android.util.Log.v(tag, msg);
- }
- }
-
- public static void v(String tag, String msg, Throwable tr) {
- if (RootCommands.DEBUG) {
- android.util.Log.v(tag, msg, tr);
- }
- }
-
- public static void d(String tag, String msg) {
- if (RootCommands.DEBUG) {
- android.util.Log.d(tag, msg);
- }
- }
-
- public static void d(String tag, String msg, Throwable tr) {
- if (RootCommands.DEBUG) {
- android.util.Log.d(tag, msg, tr);
- }
- }
-
- public static void i(String tag, String msg) {
- if (RootCommands.DEBUG) {
- android.util.Log.i(tag, msg);
- }
- }
-
- public static void i(String tag, String msg, Throwable tr) {
- if (RootCommands.DEBUG) {
- android.util.Log.i(tag, msg, tr);
- }
- }
-
- public static void w(String tag, String msg) {
- android.util.Log.w(tag, msg);
- }
-
- public static void w(String tag, String msg, Throwable tr) {
- android.util.Log.w(tag, msg, tr);
- }
-
- public static void w(String tag, Throwable tr) {
- android.util.Log.w(tag, tr);
- }
-
- public static void e(String tag, String msg) {
- android.util.Log.e(tag, msg);
- }
-
- public static void e(String tag, String msg, Throwable tr) {
- android.util.Log.e(tag, msg, tr);
- }
-
-}
diff --git a/libraries/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/util/RootAccessDeniedException.java b/libraries/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/util/RootAccessDeniedException.java
deleted file mode 100644
index 650f1ed..0000000
--- a/libraries/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/util/RootAccessDeniedException.java
+++ /dev/null
@@ -1,18 +0,0 @@
-
-
-package org.sufficientlysecure.rootcommands.util;
-
-import java.io.IOException;
-
-public class RootAccessDeniedException extends IOException {
- private static final long serialVersionUID = 9088998884166225540L;
-
- public RootAccessDeniedException() {
- super();
- }
-
- public RootAccessDeniedException(String detailMessage) {
- super(detailMessage);
- }
-
-}
diff --git a/libraries/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/util/UnsupportedArchitectureException.java b/libraries/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/util/UnsupportedArchitectureException.java
deleted file mode 100644
index e8535d2..0000000
--- a/libraries/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/util/UnsupportedArchitectureException.java
+++ /dev/null
@@ -1,16 +0,0 @@
-
-
-package org.sufficientlysecure.rootcommands.util;
-
-public class UnsupportedArchitectureException extends Exception {
- private static final long serialVersionUID = 7826528799780001655L;
-
- public UnsupportedArchitectureException() {
- super();
- }
-
- public UnsupportedArchitectureException(String detailMessage) {
- super(detailMessage);
- }
-
-}
diff --git a/libraries/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/util/Utils.java b/libraries/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/util/Utils.java
deleted file mode 100644
index e9a382f..0000000
--- a/libraries/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/util/Utils.java
+++ /dev/null
@@ -1,89 +0,0 @@
-
-
-package org.sufficientlysecure.rootcommands.util;
-
-import java.io.File;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Map;
-
-import org.sufficientlysecure.rootcommands.RootCommands;
-
-public class Utils {
- /*
- * The emulator and ADP1 device both have a su binary in /system/xbin/su, but it doesn't allow
- * apps to use it (su app_29 $ su su: uid 10029 not allowed to su).
- *
- * Cyanogen used to have su in /system/bin/su, in newer versions it's a symlink to
- * /system/xbin/su.
- *
- * The Archos tablet has it in /data/bin/su, since they don't have write access to /system yet.
- */
- static final String[] BinaryPlaces = { "/data/bin/", "/system/bin/", "/system/xbin/", "/sbin/",
- "/data/local/xbin/", "/data/local/bin/", "/system/sd/xbin/", "/system/bin/failsafe/",
- "/data/local/" };
-
- /**
- * Determine the path of the su executable.
- *
- * Code from https://github.com/miracle2k/android-autostarts, use under Apache License was
- * agreed by Michael Elsdörfer
- */
- public static String getSuPath() {
- for (String p : BinaryPlaces) {
- File su = new File(p + "su");
- if (su.exists()) {
- Log.d(RootCommands.TAG, "su found at: " + p);
- return su.getAbsolutePath();
- } else {
- Log.v(RootCommands.TAG, "No su in: " + p);
- }
- }
- Log.d(RootCommands.TAG, "No su found in a well-known location, " + "will just use \"su\".");
- return "su";
- }
-
- /**
- * This code is adapted from java.lang.ProcessBuilder.start().
- *
- * The problem is that Android doesn't allow us to modify the map returned by
- * ProcessBuilder.environment(), even though the docstring indicates that it should. This is
- * because it simply returns the SystemEnvironment object that System.getenv() gives us. The
- * relevant portion in the source code is marked as "// android changed", so presumably it's not
- * the case in the original version of the Apache Harmony project.
- *
- * Note that simply passing the environment variables we want to Process.exec won't be good
- * enough, since that would override the environment we inherited completely.
- *
- * We needed to be able to set a CLASSPATH environment variable for our new process in order to
- * use the "app_process" command directly. Note: "app_process" takes arguments passed on to the
- * Dalvik VM as well; this might be an alternative way to set the class path.
- *
- * Code from https://github.com/miracle2k/android-autostarts, use under Apache License was
- * agreed by Michael Elsdörfer
- */
- public static Process runWithEnv(String command, ArrayList customAddedEnv,
- String baseDirectory) throws IOException {
-
- Map environment = System.getenv();
- String[] envArray = new String[environment.size()
- + (customAddedEnv != null ? customAddedEnv.size() : 0)];
- int i = 0;
- for (Map.Entry entry : environment.entrySet()) {
- envArray[i++] = entry.getKey() + "=" + entry.getValue();
- }
- if (customAddedEnv != null) {
- for (String entry : customAddedEnv) {
- envArray[i++] = entry;
- }
- }
-
- Process process;
- if (baseDirectory == null) {
- process = Runtime.getRuntime().exec(command, envArray, null);
- } else {
- process = Runtime.getRuntime().exec(command, envArray, new File(baseDirectory));
- }
- return process;
- }
-}
diff --git a/libraries/sharedCode/build.gradle b/libraries/sharedCode/build.gradle
index 85baf7b..b2765d7 100644
--- a/libraries/sharedCode/build.gradle
+++ b/libraries/sharedCode/build.gradle
@@ -59,7 +59,6 @@ android {
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
- compile project(':libraries:RootCommands')
compile project(':libraries:FloatingActionButton')
// compile 'com.googlecode.juniversalchardet:juniversalchardet:1.0.3'
compile 'org.apache.commons:commons-lang3:3.1'
diff --git a/libraries/sharedCode/src/main/java/com/spazedog/lib/rootfw4/Common.java b/libraries/sharedCode/src/main/java/com/spazedog/lib/rootfw4/Common.java
new file mode 100644
index 0000000..a791f07
--- /dev/null
+++ b/libraries/sharedCode/src/main/java/com/spazedog/lib/rootfw4/Common.java
@@ -0,0 +1,119 @@
+/*
+ * This file is part of the RootFW Project: https://github.com/spazedog/rootfw
+ *
+ * Copyright (c) 2015 Daniel Bergløv
+ *
+ * RootFW is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+
+ * RootFW is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with RootFW. If not, see
+ */
+
+package com.spazedog.lib.rootfw4;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import android.os.Build;
+import android.os.Process;
+
+public class Common {
+ private static Boolean oEmulator = false;
+
+ public static final String TAG = Common.class.getPackage().getName();
+ public static Boolean DEBUG = true;
+ public static String[] BINARIES = new String[]{null, "busybox", "toolbox"};
+ public static Map UIDS = new HashMap();
+ public static Map UNAMES = new HashMap();
+
+ static {
+ UIDS.put("root", 0);
+ UIDS.put("system", 1000);
+ UIDS.put("radio", 1001);
+ UIDS.put("bluetooth", 1002);
+ UIDS.put("graphics", 1003);
+ UIDS.put("input", 1004);
+ UIDS.put("audio", 1005);
+ UIDS.put("camera", 1006);
+ UIDS.put("log", 1007);
+ UIDS.put("compass", 1008);
+ UIDS.put("mount", 1009);
+ UIDS.put("wifi", 1010);
+ UIDS.put("adb", 1011);
+ UIDS.put("install", 1012);
+ UIDS.put("media", 1013);
+ UIDS.put("dhcp", 1014);
+ UIDS.put("shell", 2000);
+ UIDS.put("cache", 2001);
+ UIDS.put("diag", 2002);
+ UIDS.put("net_bt_admin", 3001);
+ UIDS.put("net_bt", 3002);
+ UIDS.put("inet", 3003);
+ UIDS.put("net_raw", 3004);
+ UIDS.put("misc", 9998);
+ UIDS.put("nobody", 9999);
+
+ for (Entry entry : UIDS.entrySet()) {
+ UNAMES.put(entry.getValue(), entry.getKey());
+ }
+
+ oEmulator = Build.BRAND.equalsIgnoreCase("generic") ||
+ Build.MODEL.contains("google_sdk") ||
+ Build.MODEL.contains("Emulator") ||
+ Build.MODEL.contains("Android SDK");
+ }
+
+ /**
+ * Check if the current device is an emulator
+ */
+ public static Boolean isEmulator() {
+ return oEmulator;
+ }
+
+ public static Integer getUID(String name) {
+ if (name != null) {
+ if (name.matches("^[0-9]+$")) {
+ return Integer.parseInt(name);
+ }
+
+ if (UIDS.containsKey(name)) {
+ return UIDS.get(name);
+
+ } else if (name.startsWith("u")) {
+ Integer sep = name.indexOf("_");
+
+ if (sep > 0) {
+ Integer uid = Integer.parseInt( name.substring(1, sep) );
+ Integer gid = Integer.parseInt( name.substring(sep+2) );
+
+ return uid * 100000 + ((Process.FIRST_APPLICATION_UID + gid) % 100000);
+ }
+ }
+ }
+
+ return -10000;
+ }
+
+ public static String getUIDName(Integer id) {
+ if (UNAMES.containsKey(id)) {
+ return UNAMES.get(id);
+
+ } else if (id >= 10000) {
+ Integer uid = id / 100000;
+ Integer gid = id % Process.FIRST_APPLICATION_UID;
+
+ return "u" + uid + "_a" + gid + "";
+ }
+
+ return null;
+ }
+}
diff --git a/libraries/sharedCode/src/main/java/com/spazedog/lib/rootfw4/RootFW.java b/libraries/sharedCode/src/main/java/com/spazedog/lib/rootfw4/RootFW.java
new file mode 100644
index 0000000..6bd24b9
--- /dev/null
+++ b/libraries/sharedCode/src/main/java/com/spazedog/lib/rootfw4/RootFW.java
@@ -0,0 +1,348 @@
+/*
+ * Copyright (C) 2014 Vlad Mihalachi
+ *
+ * This file is part of Turbo Editor.
+ *
+ * Turbo Editor is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Turbo Editor is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.spazedog.lib.rootfw4;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import com.spazedog.lib.rootfw4.Shell.Attempts;
+import com.spazedog.lib.rootfw4.Shell.OnShellConnectionListener;
+import com.spazedog.lib.rootfw4.Shell.OnShellResultListener;
+import com.spazedog.lib.rootfw4.Shell.OnShellValidateListener;
+import com.spazedog.lib.rootfw4.Shell.Result;
+import com.spazedog.lib.rootfw4.utils.Device;
+import com.spazedog.lib.rootfw4.utils.Device.Process;
+import com.spazedog.lib.rootfw4.utils.File;
+import com.spazedog.lib.rootfw4.utils.Filesystem;
+import com.spazedog.lib.rootfw4.utils.Memory;
+import com.spazedog.lib.rootfw4.utils.Filesystem.Disk;
+import com.spazedog.lib.rootfw4.utils.Memory.CompCache;
+import com.spazedog.lib.rootfw4.utils.Memory.Swap;
+import com.spazedog.lib.rootfw4.utils.io.FileReader;
+import com.spazedog.lib.rootfw4.utils.io.FileWriter;
+
+/**
+ * This is a global static front-end to {@link com.spazedog.lib.rootfw4.Shell}. It allows one global shell connection to be
+ * easily shared across classes and threads without having to create multiple connections.
+ */
+public class RootFW {
+
+ protected static volatile Shell mShell;
+ protected static volatile Integer mLockCount = 0;
+ protected static final Object mLock = new Object();
+
+ protected static Set mListeners = new HashSet();
+
+ /**
+ * An interface that can be used to monitor the current state of the global connection.
+ *
+ * @see #addConnectionListener(com.spazedog.lib.rootfw4.RootFW.OnConnectionListener)
+ */
+ public static interface OnConnectionListener extends OnShellConnectionListener {
+ /**
+ * Invoked when the shell has been connected
+ */
+ public void onShellConnect();
+ }
+
+ /**
+ * Create a new connection to the global shell.
+ *
+ * Note that this method will fallback on a normal user shell if root could not be obtained.
+ * Therefore it is always a good idea to check the state of root using {@link #isRoot()}
+ *
+ * @return
+ * True if the shell was connected successfully
+ */
+ public static Boolean connect() {
+ synchronized(mLock) {
+ if (mShell == null || !mShell.isConnected()) {
+ mLockCount = 0;
+ mShell = new Shell(true);
+
+ /*
+ * Fallback to a regular user shell
+ */
+ if (!mShell.isConnected()) {
+ mShell = new Shell(false);
+ }
+
+ mShell.addShellConnectionListener(new OnShellConnectionListener(){
+ @Override
+ public void onShellDisconnect() {
+ for (OnConnectionListener listener : mListeners) {
+ listener.onShellDisconnect();
+ }
+ }
+ });
+
+ for (OnConnectionListener listener : mListeners) {
+ listener.onShellConnect();
+ }
+ }
+
+ return mShell.isConnected();
+ }
+ }
+
+ /**
+ * @see #disconnect(Boolean)
+ */
+ public static void disconnect() {
+ disconnect(false);
+ }
+
+ /**
+ * Destroy the connection to the global shell.
+ *
+ * The connection will only be destroyed if there is no current locks on this connecton.
+ *
+ * @see #lock()
+ */
+ public static void disconnect(Boolean force) {
+ synchronized(mLock) {
+ if (mLockCount == 0 || force) {
+ mLockCount = 0;
+ mShell.destroy();
+ mShell = null;
+ }
+ }
+ }
+
+ /**
+ * Add a new lock on this connection. Each call to this method will add an additional lock.
+ * As long as there are 1 or more locks on this connection, it cannot be destroyed using {@link #disconnect()}
+ *
+ * @see #unlock()
+ */
+ public static void lock() {
+ synchronized(mLock) {
+ mLockCount += 1;
+ }
+ }
+
+ /**
+ * Removes one lock from this connection. Each call will remove 1 lock as long as there are 1 or more locks attached.
+ *
+ * @see #lock()
+ */
+ public static void unlock() {
+ synchronized(mLock) {
+ if (mLockCount > 0) {
+ mLockCount -= 1;
+
+ } else {
+ mLockCount = 0;
+ }
+ }
+ }
+
+ /**
+ * Checks if there are any active locks on the connection.
+ */
+ public static Boolean isLocked() {
+ synchronized(mLock) {
+ return mLockCount == 0;
+ }
+ }
+
+ /**
+ * Add a new {@link com.spazedog.lib.rootfw4.RootFW.OnConnectionListener} to the global shell
+ *
+ * @see #removeConnectionListener(com.spazedog.lib.rootfw4.RootFW.OnConnectionListener)
+ */
+ public static void addConnectionListener(OnConnectionListener listener) {
+ synchronized(mLock) {
+ mListeners.add(listener);
+ }
+ }
+
+ /**
+ * Remove a {@link com.spazedog.lib.rootfw4.RootFW.OnConnectionListener} from the global shell
+ *
+ * @see #addConnectionListener(com.spazedog.lib.rootfw4.RootFW.OnConnectionListener)
+ */
+ public static void removeConnectionListener(OnConnectionListener listener) {
+ synchronized(mLock) {
+ mListeners.remove(listener);
+ }
+ }
+
+ /**
+ * @see com.spazedog.lib.rootfw4.Shell#execute(String)
+ */
+ public static Result execute(String command) {
+ return mShell.execute(command);
+ }
+
+ /**
+ * @see com.spazedog.lib.rootfw4.Shell#execute(String[])
+ */
+ public static Result execute(String[] commands) {
+ return mShell.execute(commands);
+ }
+
+ /**
+ * @see com.spazedog.lib.rootfw4.Shell#execute(String[], Integer[], com.spazedog.lib.rootfw4.Shell.OnShellValidateListener)
+ */
+ public static Result execute(String[] commands, Integer[] resultCodes, OnShellValidateListener validater) {
+ return mShell.execute(commands, resultCodes, validater);
+ }
+
+ /**
+ * @see com.spazedog.lib.rootfw4.Shell#executeAsync(String, com.spazedog.lib.rootfw4.Shell.OnShellResultListener)
+ */
+ public static void executeAsync(String command, OnShellResultListener listener) {
+ mShell.executeAsync(command, listener);
+ }
+
+ /**
+ * @see com.spazedog.lib.rootfw4.Shell#executeAsync(String[], com.spazedog.lib.rootfw4.Shell.OnShellResultListener)
+ */
+ public static void executeAsync(String[] commands, OnShellResultListener listener) {
+ mShell.executeAsync(commands, listener);
+ }
+
+ /**
+ * @see com.spazedog.lib.rootfw4.Shell#executeAsync(String[], Integer[], com.spazedog.lib.rootfw4.Shell.OnShellValidateListener, com.spazedog.lib.rootfw4.Shell.OnShellResultListener)
+ */
+ public static void executeAsync(String[] commands, Integer[] resultCodes, OnShellValidateListener validater, OnShellResultListener listener) {
+ mShell.executeAsync(commands, resultCodes, validater, listener);
+ }
+
+ /**
+ * @see com.spazedog.lib.rootfw4.Shell#isRoot()
+ */
+ public static Boolean isRoot() {
+ return mShell.isRoot();
+ }
+
+ /**
+ * @see com.spazedog.lib.rootfw4.Shell#isConnected()
+ */
+ public static Boolean isConnected() {
+ return mShell != null && mShell.isConnected();
+ }
+
+ /**
+ * @see com.spazedog.lib.rootfw4.Shell#getTimeout()
+ */
+ public static Integer getTimeout() {
+ return mShell.getTimeout();
+ }
+
+ /**
+ * @see com.spazedog.lib.rootfw4.Shell#setTimeout(Integer)
+ */
+ public static void setTimeout(Integer timeout) {
+ mShell.setTimeout(timeout);
+ }
+
+ /**
+ * @see com.spazedog.lib.rootfw4.Shell#getBinary(String)
+ */
+ public static String findCommand(String bin) {
+ return mShell.findCommand(bin);
+ }
+
+ /**
+ * @see com.spazedog.lib.rootfw4.Shell#createAttempts(String)
+ */
+ public static Attempts createAttempts(String command) {
+ return mShell.createAttempts(command);
+ }
+
+ /**
+ * @see com.spazedog.lib.rootfw4.Shell#getFileReader(String)
+ */
+ public static FileReader getFileReader(String file) {
+ return mShell.getFileReader(file);
+ }
+
+ /**
+ * @see com.spazedog.lib.rootfw4.Shell#getFileWriter(String, Boolean)
+ */
+ public static FileWriter getFileWriter(String file, Boolean append) {
+ return mShell.getFileWriter(file, append);
+ }
+
+ /**
+ * @see com.spazedog.lib.rootfw4.Shell#getFile(String)
+ */
+ public static File getFile(String file) {
+ return mShell.getFile(file);
+ }
+
+ /**
+ * @see com.spazedog.lib.rootfw4.Shell#getFilesystem()
+ */
+ public static Filesystem getFilesystem() {
+ return mShell.getFilesystem();
+ }
+
+ /**
+ * @see com.spazedog.lib.rootfw4.Shell#getDisk(String)
+ */
+ public static Disk getDisk(String disk) {
+ return mShell.getDisk(disk);
+ }
+
+ /**
+ * @see com.spazedog.lib.rootfw4.Shell#getDevice()
+ */
+ public static Device getDevice() {
+ return mShell.getDevice();
+ }
+
+ /**
+ * @see com.spazedog.lib.rootfw4.Shell#getProcess(String)
+ */
+ public static Process getProcess(String process) {
+ return mShell.getProcess(process);
+ }
+
+ /**
+ * @see com.spazedog.lib.rootfw4.Shell#getProcess(Integer)
+ */
+ public static Process getProcess(Integer pid) {
+ return mShell.getProcess(pid);
+ }
+
+ /**
+ * @see com.spazedog.lib.rootfw4.Shell#getMemory()
+ */
+ public static Memory getMemory() {
+ return mShell.getMemory();
+ }
+
+ /**
+ * @see com.spazedog.lib.rootfw4.Shell#getCompCache()
+ */
+ public static CompCache getCompCache() {
+ return mShell.getCompCache();
+ }
+
+ /**
+ * @see com.spazedog.lib.rootfw4.Shell#getSwap(String device)
+ */
+ public static Swap getSwap(String device) {
+ return mShell.getSwap(device);
+ }
+}
diff --git a/libraries/sharedCode/src/main/java/com/spazedog/lib/rootfw4/Shell.java b/libraries/sharedCode/src/main/java/com/spazedog/lib/rootfw4/Shell.java
new file mode 100644
index 0000000..9fb3eef
--- /dev/null
+++ b/libraries/sharedCode/src/main/java/com/spazedog/lib/rootfw4/Shell.java
@@ -0,0 +1,787 @@
+/*
+ * Copyright (C) 2014 Vlad Mihalachi
+ *
+ * This file is part of Turbo Editor.
+ *
+ * Turbo Editor is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Turbo Editor is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.spazedog.lib.rootfw4;
+
+import android.os.Bundle;
+import android.util.Log;
+
+import com.spazedog.lib.rootfw4.ShellStream.OnStreamListener;
+import com.spazedog.lib.rootfw4.containers.Data;
+import com.spazedog.lib.rootfw4.utils.Device;
+import com.spazedog.lib.rootfw4.utils.Device.Process;
+import com.spazedog.lib.rootfw4.utils.File;
+import com.spazedog.lib.rootfw4.utils.Filesystem;
+import com.spazedog.lib.rootfw4.utils.Filesystem.Disk;
+import com.spazedog.lib.rootfw4.utils.Memory;
+import com.spazedog.lib.rootfw4.utils.Memory.CompCache;
+import com.spazedog.lib.rootfw4.utils.Memory.Swap;
+import com.spazedog.lib.rootfw4.utils.io.FileReader;
+import com.spazedog.lib.rootfw4.utils.io.FileWriter;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.WeakHashMap;
+
+/**
+ * This class is a front-end to {@link ShellStream} which makes it easier to work
+ * with normal shell executions. If you need to execute a consistent command (one that never ends),
+ * you should work with {@link ShellStream} directly.
+ */
+public class Shell {
+ public static final String TAG = Common.TAG + ".Shell";
+
+ protected static Set mInstances = Collections.newSetFromMap(new WeakHashMap());
+ protected static Map mBinaries = new HashMap();
+
+ protected Set mBroadcastRecievers = Collections.newSetFromMap(new WeakHashMap());
+ protected Set mConnectionRecievers = new HashSet();
+
+ protected Object mLock = new Object();
+ protected ShellStream mStream;
+ protected Boolean mIsConnected = false;
+ protected Boolean mIsRoot = false;
+ protected List mOutput = null;
+ protected Integer mResultCode = 0;
+ protected Integer mShellTimeout = 15000;
+ protected Set mResultCodes = new HashSet();
+
+ /**
+ * This interface is used internally across utility classes.
+ */
+ public static interface OnShellBroadcastListener {
+ public void onShellBroadcast(String key, Bundle data);
+ }
+
+ /**
+ * This interface is for use with {@link com.spazedog.lib.rootfw4.Shell#executeAsync(String[], com.spazedog.lib.rootfw4.Shell.OnShellResultListener)}.
+ */
+ public static interface OnShellResultListener {
+ /**
+ * Called when an asynchronous execution has finished.
+ *
+ * @param result
+ * The result from the asynchronous execution
+ */
+ public void onShellResult(Result result);
+ }
+
+ /**
+ * This interface is for use with the execute methods. It can be used to validate an attempt command, if that command
+ * cannot be validated by result code alone.
+ */
+ public static interface OnShellValidateListener {
+ /**
+ * Called at the end of each attempt in order to validate it. If this method returns false, then the next attempt will be executed.
+ *
+ * @param command
+ * The command that was executed during this attempt
+ *
+ * @param result
+ * The result code from this attempt
+ *
+ * @param output
+ * The output from this attempt
+ *
+ * @param resultCodes
+ * All of the result codes that has been added as successful ones
+ *
+ * @return
+ * False to continue to the next attempts, or True to stop the current execution
+ */
+ public Boolean onShellValidate(String command, Integer result, List output, Set resultCodes);
+ }
+
+ /**
+ * This interface is used to get information about connection changes.
+ */
+ public static interface OnShellConnectionListener {
+ /**
+ * Called when the connection to the shell is lost.
+ */
+ public void onShellDisconnect();
+ }
+
+ /**
+ * This class is used to store the result from shell executions.
+ * It extends the {@link com.spazedog.lib.rootfw4.containers.Data} class.
+ */
+ public static class Result extends Data {
+ private Integer mResultCode;
+ private Integer[] mValidResults;
+ private Integer mCommandNumber;
+
+ public Result(String[] lines, Integer result, Integer[] validResults, Integer commandNumber) {
+ super(lines);
+
+ mResultCode = result;
+ mValidResults = validResults;
+ mCommandNumber = commandNumber;
+ }
+
+ /**
+ * Get the result code from the shell execution.
+ */
+ public Integer getResultCode() {
+ return mResultCode;
+ }
+
+ /**
+ * Compare the result code with {@link com.spazedog.lib.rootfw4.Shell#addResultCode(Integer)} to determine
+ * whether or not the execution was a success.
+ */
+ public Boolean wasSuccessful() {
+ for (int i=0; i < mValidResults.length; i++) {
+ if ((int) mValidResults[i] == (int) mResultCode) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Get the command number that produced a successful result.
+ */
+ public Integer getCommandNumber() {
+ return mCommandNumber;
+ }
+ }
+
+ /**
+ * A class containing automatically created shell attempts and links to both {@link com.spazedog.lib.rootfw4.Shell#executeAsync(String[], Integer[], com.spazedog.lib.rootfw4.Shell.OnShellResultListener)} and {@link com.spazedog.lib.rootfw4.Shell#execute(String[], Integer[])}
+ *
+ * All attempts are created based on {@link Common#BINARIES}.
+ *
+ * Example: String("ls") would become String["ls", "busybox ls", "toolbox ls"] if {@link Common#BINARIES} equals String[null, "busybox", "toolbox"].
+ *
+ * You can also apply the keyword %binary if you need to apply the binaries to more than the beginning of a command.
+ *
+ * Example: String("(%binary test -d '%binary pwd') || exit 1") would become String["(test -d 'pwd') || exit 1", "(busybox test -d 'busybox pwd') || exit 1", "(toolbox test -d 'toolbox pwd') || exit 1"]
+ *
+ * @see com.spazedog.lib.rootfw4.Shell#createAttempts(String)
+ */
+ public class Attempts {
+ protected String[] mAttempts;
+ protected Integer[] mResultCodes;
+ protected OnShellValidateListener mValidateListener;
+ protected OnShellResultListener mResultListener;
+
+ protected Attempts(String command) {
+ if (command != null) {
+ Integer pos = 0;
+ mAttempts = new String[ Common.BINARIES.length ];
+
+ for (String binary : Common.BINARIES) {
+ if (command.contains("%binary ")) {
+ mAttempts[pos] = command.replaceAll("%binary ", (binary != null && binary.length() > 0 ? binary + " " : ""));
+
+ } else {
+ mAttempts[pos] = (binary != null && binary.length() > 0 ? binary + " " : "") + command;
+ }
+
+ pos += 1;
+ }
+ }
+ }
+
+ public Attempts setValidateListener(OnShellValidateListener listener) {
+ mValidateListener = listener; return this;
+ }
+
+ public Attempts setResultListener(OnShellResultListener listener) {
+ mResultListener = listener; return this;
+ }
+
+ public Attempts setResultCodes(Integer... resultCodes) {
+ mResultCodes = resultCodes; return this;
+ }
+
+ public Result execute(OnShellValidateListener listener) {
+ return setValidateListener(listener).execute();
+ }
+
+ public Result execute() {
+ return Shell.this.execute(mAttempts, mResultCodes, mValidateListener);
+ }
+
+ public void executeAsync(OnShellResultListener listener) {
+ setResultListener(listener).executeAsync();
+ }
+
+ public void executeAsync() {
+ Shell.this.executeAsync(mAttempts, mResultCodes, mValidateListener, mResultListener);
+ }
+ }
+
+ /**
+ * Establish a {@link ShellStream} connection.
+ *
+ * @param requestRoot
+ * Whether or not to request root privileges for the shell connection
+ */
+ public Shell(Boolean requestRoot) {
+ mResultCodes.add(0);
+ mIsRoot = requestRoot;
+
+ /*
+ * Kutch superuser daemon mode sometimes has problems connecting the first time.
+ * So we will give it two tries before giving up.
+ */
+ for (int i=0; i < 2; i++) {
+ if(Common.DEBUG)Log.d(TAG, "Construct: Running connection attempt number " + (i+1));
+
+ mStream = new ShellStream(requestRoot, new OnStreamListener() {
+ @Override
+ public void onStreamStart() {
+ if(Common.DEBUG)Log.d(TAG, "onStreamStart: ...");
+
+ mOutput = new ArrayList();
+ }
+
+ @Override
+ public void onStreamInput(String outputLine) {
+ if(Common.DEBUG)Log.d(TAG, "onStreamInput: " + (outputLine != null ? (outputLine.length() > 50 ? outputLine.substring(0, 50) + " ..." : outputLine) : "NULL"));
+
+ mOutput.add(outputLine);
+ }
+
+ @Override
+ public void onStreamStop(Integer resultCode) {
+ if(Common.DEBUG)Log.d(TAG, "onStreamStop: " + resultCode);
+
+ mResultCode = resultCode;
+ }
+
+ @Override
+ public void onStreamDied() {
+ if(Common.DEBUG)Log.d(TAG, "onStreamDied: The stream has been closed");
+
+ if (mIsConnected) {
+ if(Common.DEBUG)Log.d(TAG, "onStreamDied: The stream seams to have died, reconnecting");
+
+ mStream = new ShellStream(mIsRoot, this);
+
+ if (mStream.isActive()) {
+ Result result = execute("echo connected");
+
+ mIsConnected = result != null && "connected".equals(result.getLine());
+
+ } else {
+ if(Common.DEBUG)Log.d(TAG, "onStreamDied: Could not reconnect");
+
+ mIsConnected = false;
+ }
+ }
+
+ if (!mIsConnected) {
+ for (OnShellConnectionListener reciever : mConnectionRecievers) {
+ reciever.onShellDisconnect();
+ }
+ }
+ }
+ });
+
+ if (mStream.isActive()) {
+ Result result = execute("echo connected");
+
+ mIsConnected = result != null && "connected".equals(result.getLine());
+
+ if (mIsConnected) {
+ if(Common.DEBUG)Log.d(TAG, "Construct: Connection has been established");
+
+ mInstances.add(this); break;
+ }
+ }
+ }
+ }
+
+ /**
+ * Execute a shell command.
+ *
+ * @see com.spazedog.lib.rootfw4.Shell#execute(String[], Integer[])
+ *
+ * @param command
+ * The command to execute
+ */
+ public Result execute(String command) {
+ return execute(new String[]{command}, null, null);
+ }
+
+ /**
+ * Execute a range of commands until one is successful.
+ *
+ * @see com.spazedog.lib.rootfw4.Shell#execute(String[], Integer[])
+ *
+ * @param commands
+ * The commands to try
+ */
+ public Result execute(String[] commands) {
+ return execute(commands, null, null);
+ }
+
+ /**
+ * Execute a range of commands until one is successful.
+ *
+ * Android shells differs a lot from one another, which makes it difficult to program shell scripts for.
+ * This method can help with that by trying different commands until one works.
+ *
+ * Shell.execute( new String(){"cat file", "toolbox cat file", "busybox cat file"} );
+ *
+ * Whether or not a command was successful, depends on {@link com.spazedog.lib.rootfw4.Shell#addResultCode(Integer)} which by default only contains '0'.
+ * The command number that was successful can be checked using {@link com.spazedog.lib.rootfw4.Shell.Result#getCommandNumber()}.
+ *
+ * @param commands
+ * The commands to try
+ *
+ * @param resultCodes
+ * Result Codes representing successful execution. These will be temp. merged with {@link com.spazedog.lib.rootfw4.Shell#addResultCode(Integer)}.
+ *
+ * @param validater
+ * A {@link com.spazedog.lib.rootfw4.Shell.OnShellValidateListener} instance or NULL
+ */
+ public Result execute(String[] commands, Integer[] resultCodes, OnShellValidateListener validater) {
+ synchronized(mLock) {
+ if (mStream.waitFor(mShellTimeout)) {
+ Integer cmdCount = 0;
+ Set codes = new HashSet(mResultCodes);
+
+ if (resultCodes != null) {
+ Collections.addAll(codes, resultCodes);
+ }
+
+ for (String command : commands) {
+ if(Common.DEBUG)Log.d(TAG, "execute: Executing the command '" + command + "'");
+
+ mStream.execute(command);
+
+ if(!mStream.waitFor(mShellTimeout)) {
+ /*
+ * Something is wrong, reconnect to the shell.
+ */
+ mStream.destroy();
+
+ return null;
+ }
+
+ if(Common.DEBUG)Log.d(TAG, "execute: The command finished with the result code '" + mResultCode + "'");
+
+ if ((validater != null && validater.onShellValidate(command, mResultCode, mOutput, codes)) || codes.contains(mResultCode)) {
+ /*
+ * If a validater excepts this, then add the result code to the list of successful codes
+ */
+ codes.add(mResultCode); break;
+ }
+
+ cmdCount += 1;
+ }
+
+ return new Result(mOutput.toArray(new String[mOutput.size()]), mResultCode, codes.toArray(new Integer[codes.size()]), cmdCount);
+ }
+
+ return null;
+ }
+ }
+
+ /**
+ * Execute a shell command asynchronous.
+ *
+ * @see com.spazedog.lib.rootfw4.Shell#executeAsync(String[], Integer[], com.spazedog.lib.rootfw4.Shell.OnShellResultListener)
+ *
+ * @param command
+ * The command to execute
+ *
+ * @param listener
+ * A {@link com.spazedog.lib.rootfw4.Shell.OnShellResultListener} callback instance
+ */
+ public void executeAsync(String command, OnShellResultListener listener) {
+ executeAsync(new String[]{command}, null, null, listener);
+ }
+
+ /**
+ * Execute a range of commands asynchronous until one is successful.
+ *
+ * @see com.spazedog.lib.rootfw4.Shell#executeAsync(String[], Integer[], com.spazedog.lib.rootfw4.Shell.OnShellResultListener)
+ *
+ * @param commands
+ * The commands to try
+ *
+ * @param listener
+ * A {@link com.spazedog.lib.rootfw4.Shell.OnShellResultListener} callback instance
+ */
+ public void executeAsync(String[] commands, OnShellResultListener listener) {
+ executeAsync(commands, null, null, listener);
+ }
+
+ /**
+ * Execute a range of commands asynchronous until one is successful.
+ *
+ * @see com.spazedog.lib.rootfw4.Shell#execute(String[], Integer[])
+ *
+ * @param commands
+ * The commands to try
+ *
+ * @param resultCodes
+ * Result Codes representing successful execution. These will be temp. merged with {@link com.spazedog.lib.rootfw4.Shell#addResultCode(Integer)}.
+ *
+ * @param validater
+ * A {@link com.spazedog.lib.rootfw4.Shell.OnShellValidateListener} instance or NULL
+ *
+ * @param listener
+ * A {@link com.spazedog.lib.rootfw4.Shell.OnShellResultListener} callback instance
+ */
+ public synchronized void executeAsync(final String[] commands, final Integer[] resultCodes, final OnShellValidateListener validater, final OnShellResultListener listener) {
+ if(Common.DEBUG)Log.d(TAG, "executeAsync: Starting an async shell execution");
+
+ /*
+ * If someone execute more than one async task after another, and use the same listener,
+ * we could end up getting the result in the wrong order. We need to make sure that each Thread is started in the correct order.
+ */
+ final Object lock = new Object();
+
+ new Thread() {
+ @Override
+ public void run() {
+ Result result = null;
+
+ synchronized (lock) {
+ lock.notifyAll();
+ }
+
+ synchronized(mLock) {
+ result = Shell.this.execute(commands, resultCodes, validater);
+ }
+
+ listener.onShellResult(result);
+ }
+
+ }.start();
+
+ /*
+ * Do not exit this method, until the Thread is started.
+ */
+ synchronized (lock) {
+ try {
+ lock.wait();
+
+ } catch (InterruptedException e) {}
+ }
+ }
+
+ /**
+ * For internal usage
+ */
+ public static void sendBroadcast(String key, Bundle data) {
+ for (Shell instance : mInstances) {
+ instance.broadcastReciever(key, data);
+ }
+ }
+
+ /**
+ * For internal usage
+ */
+ protected void broadcastReciever(String key, Bundle data) {
+ for (OnShellBroadcastListener recievers : mBroadcastRecievers) {
+ recievers.onShellBroadcast(key, data);
+ }
+ }
+
+ /**
+ * For internal usage
+ */
+ public void addBroadcastListener(OnShellBroadcastListener listener) {
+ mBroadcastRecievers.add(listener);
+ }
+
+ /**
+ * Add a shell connection listener. This callback will be invoked whenever the connection to
+ * the shell changes.
+ *
+ * @param listener
+ * A {@link com.spazedog.lib.rootfw4.Shell.OnShellConnectionListener} callback instance
+ */
+ public void addShellConnectionListener(OnShellConnectionListener listener) {
+ mConnectionRecievers.add(listener);
+ }
+
+ /**
+ * Remove a shell connection listener from the stack.
+ *
+ * @param listener
+ * A {@link com.spazedog.lib.rootfw4.Shell.OnShellConnectionListener} callback instance
+ */
+ public void removeShellConnectionListener(OnShellConnectionListener listener) {
+ mConnectionRecievers.remove(listener);
+ }
+
+ /**
+ * Check whether or not root was requested for this shell.
+ */
+ public Boolean isRoot() {
+ return mIsRoot;
+ }
+
+ /**
+ * Check whether or not a shell connection was established.
+ */
+ public Boolean isConnected() {
+ return mIsConnected;
+ }
+
+ /**
+ * Get the current shell execution timeout.
+ * This is the time in milliseconds from which an execution is killed in case it has stalled.
+ */
+ public Integer getTimeout() {
+ return mShellTimeout;
+ }
+
+ /**
+ * Change the shell execution timeout. This should be in milliseconds.
+ * If this is set to '0', there will be no timeout.
+ */
+ public void setTimeout(Integer timeout) {
+ if (timeout >= 0) {
+ mShellTimeout = timeout;
+ }
+ }
+ /**
+ * Add another result code that represent a successful execution. By default only '0' is used, since
+ * most shell commands uses '0' for success and '1' for error. But some commands uses different values, like 'cat'
+ * that uses '130' as success when piping content.
+ *
+ * @param resultCode
+ * The result code to add to the stack
+ */
+ public void addResultCode(Integer resultCode) {
+ mResultCodes.add(resultCode);
+ }
+
+ /**
+ * Remove a result code from the stack.
+ *
+ * @see com.spazedog.lib.rootfw4.Shell#addResultCode(Integer)
+ *
+ * @param resultCode
+ * The result code to remove from the stack
+ */
+ public void removeResultCode(Integer resultCode) {
+ mResultCodes.remove(resultCode);
+ }
+
+ /**
+ * Reset the stack containing result codes and set it back to default only containing '0'.
+ *
+ * @see com.spazedog.lib.rootfw4.Shell#addResultCode(Integer)
+ */
+ public void resetResultCodes() {
+ mResultCodes.clear();
+ mResultCodes.add(0);
+ }
+
+ /**
+ * Close the shell connection using 'exit 0' if possible, or by force and release all data stored in this instance.
+ */
+ public void destroy() {
+ if (mStream != null) {
+ mIsConnected = false;
+
+ if (mStream.isRunning() || !mStream.isActive()) {
+ if(Common.DEBUG)Log.d(TAG, "destroy: Destroying the stream");
+
+ mStream.destroy();
+
+ } else {
+ if(Common.DEBUG)Log.d(TAG, "destroy: Making a clean exit on the stream");
+
+ execute("exit 0");
+ }
+
+ mStream = null;
+ mInstances.remove(this);
+ mBroadcastRecievers.clear();
+ }
+ }
+
+ /**
+ * Locate whichever toolbox in {@value Common#BINARIES} that supports a specific command.
+ *
+ * Example: String("cat") might return String("busybox cat") or String("toolbox cat")
+ *
+ * @param bin
+ * The command to check
+ */
+ public String findCommand(String bin) {
+ if (!mBinaries.containsKey(bin)) {
+ for (String toolbox : Common.BINARIES) {
+ String cmd = toolbox != null && toolbox.length() > 0 ? toolbox + " " + bin : bin;
+ Result result = execute( cmd + " -h" );
+
+ if (result != null) {
+ String line = result.getLine();
+
+ if (!line.endsWith("not found") && !line.endsWith("such tool")) {
+ mBinaries.put(bin, cmd); break;
+ }
+ }
+ }
+ }
+
+ return mBinaries.get(bin);
+ }
+
+ /**
+ * Create a new instance of {@link com.spazedog.lib.rootfw4.Shell.Attempts}
+ *
+ * @param command
+ * The command to convert into multiple attempts
+ */
+ public Attempts createAttempts(String command) {
+ if (command != null) {
+ return new Attempts(command);
+ }
+
+ return null;
+ }
+
+ /**
+ * Open a new RootFW {@link com.spazedog.lib.rootfw4.utils.io.FileReader}. This is the same as {@link com.spazedog.lib.rootfw4.utils.io.FileReader#FileReader(com.spazedog.lib.rootfw4.Shell, String)}.
+ *
+ * @param file
+ * Path to the file
+ *
+ * @return
+ * NULL if the file could not be opened
+ */
+ public FileReader getFileReader(String file) {
+ try {
+ return new FileReader(this, file);
+
+ } catch (FileNotFoundException e) {
+ return null;
+ }
+ }
+
+ /**
+ * Open a new RootFW {@link com.spazedog.lib.rootfw4.utils.io.FileWriter}. This is the same as {@link com.spazedog.lib.rootfw4.utils.io.FileWriter#FileWriter(com.spazedog.lib.rootfw4.Shell, String, boolean)}.
+ *
+ * @param file
+ * Path to the file
+ *
+ * @param append
+ * Whether or not to append new content to existing content
+ *
+ * @return
+ * NULL if the file could not be opened
+ */
+ public FileWriter getFileWriter(String file, Boolean append) {
+ try {
+ return new FileWriter(this, file, append);
+
+ } catch (IOException e) {
+ return null;
+ }
+ }
+
+ /**
+ * Get a new {@link com.spazedog.lib.rootfw4.utils.File} instance.
+ *
+ * @param file
+ * Path to the file or directory
+ */
+ public File getFile(String file) {
+ return new File(this, file);
+ }
+
+ /**
+ * Get a new {@link com.spazedog.lib.rootfw4.utils.Filesystem} instance.
+ */
+ public Filesystem getFilesystem() {
+ return new Filesystem(this);
+ }
+
+ /**
+ * Get a new {@link com.spazedog.lib.rootfw4.utils.Filesystem.Disk} instance.
+ *
+ * @param disk
+ * Path to a disk, partition or a mount point
+ */
+ public Disk getDisk(String disk) {
+ return new Disk(this, disk);
+ }
+
+
+ /**
+ * Get a new {@link com.spazedog.lib.rootfw4.utils.Device} instance.
+ */
+ public Device getDevice() {
+ return new Device(this);
+ }
+
+ /**
+ * Get a new {@link com.spazedog.lib.rootfw4.utils.Device.Process} instance.
+ *
+ * @param process
+ * The name of the process
+ */
+ public Process getProcess(String process) {
+ return new Process(this, process);
+ }
+
+ /**
+ * Get a new {@link com.spazedog.lib.rootfw4.utils.Device.Process} instance.
+ *
+ * @param pid
+ * The process id
+ */
+ public Process getProcess(Integer pid) {
+ return new Process(this, pid);
+ }
+
+ /**
+ * Get a new {@link com.spazedog.lib.rootfw4.utils.Memory} instance.
+ */
+ public Memory getMemory() {
+ return new Memory(this);
+ }
+
+ /**
+ * Get a new {@link com.spazedog.lib.rootfw4.utils.Memory.CompCache} instance.
+ */
+ public CompCache getCompCache() {
+ return new CompCache(this);
+ }
+
+ /**
+ * Get a new {@link com.spazedog.lib.rootfw4.utils.Memory.Swap} instance.
+ *
+ * @param device
+ * The /dev/ swap device
+ */
+ public Swap getSwap(String device) {
+ return new Swap(this, device);
+ }
+}
diff --git a/libraries/sharedCode/src/main/java/com/spazedog/lib/rootfw4/ShellStream.java b/libraries/sharedCode/src/main/java/com/spazedog/lib/rootfw4/ShellStream.java
new file mode 100644
index 0000000..ad4b3f9
--- /dev/null
+++ b/libraries/sharedCode/src/main/java/com/spazedog/lib/rootfw4/ShellStream.java
@@ -0,0 +1,358 @@
+/*
+ * Copyright (C) 2014 Vlad Mihalachi
+ *
+ * This file is part of Turbo Editor.
+ *
+ * Turbo Editor is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Turbo Editor is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.spazedog.lib.rootfw4;
+
+import java.io.BufferedReader;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.ArrayList;
+import java.util.List;
+
+import android.util.Log;
+
+/**
+ * This class opens a connection to the shell and creates a consistent output stream
+ * that can be read using the {@link com.spazedog.lib.rootfw4.ShellStream.OnStreamListener} interface. It also
+ * contains an input stream that can be used to execute shell commands.
+ */
+public class ShellStream {
+ public static final String TAG = Common.TAG + ".ShellStream";
+
+ protected Process mConnection;
+
+ protected DataOutputStream mStdInput;
+ protected BufferedReader mStdOutput;
+
+ protected Thread mStdOutputWorker;
+
+ protected OnStreamListener mListener;
+
+ protected final Counter mCounter = new Counter();
+ protected final Object mLock = new Object();
+ protected Boolean mIsActive = false;
+ protected Boolean mIsRoot = false;
+
+ protected String mCommandEnd = "EOL:a00c38d8:EOL";
+
+ protected static class Counter {
+ private volatile Integer mCount = 0;
+ private volatile Object mLock = new Object();
+
+ public Integer size() {
+ synchronized(mLock) {
+ return mCount;
+ }
+ }
+
+ public Integer encrease() {
+ synchronized(mLock) {
+ return (mCount += 1);
+ }
+ }
+
+ public Integer decrease() {
+ synchronized(mLock) {
+ return mCount > 0 ? (mCount -= 1) : (mCount = 0);
+ }
+ }
+
+ public void reset() {
+ synchronized(mLock) {
+ mCount = 0;
+ }
+ }
+ }
+
+ /**
+ * This interface is used to read the input from the shell.
+ */
+ public static interface OnStreamListener {
+ /**
+ * This is called before a command is sent to the shell output stream.
+ */
+ public void onStreamStart();
+
+ /**
+ * This is called on each line from the shell input stream.
+ *
+ * @param inputLine
+ * The current shell input line that is being processed
+ */
+ public void onStreamInput(String outputLine);
+
+ /**
+ * This is called after all shell input has been processed.
+ *
+ * @param exitCode
+ * The exit code returned by the shell
+ */
+ public void onStreamStop(Integer resultCode);
+
+ /**
+ * This is called when the shell connection dies.
+ * This can either be because a command executed 'exit', or if the method {@link com.spazedog.lib.rootfw4.ShellStream#destroy()} was called.
+ */
+ public void onStreamDied();
+ }
+
+ /**
+ * Connect to a shell and create a consistent I/O stream.
+ */
+ public ShellStream(Boolean requestRoot, OnStreamListener listener) {
+ try {
+ if(Common.DEBUG)Log.d(TAG, "Construct: Establishing a new shell stream");
+
+ ProcessBuilder builder = new ProcessBuilder(requestRoot ? "su" : "sh");
+ builder.redirectErrorStream(true);
+
+ mIsRoot = requestRoot;
+ mIsActive = true;
+ mListener = listener;
+ mConnection = builder.start();
+ mStdInput = new DataOutputStream(mConnection.getOutputStream());
+ mStdOutput = new BufferedReader(new InputStreamReader(mConnection.getInputStream()));
+
+ mStdOutputWorker = new Thread() {
+ @Override
+ public void run() {
+ String output = null;
+
+ try {
+ while (mIsActive && (output = mStdOutput.readLine()) != null) {
+ if (mListener != null && mCounter.size() > 0) {
+ if (output.startsWith(mCommandEnd)) {
+ Integer result = 0;
+
+ try {
+ result = Integer.parseInt(output.substring(mCommandEnd.length()+1));
+
+ } catch (Throwable e) {
+ Log.w(TAG, e.getMessage(), e);
+ }
+
+ mListener.onStreamStop(result);
+ mCounter.decrease();
+
+ synchronized(mLock) {
+ mLock.notifyAll();
+ }
+
+ } else {
+ mListener.onStreamInput(output);
+ }
+ }
+ }
+
+ } catch (IOException e) {
+ Log.w(TAG, e.getMessage(), e); output = null;
+ }
+
+ if (output == null) {
+ ShellStream.this.destroy();
+ }
+ }
+ };
+
+ mStdOutputWorker.start();
+
+ } catch (IOException e) {
+ Log.w(TAG, e.getMessage(), e); mIsActive = false;
+ }
+ }
+
+ /**
+ * Send a command to the shell input stream.
+ *
+ * This method is executed asynchronous. If you need to wait until the command finishes,
+ * then use {@link com.spazedog.lib.rootfw4.ShellStream#waitFor()}.
+ *
+ * @param command
+ * The command to send to the shell
+ */
+ public synchronized void execute(final String command) {
+ final Object lock = new Object();
+
+ new Thread() {
+ @Override
+ public void run() {
+ mCounter.encrease();
+
+ synchronized(lock) {
+ lock.notifyAll();
+ }
+
+ synchronized(mLock) {
+ if (waitFor(0, -1)) {
+ mListener.onStreamStart();
+
+ String input = command + "\n";
+ input += "echo " + mCommandEnd + " $?\n";
+
+ try {
+ mStdInput.write( input.getBytes() );
+
+ /*
+ * Things often get written to the shell without flush().
+ * This breaks when using exit, as it some times get destroyed before reaching here.
+ */
+ if (mStdInput != null) {
+ mStdInput.flush();
+ }
+
+ } catch (IOException e) {
+ Log.w(TAG, e.getMessage(), e);
+ }
+ }
+ }
+ }
+
+ }.start();
+
+ synchronized (lock) {
+ try {
+ lock.wait();
+
+ } catch (InterruptedException e) {}
+ }
+ }
+
+ /**
+ * Sleeps until the shell is done with a current command and ready for new input.
+ *
+ * @see {@link com.spazedog.lib.rootfw4.ShellStream#waitFor(Integer)}
+ *
+ * @return
+ * True if the shell connection is OK or false on connection error
+ */
+ public Boolean waitFor() {
+ return waitFor(0, 0);
+ }
+
+ /**
+ * Sleeps until the shell is done with a current command and ready for new input,
+ * or until the specified timeout has expired.
+ *
+ * Note that this method keeps track of the order of executions. This means that
+ * the shell might not be ready, just because this lock was cleared. There might have been
+ * added more locks after this one was set.
+ *
+ * @param timeout
+ * Timeout in milliseconds
+ *
+ * @return
+ * True if the shell connection is OK or false on connection error
+ */
+ public Boolean waitFor(Integer timeout) {
+ return waitFor(timeout, 0);
+ }
+
+ /**
+ * This is an internal method, which is used to change which object to add a lock to.
+ */
+ protected Boolean waitFor(Integer timeout, Integer index) {
+ Integer counter = mCounter.size()+index;
+
+ if (counter > 0) {
+ Long timeoutMilis = timeout > 0 ? System.currentTimeMillis() + timeout : 0L;
+
+ synchronized(mLock) {
+ while (mCounter.size() > 0 && mIsActive) {
+ try {
+ counter -= 1;
+
+ mLock.wait(timeout.longValue());
+
+ if (timeout > 0 && System.currentTimeMillis() >= timeoutMilis) {
+ return mCounter.size() == 0 && mIsActive;
+
+ } else if (counter <= 0) {
+ return mIsActive;
+ }
+
+ } catch (InterruptedException e) {
+ Log.w(TAG, e.getMessage(), e);
+ }
+ }
+ }
+ }
+
+ return mIsActive;
+ }
+
+ /**
+ * Check whether there is a connection to the shell.
+ *
+ * @return
+ * True if there is a connection or False if not
+ */
+ public Boolean isActive() {
+ return mIsActive;
+ }
+
+ /**
+ * Check whether the shell is currently busy processing a command.
+ *
+ * @return
+ * True if the shell is busy or False otherwise
+ */
+ public Boolean isRunning() {
+ return mCounter.size() > 0;
+ }
+
+ /**
+ * Check whether or not root was requested for this instance.
+ */
+ public Boolean isRoot() {
+ return mIsRoot;
+ }
+
+ /**
+ * Close the shell connection.
+ *
+ * This will force close the connection. Use this only when running a consistent command (if {@link com.spazedog.lib.rootfw4.ShellStream#isRunning()} returns true).
+ * When possible, sending the 'exit' command to the shell is a better choice.
+ *
+ * This method is executed asynchronous.
+ */
+ public synchronized void destroy() {
+ if (mStdInput != null) {
+ mIsActive = false;
+
+ mCounter.reset();
+
+ try {
+ mStdInput.close();
+ mStdInput = null;
+
+ } catch (IOException e) {}
+
+ mStdOutputWorker.interrupt();
+ mStdOutputWorker = null;
+
+ synchronized (mLock) {
+ mLock.notifyAll();
+ }
+
+ mListener.onStreamDied();
+ mListener = null;
+ }
+ }
+}
diff --git a/libraries/sharedCode/src/main/java/com/spazedog/lib/rootfw4/containers/BasicContainer.java b/libraries/sharedCode/src/main/java/com/spazedog/lib/rootfw4/containers/BasicContainer.java
new file mode 100644
index 0000000..636d16d
--- /dev/null
+++ b/libraries/sharedCode/src/main/java/com/spazedog/lib/rootfw4/containers/BasicContainer.java
@@ -0,0 +1,35 @@
+/*
+ * This file is part of the RootFW Project: https://github.com/spazedog/rootfw
+ *
+ * Copyright (c) 2015 Daniel Bergløv
+ *
+ * RootFW is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+
+ * RootFW is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with RootFW. If not, see
+ */
+
+package com.spazedog.lib.rootfw4.containers;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public abstract class BasicContainer {
+ private Map mObjects = new HashMap();
+
+ public void putObject(String name, Object object) {
+ mObjects.put(name, object);
+ }
+
+ public Object getObject(String name) {
+ return mObjects.get(name);
+ }
+}
diff --git a/libraries/sharedCode/src/main/java/com/spazedog/lib/rootfw4/containers/Data.java b/libraries/sharedCode/src/main/java/com/spazedog/lib/rootfw4/containers/Data.java
new file mode 100644
index 0000000..2c0b828
--- /dev/null
+++ b/libraries/sharedCode/src/main/java/com/spazedog/lib/rootfw4/containers/Data.java
@@ -0,0 +1,414 @@
+/*
+ * Copyright (C) 2014 Vlad Mihalachi
+ *
+ * This file is part of Turbo Editor.
+ *
+ * Turbo Editor is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Turbo Editor is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.spazedog.lib.rootfw4.containers;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import android.text.TextUtils;
+
+/**
+ * This container is used to store any kind of data. All of the data is located within a String Array, where each index is considered a line.
+ */
+@SuppressWarnings("unchecked")
+public class Data> extends BasicContainer {
+ protected String[] mLines;
+
+ /**
+ * This interface is used as the argument for the sort()
and assort()
methods. It can be used to create custom sorting of the data array.
+ */
+ public static interface DataSorting {
+ /**
+ * The method which checks the line and determines whether or not it should be added or removed from the array.
+ *
+ * Note that the sort()
method will remove the line upon false, whereas assort()
will remove it upon true.
+ *
+ * @param input
+ * One line of the data array
+ */
+ public Boolean test(String input);
+ }
+
+ /**
+ * This interface is used as the argument for the replace()
method. It can be used to replace or alter lines in the output.
+ */
+ public static interface DataReplace {
+ /**
+ * This method is used to alter the lines in the data array. Each line is parsed to this method, and whatever is returned will replace the current line.
+ *
+ * @param input
+ * One line of the data array
+ */
+ public String replace(String input);
+ }
+
+ /**
+ * Create a new Data instance.
+ *
+ * @param lines
+ * An array representing the lines of the data
+ */
+ public Data(String[] lines) {
+ mLines = lines;
+ }
+
+ /**
+ * This can be used to replace part of a line, or the whole line. It uses the replace() method in the DataSorting interface where custom replacement of a line can be done. It parses the original line as an argument, and requires the new line to be returned.
+ *
+ * @param DataSorting
+ * An instance of the DataSorting
class which should handle the line replacement
+ *
+ * @return
+ * This instance
+ */
+ public DATATYPE replace(DataReplace dataReplace) {
+ if (size() > 0) {
+ List list = new ArrayList();
+
+ for (int i=0; i < mLines.length; i++) {
+ list.add( dataReplace.replace(mLines[i]) );
+ }
+ }
+
+ return (DATATYPE) this;
+ }
+
+ /**
+ * This can be used to replace whole lines based on a contained pattern.
+ *
+ * @param contains
+ * The pattern which the line should contain
+ *
+ * @param newLine
+ * The new line that should be used as a replacement
+ *
+ * @return
+ * This instance
+ */
+ public DATATYPE replace(final String contains, final String newLine) {
+ return (DATATYPE) replace(new DataReplace() {
+ @Override
+ public String replace(String input) {
+ return input != null && input.contains(contains) ? newLine : input;
+ }
+ });
+ }
+
+ /**
+ * This is used to determine whether or not to remove lines from the data array. Each line will be parsed to the custom DataSorting
instance and then removed upon a true return.
+ *
+ * @param DataSorting
+ * An instance of the DataSorting
class which should determine whether or not to remove the line
+ *
+ * @return
+ * This instance
+ */
+ public DATATYPE assort(DataSorting test) {
+ if (size() > 0) {
+ List list = new ArrayList();
+
+ for (int i=0; i < mLines.length; i++) {
+ if (!test.test(mLines[i])) {
+ list.add(mLines[i]);
+ }
+ }
+
+ mLines = list.toArray( new String[list.size()] );
+ }
+
+ return (DATATYPE) this;
+ }
+
+ /**
+ * This is used to determine whether or not to remove lines from the data array. Each line will be compared to the argument. If the line contains anything from the argument, it will be removed from the data array.
+ *
+ * @param contains
+ * A string to locate within each line to determine whether or not to remove the line
+ *
+ * @return
+ * This instance
+ */
+ public DATATYPE assort(final String contains) {
+ return (DATATYPE) assort(new DataSorting() {
+ @Override
+ public Boolean test(String input) {
+ return input.contains( contains );
+ }
+ });
+ }
+
+ /**
+ * This is used to determine whether or not to keep lines in the data array. Each line will be parsed to the custom DataSorting
instance and then removed upon a false return.
+ *
+ * @param DataSorting
+ * An instance of the DataSorting
interface which should determine whether or not to keep the line
+ *
+ * @return
+ * This instance
+ */
+ public DATATYPE sort(DataSorting test) {
+ if (size() > 0) {
+ List list = new ArrayList();
+
+ for (int i=0; i < mLines.length; i++) {
+ if (test.test(mLines[i])) {
+ list.add(mLines[i]);
+ }
+ }
+
+ mLines = list.toArray( new String[list.size()] );
+ }
+
+ return (DATATYPE) this;
+ }
+
+ /**
+ * This is used to determine whether or not to keep lines in the data array. Each line will be compared to the argument. If the line contains anything from the argument, it will not be removed from the data array.
+ *
+ * @param contains
+ * A string to locate within each line to determine whether or not to remove the line
+ *
+ * @return
+ * This instance
+ */
+ public DATATYPE sort(final String contains) {
+ return (DATATYPE) sort(new DataSorting() {
+ public Boolean test(String input) {
+ return input.contains( contains );
+ }
+ });
+ }
+
+ /**
+ * @see com.spazedog.lib.rootfw4.containers.Data#sort(Integer, Integer)
+ */
+ public DATATYPE sort(Integer start) {
+ return (DATATYPE) sort(start, mLines.length);
+ }
+
+ /**
+ * This is used to determine whether or not to keep lines in the data array. The method will keep each index within the start
and stop
indexes parsed via the arguments.
+ *
+ * Note that the method will also except negative values.
+ *
+ *
+ * - Example 1:
+ * - Remove the first and last index in the array
+ * sort(1, -1)
+ *
+ *
+ *
+ * - Example 2:
+ * - Only keep the first and last index in the array
+ * sort(-1, 1)
+ *
+ *
+ * @param start
+ * Where to start
+ *
+ * @param stop
+ * Where to stop
+ *
+ * @return
+ * This instance
+ */
+ public DATATYPE sort(Integer start, Integer stop) {
+ if (size() > 0) {
+ List list = new ArrayList();
+ Integer begin = start < 0 ? (mLines.length + start) : start;
+ Integer end = stop < 0 ? (mLines.length + stop) : stop;
+
+ Integer[] min = null, max = null;
+
+ if (begin > end) {
+ if (end == 0) {
+ min = new Integer[]{ begin };
+ max = new Integer[]{ mLines.length };
+
+ } else {
+ min = new Integer[]{ 0, begin };
+ max = new Integer[]{ end, mLines.length };
+ }
+
+ } else {
+ min = new Integer[]{ begin };
+ max = new Integer[]{ end };
+ }
+
+ for (int i=0; i < min.length; i++) {
+ for (int x=min[i]; x < max[i]; x++) {
+ list.add(mLines[x]);
+ }
+ }
+
+ mLines = list.toArray( new String[list.size()] );
+ }
+
+ return (DATATYPE) this;
+ }
+
+ /**
+ * This is used to determine whether or not to remove lines from the data array. The method will remove each index within the start
and stop
indexes parsed via the arguments.
+ *
+ * Note that the method will also except negative values.
+ *
+ *
+ * - Example 1:
+ * - Remove the first and last index in the array
+ * sort(-1, 1)
+ *
+ *
+ *
+ * - Example 2:
+ * - Only keep the first and last index in the array
+ * sort(1, -1)
+ *
+ *
+ * @param start
+ * Where to start
+ *
+ * @param stop
+ * Where to stop
+ *
+ * @return
+ * This instance
+ */
+ public DATATYPE assort(Integer start, Integer stop) {
+ return (DATATYPE) sort(stop, start);
+ }
+
+ /**
+ * @see com.spazedog.lib.rootfw4.containers.Data#assort(Integer, Integer)
+ */
+ public DATATYPE assort(Integer start) {
+ return (DATATYPE) assort(mLines.length, start);
+ }
+
+ /**
+ * This method will remove all of the empty lines from the data array
+ *
+ * @return
+ * This instance
+ */
+ public DATATYPE trim() {
+ if (size() > 0) {
+ List list = new ArrayList();
+
+ for (int i=0; i < mLines.length; i++) {
+ if (mLines[i].trim().length() > 0) {
+ list.add(mLines[i]);
+ }
+ }
+
+ mLines = list.toArray( new String[list.size()] );
+ }
+
+ return (DATATYPE) this;
+ }
+
+ /**
+ * This will return the data array
+ *
+ * @return
+ * The data array
+ */
+ public String[] getArray() {
+ return mLines;
+ }
+
+ /**
+ * This will return a string of the data array with added line breakers
+ *
+ * @return
+ * The data array as a string
+ */
+ public String getString() {
+ return getString("\n");
+ }
+
+ /**
+ * This will return a string of the data array with custom characters used as line breakers
+ *
+ * @param start
+ * A separator character used to separate each line
+ *
+ * @return
+ * The data array as a string
+ */
+ public String getString(String separater) {
+ return mLines == null ? null : TextUtils.join(separater, Arrays.asList(mLines));
+ }
+
+ /**
+ * @return
+ * The last non-empty line of the data array
+ */
+ public String getLine() {
+ return getLine(-1, true);
+ }
+
+ /**
+ * @see com.spazedog.lib.rootfw4.containers.Data#getLine(Integer, Boolean)
+ */
+ public String getLine(Integer aLineNumber) {
+ return getLine(aLineNumber, false);
+ }
+
+ /**
+ * This will return one specified line of the data array.
+ *
+ * Note that this also takes negative number to get a line from the end and up
+ *
+ * @param aLineNumber
+ * The line number to return
+ *
+ * @param aSkipEmpty
+ * Whether or not to include empty lines
+ *
+ * @return
+ * The specified line
+ */
+ public String getLine(Integer aLineNumber, Boolean aSkipEmpty) {
+ if (size() > 0) {
+ Integer count = aLineNumber < 0 ? (mLines.length + aLineNumber) : aLineNumber;
+
+ while(count >= 0 && count < mLines.length) {
+ if (!aSkipEmpty || mLines[count].trim().length() > 0) {
+ return mLines[count].trim();
+ }
+
+ count = aLineNumber < 0 ? (count - 1) : (count + 1);
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Count the lines in the data array
+ *
+ * @return
+ * The number of lines
+ */
+ public Integer size() {
+ return mLines == null ? 0 : mLines.length;
+ }
+}
diff --git a/libraries/sharedCode/src/main/java/com/spazedog/lib/rootfw4/utils/Device.java b/libraries/sharedCode/src/main/java/com/spazedog/lib/rootfw4/utils/Device.java
new file mode 100644
index 0000000..433ca69
--- /dev/null
+++ b/libraries/sharedCode/src/main/java/com/spazedog/lib/rootfw4/utils/Device.java
@@ -0,0 +1,444 @@
+/*
+ * Copyright (C) 2014 Vlad Mihalachi
+ *
+ * This file is part of Turbo Editor.
+ *
+ * Turbo Editor is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Turbo Editor is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.spazedog.lib.rootfw4.utils;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Pattern;
+
+import android.content.Context;
+import android.os.PowerManager;
+import android.util.Log;
+
+import com.spazedog.lib.rootfw4.Common;
+import com.spazedog.lib.rootfw4.Shell;
+import com.spazedog.lib.rootfw4.Shell.Result;
+import com.spazedog.lib.rootfw4.containers.BasicContainer;
+
+public class Device {
+ public static final String TAG = Common.TAG + ".Device";
+
+ protected final static Pattern oPatternPidMatch = Pattern.compile("^[0-9]+$");
+ protected final static Pattern oPatternSpaceSearch = Pattern.compile("[ \t]+");
+
+ protected Shell mShell;
+
+ /**
+ * This is a container class used to store information about a process.
+ */
+ public static class ProcessInfo extends BasicContainer {
+ private String mPath;
+ private String mProcess;
+ private Integer mProcessId;
+
+ /**
+ * @return
+ * The process path (Could be NULL) as not all processes has a path assigned
+ */
+ public String path() {
+ return mPath;
+ }
+
+ /**
+ * @return
+ * The name of the process
+ */
+ public String name() {
+ return mProcess;
+ }
+
+ /**
+ * @return
+ * The pid of the process
+ */
+ public Integer pid() {
+ return mProcessId;
+ }
+ }
+
+ public Device(Shell shell) {
+ mShell = shell;
+ }
+
+ /**
+ * Return a list of all active processes.
+ */
+ public ProcessInfo[] getProcessList() {
+ return getProcessList(null);
+ }
+
+ /**
+ * Return a list of all active processes which names matches 'pattern'.
+ * Note that this does not provide an advanced search, it just checks whether or not 'pattern' exists in the process name.
+ *
+ * @param pattern
+ * The pattern to search for
+ */
+ public ProcessInfo[] getProcessList(String pattern) {
+ String[] files = mShell.getFile("/proc").getList();
+
+ if (files != null) {
+ List processes = new ArrayList();
+ String process = null;
+ String path = null;
+
+ for (int i=0; i < files.length; i++) {
+ if (oPatternPidMatch.matcher(files[i]).matches()) {
+ if ((process = mShell.getFile("/proc/" + files[i] + "/cmdline").readOneLine()) == null) {
+ if ((process = mShell.getFile("/proc/" + files[i] + "/stat").readOneLine()) != null) {
+ try {
+ if (pattern == null || process.contains(pattern)) {
+ process = oPatternSpaceSearch.split(process.trim())[1];
+ process = process.substring(1, process.length()-1);
+
+ } else {
+ continue;
+ }
+
+ } catch(Throwable e) { process = null; }
+ }
+
+ } else if (pattern == null || process.contains(pattern)) {
+ if (process.contains("/")) {
+ try {
+ path = process.substring(process.indexOf("/"), process.contains("-") ? process.indexOf("-", process.lastIndexOf("/", process.indexOf("-"))) : process.length());
+ } catch (Throwable e) { path = null; }
+
+ if (!process.startsWith("/")) {
+ process = process.substring(0, process.indexOf("/"));
+
+ } else {
+ try {
+ process = process.substring(process.lastIndexOf("/", process.contains("-") ? process.indexOf("-") : process.length())+1, process.contains("-") ? process.indexOf("-", process.lastIndexOf("/", process.indexOf("-"))) : process.length());
+
+ } catch (Throwable e) { process = null; }
+ }
+
+ } else if (process.contains("-")) {
+ process = process.substring(0, process.indexOf("-"));
+ }
+
+ } else {
+ continue;
+ }
+
+ if (pattern == null || (process != null && process.contains(pattern))) {
+ ProcessInfo stat = new ProcessInfo();
+ stat.mPath = path;
+ stat.mProcess = process;
+ stat.mProcessId = Integer.parseInt(files[i]);
+
+ processes.add(stat);
+ }
+ }
+ }
+
+ return processes.toArray( new ProcessInfo[ processes.size() ] );
+ }
+
+ return null;
+ }
+
+ /**
+ * Reboots the device into the recovery.
+ *
+ * This method first tries using the {@link android.os.PowerManager}, if that fails it fallbacks on using the reboot command from toolbox.
+ *
+ * Note that using the {@link android.os.PowerManager} requires your app to optain the 'REBOOT' permission. If you don't want this, just parse NULL as {@link android.content.Context}
+ * and the method will use the fallback. This however is more likely to fail, as many toolbox versions does not support the reboot command.
+ * And since only the kernel can write to the CBC, we need a native caller to invoke this. So there is no fallback for missing toolbox support when it comes
+ * to rebooting into the recovery.
+ *
+ * @param context
+ * A {@link android.content.Context} or NULL to skip using the {@link android.os.PowerManager}
+ */
+ public Boolean rebootRecovery(Context context) {
+ if (context != null) {
+ try {
+ PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
+ pm.reboot(null);
+
+ /*
+ * This will never be reached if the reboot is successful
+ */
+ return false;
+
+ } catch (Throwable e) {}
+ }
+
+ Result result = mShell.execute("toolbox reboot recovery");
+
+ return result != null && result.wasSuccessful();
+ }
+
+ /**
+ * Invokes a soft reboot on the device (Restart all services) by using a sysrq trigger.
+ */
+ public Boolean rebootSoft() {
+ Result result = mShell.execute("echo 1 > /proc/sys/kernel/sysrq && echo s > /proc/sysrq-trigger && echo e > /proc/sysrq-trigger");
+
+ return result != null && result.wasSuccessful();
+ }
+
+ /**
+ * Reboots the device.
+ *
+ * This method first tries using the reboot command from toolbox.
+ * But since some toolbox versions does not have this, it further fallbacks on using a sysrq trigger.
+ */
+ public Boolean reboot() {
+ Result result = mShell.execute("toolbox reboot");
+
+ if (result == null || !result.wasSuccessful()) {
+ result = mShell.execute("echo 1 > /proc/sys/kernel/sysrq && echo s > /proc/sysrq-trigger && echo b > /proc/sysrq-trigger");
+ }
+
+ return result != null && result.wasSuccessful();
+ }
+
+ /**
+ * Shuts down the device.
+ *
+ * This method first tries using the reboot command from toolbox.
+ * But since some toolbox versions does not have this, it further fallbacks on using a sysrq trigger.
+ */
+ public Boolean shutdown() {
+ Result result = mShell.execute("toolbox reboot -p");
+
+ if (result == null || !result.wasSuccessful()) {
+ result = mShell.execute("echo 1 > /proc/sys/kernel/sysrq && echo s > /proc/sysrq-trigger && echo o > /proc/sysrq-trigger");
+ }
+
+ return result != null && result.wasSuccessful();
+ }
+
+ /**
+ * Get a new {@link com.spazedog.lib.rootfw4.utils.Device.Process} instance
+ *
+ * @param process
+ * The name of the process
+ */
+ public Process getProcess(String process) {
+ return new Process(mShell, process);
+ }
+
+ /**
+ * Get a new {@link com.spazedog.lib.rootfw4.utils.Device.Process} instance
+ *
+ * @param pid
+ * The process id
+ */
+ public Process getProcess(Integer pid) {
+ return new Process(mShell, pid);
+ }
+
+ public static class Process extends Device {
+
+ protected Integer mPid;
+ protected String mProcess;
+
+ public Process(Shell shell, String process) {
+ super(shell);
+
+ if (oPatternPidMatch.matcher(process).matches()) {
+ mPid = Integer.parseInt(process);
+
+ } else {
+ mProcess = process;
+ }
+ }
+
+ public Process(Shell shell, Integer pid) {
+ super(shell);
+
+ mPid = pid;
+ }
+
+ /**
+ * Get the pid of the current process.
+ * If you initialized this object using a process id, this method will return that id.
+ * Otherwise it will return the first found pid in /proc.
+ */
+ public Integer getPid() {
+ /*
+ * A process might have more than one pid. So we never cache a search. If mPid is not null,
+ * then a specific pid was chosen for this instance, and this is what we should work with.
+ * But if a process name was chosen, we should never cache the pid as we might one the next one if we kill this process or it dies or reboots.
+ */
+ if (mPid != null) {
+ return mPid;
+ }
+
+ /*
+ * Busybox returns 1 both when pidof is not supported and if the process does not exist.
+ * We need to check if we have some kind of pidof support from either busybox, toolbox or another registered binary.
+ * If not, we fallback on a /proc search.
+ */
+ String cmd = mShell.findCommand("pidof");
+
+ if (cmd != null) {
+ Result result = mShell.execute(cmd + " '" + mProcess + "'");
+
+ String pids = result.getLine();
+
+ if (pids != null) {
+ try {
+ return Integer.parseInt(oPatternSpaceSearch.split(pids.trim())[0]);
+
+ } catch (Throwable e) {
+ Log.w(TAG, e.getMessage(), e);
+ }
+ }
+
+ } else {
+ ProcessInfo[] processes = getProcessList();
+
+ if (processes != null) {
+ for (int i=0; i < processes.length; i++) {
+ if (mProcess.equals(processes[i].name())) {
+ return processes[i].pid();
+ }
+ }
+ }
+ }
+
+ return 0;
+ }
+
+ /**
+ * Get a list of all pid's for this process name.
+ */
+ public Integer[] getPids() {
+ String name = getName();
+ String cmd = mShell.findCommand("pidof");
+
+ if (cmd != null) {
+ Result result = mShell.createAttempts(cmd + " '" + name + "'").execute();
+
+ if (result != null && result.wasSuccessful()) {
+ String pids = result.getLine();
+
+ if (pids != null) {
+ String[] parts = oPatternSpaceSearch.split(pids.trim());
+ Integer[] values = new Integer[ parts.length ];
+
+ for (int i=0; i < parts.length; i++) {
+ try {
+ values[i] = Integer.parseInt(parts[i]);
+
+ } catch(Throwable e) {}
+ }
+
+ return values;
+ }
+ }
+
+ } else {
+ ProcessInfo[] processes = getProcessList();
+
+ if (name != null && processes != null && processes.length > 0) {
+ List list = new ArrayList();
+
+ for (int i=0; i < processes.length; i++) {
+ if (name.equals(processes[i].name())) {
+ list.add(processes[i].pid());
+ }
+ }
+
+ return list.toArray( new Integer[ list.size() ] );
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Get the process name of the current process.
+ * If you initialized this object using a process name, this method will return that name.
+ * Otherwise it will locate it in /proc based on the pid.
+ */
+ public String getName() {
+ if (mProcess != null) {
+ return mProcess;
+ }
+
+ String process = null;
+
+ if ((process = mShell.getFile("/proc/" + mPid + "/cmdline").readOneLine()) == null) {
+ if ((process = mShell.getFile("/proc/" + mPid + "/stat").readOneLine()) != null) {
+ try {
+ process = oPatternSpaceSearch.split(process.trim())[1];
+ process = process.substring(1, process.length()-1);
+
+ } catch(Throwable e) { process = null; }
+ }
+
+ } else if (process.contains("/")) {
+ if (!process.startsWith("/")) {
+ process = process.substring(0, process.indexOf("/"));
+
+ } else {
+ try {
+ process = process.substring(process.lastIndexOf("/", process.contains("-") ? process.indexOf("-") : process.length())+1, process.contains("-") ? process.indexOf("-", process.lastIndexOf("/", process.indexOf("-"))) : process.length());
+
+ } catch (Throwable e) { process = null; }
+ }
+
+ } else if (process.contains("-")) {
+ process = process.substring(0, process.indexOf("-"));
+ }
+
+ return process;
+ }
+
+ /**
+ * Kill this process.
+ * If you initialized this object using a pid, only this single process will be killed.
+ * If you used a process name, all processes with this process name will be killed.
+ */
+ public Boolean kill() {
+ Result result = null;
+
+ if (mPid != null) {
+ result = mShell.createAttempts("kill -9 '" + mPid + "'").execute();
+
+ } else {
+ result = mShell.createAttempts("killall '" + mProcess + "'").execute();
+
+ /*
+ * Toolbox does not support killall
+ */
+ if (result == null || !result.wasSuccessful()) {
+ Integer[] pids = getPids();
+
+ for (Integer pid : pids) {
+ result = mShell.createAttempts("kill -9 '" + pid + "'").execute();
+
+ if (result == null || !result.wasSuccessful()) {
+ return false;
+ }
+ }
+ }
+ }
+
+ return result != null && result.wasSuccessful();
+ }
+ }
+}
diff --git a/libraries/sharedCode/src/main/java/com/spazedog/lib/rootfw4/utils/File.java b/libraries/sharedCode/src/main/java/com/spazedog/lib/rootfw4/utils/File.java
new file mode 100644
index 0000000..6fe016a
--- /dev/null
+++ b/libraries/sharedCode/src/main/java/com/spazedog/lib/rootfw4/utils/File.java
@@ -0,0 +1,1674 @@
+/*
+ * Copyright (C) 2014 Vlad Mihalachi
+ *
+ * This file is part of Turbo Editor.
+ *
+ * Turbo Editor is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Turbo Editor is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.spazedog.lib.rootfw4.utils;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.spazedog.lib.rootfw4.Common;
+import com.spazedog.lib.rootfw4.Shell;
+import com.spazedog.lib.rootfw4.Shell.Attempts;
+import com.spazedog.lib.rootfw4.Shell.OnShellBroadcastListener;
+import com.spazedog.lib.rootfw4.Shell.OnShellResultListener;
+import com.spazedog.lib.rootfw4.Shell.OnShellValidateListener;
+import com.spazedog.lib.rootfw4.Shell.Result;
+import com.spazedog.lib.rootfw4.containers.BasicContainer;
+import com.spazedog.lib.rootfw4.containers.Data;
+import com.spazedog.lib.rootfw4.containers.Data.DataSorting;
+import com.spazedog.lib.rootfw4.utils.Filesystem.DiskStat;
+import com.spazedog.lib.rootfw4.utils.Filesystem.MountStat;
+import com.spazedog.lib.rootfw4.utils.io.FileReader;
+import com.spazedog.lib.rootfw4.utils.io.FileWriter;
+
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Pattern;
+
+public class File {
+ public static final String TAG = Common.TAG + ".File";
+
+ protected final static Pattern oPatternEscape = Pattern.compile("([\"\'`\\\\])");
+ protected final static Pattern oPatternColumnSearch = Pattern.compile("[ ]{2,}");
+ protected final static Pattern oPatternSpaceSearch = Pattern.compile("[ \t]+");
+ protected final static Pattern oPatternStatSplitter = Pattern.compile("\\|");
+ protected final static Pattern oPatternStatSearch = Pattern.compile("^([a-z-]+)(?:[ \t]+([0-9]+))?[ \t]+([0-9a-z_]+)[ \t]+([0-9a-z_]+)(?:[ \t]+(?:([0-9]+),[ \t]+)?([0-9]+))?[ \t]+([A-Za-z]+[ \t]+[0-9]+[ \t]+[0-9:]+|[0-9-/]+[ \t]+[0-9:]+)[ \t]+(?:(.*) -> )?(.*)$");
+
+ protected final static Map oOctals = new HashMap();
+ static {
+ oOctals.put("1:r", 400);
+ oOctals.put("2:w", 200);
+ oOctals.put("3:x", 100);
+ oOctals.put("3:s", 4100);
+ oOctals.put("3:S", 4000);
+ oOctals.put("4:r", 40);
+ oOctals.put("5:w", 20);
+ oOctals.put("6:x", 10);
+ oOctals.put("6:s", 2010);
+ oOctals.put("6:S", 2000);
+ oOctals.put("7:r", 4);
+ oOctals.put("8:w", 2);
+ oOctals.put("9:x", 1);
+ oOctals.put("9:t", 1001);
+ oOctals.put("9:T", 1000);
+ }
+
+ protected java.io.File mFile;
+ protected Shell mShell;
+ protected final Object mLock = new Object();
+
+ protected Integer mExistsLevel = -1;
+ protected Integer mFolderLevel = -1;
+ protected Integer mLinkLevel = -1;
+
+ /**
+ * This class is extended from the Data class. As for now, there is nothing custom added to this class. But it might differ from the Data class at some point.
+ */
+ public static class FileData extends Data {
+ public FileData(String[] lines) {
+ super(lines);
+ }
+ }
+
+ /**
+ * This class is a container which is used by {@link FileExtender#getDetails()} and {@link FileExtender#getDetailedList(Integer)}
+ */
+ public static class FileStat extends BasicContainer {
+ private String mName;
+ private String mLink;
+ private String mType;
+ private Integer mUser;
+ private Integer mGroup;
+ private String mAccess;
+ private Integer mPermission;
+ private String mMM;
+ private Long mSize;
+
+ /**
+ * @return
+ * The filename
+ */
+ public String name() {
+ return mName;
+ }
+
+ /**
+ * @return
+ * The path to the original file if this is a symbolic link
+ */
+ public String link() {
+ return mLink;
+ }
+
+ /**
+ * @return
+ * The file type ('d'=>Directory, 'f'=>File, 'b'=>Block Device, 'c'=>Character Device, 'l'=>Symbolic Link)
+ */
+ public String type() {
+ return mType;
+ }
+
+ /**
+ * @return
+ * The owners user id
+ */
+ public Integer user() {
+ return mUser;
+ }
+
+ /**
+ * @return
+ * The owners group id
+ */
+ public Integer group() {
+ return mGroup;
+ }
+
+ /**
+ * @return
+ * The files access string like (drwxrwxr-x)
+ */
+ public String access() {
+ return mAccess;
+ }
+
+ /**
+ * @return
+ * The file permissions like (0755)
+ */
+ public Integer permission() {
+ return mPermission;
+ }
+
+ /**
+ * @return
+ * The file Major:Minor number (If this is a Block or Character device file)
+ */
+ public String mm() {
+ return mMM;
+ }
+
+ /**
+ * @return
+ * The file size in bytes
+ */
+ public Long size() {
+ return mSize;
+ }
+ }
+
+ public File(Shell shell, String file) {
+ mFile = new java.io.File(file);
+ mShell = shell;
+
+ /*
+ * This broadcast listener lets us update information about files in other instances of this class.
+ * Some information uses a lot of resources to gather and are quite persistent, and by using this mechanism, we can cache those information
+ * and still have them updated when needed.
+ */
+ mShell.addBroadcastListener(new OnShellBroadcastListener() {
+ @Override
+ public void onShellBroadcast(String key, Bundle data) {
+ if ("file".equals(key)) {
+ String action = data.getString("action");
+ String location = data.getString("location");
+
+ if ("exists".equals(action) && (getAbsolutePath().equals(location) || getAbsolutePath().startsWith(location + "/"))) {
+ mExistsLevel = -1;
+ mFolderLevel = -1;
+ mLinkLevel = -1;
+
+ } else if ("moved".equals(action) && getAbsolutePath().equals(location)) {
+ mFile = new java.io.File(data.getString("destination"));
+ }
+ }
+ }
+ });
+ }
+
+ /**
+ * Get information about this file or folder. This will return information like
+ * size (on files), path to linked file (on links), permissions, group, user etc.
+ *
+ * @return
+ * A new {@link com.spazedog.lib.rootfw4.utils.File.FileStat} object with all the file information
+ */
+ public FileStat getDetails() {
+ synchronized (mLock) {
+ if (exists()) {
+ FileStat[] stat = getDetailedList(1);
+
+ if (stat != null && stat.length > 0) {
+ String name = mFile.getName();
+
+ if (stat[0].name().equals(".")) {
+ stat[0].mName = name;
+
+ return stat[0];
+
+ } else if (stat[0].name().equals(name)) {
+ return stat[0];
+
+ } else {
+ /* On devices without busybox, we could end up using limited toolbox versions
+ * that does not support the "-a" argument in it's "ls" command. In this case,
+ * we need to do a more manual search for folders.
+ */
+ stat = getParentFile().getDetailedList();
+
+ if (stat != null && stat.length > 0) {
+ for (int i=0; i < stat.length; i++) {
+ if (stat[i].name().equals(name)) {
+ return stat[i];
+ }
+ }
+ }
+ }
+ }
+ }
+
+ return null;
+ }
+ }
+
+ /**
+ * @see #getDetailedList(Integer)
+ */
+ public FileStat[] getDetailedList() {
+ return getDetailedList(0);
+ }
+
+ /**
+ * This is the same as {@link #getDetails()}, only this will provide a whole list
+ * with information about each item in a directory.
+ *
+ * @param maxLines
+ * The max amount of lines to return. This also excepts negative numbers. 0 equals all lines.
+ *
+ * @return
+ * An array of {@link com.spazedog.lib.rootfw4.utils.File.FileStat} object
+ */
+ public FileStat[] getDetailedList(Integer maxLines) {
+ synchronized (mLock) {
+ if (exists()) {
+ String path = getAbsolutePath();
+ String[] attemptCommands = new String[]{"ls -lna '" + path + "'", "ls -la '" + path + "'", "ls -ln '" + path + "'", "ls -l '" + path + "'"};
+
+ for (String command : attemptCommands) {
+ Result result = mShell.createAttempts(command).execute();
+
+ if (result.wasSuccessful()) {
+ List list = new ArrayList();
+ String[] lines = result.trim().getArray();
+ Integer maxIndex = (maxLines == null || maxLines == 0 ? lines.length : (maxLines < 0 ? lines.length + maxLines : maxLines));
+
+ for (int i=0,indexCount=1; i < lines.length && indexCount <= maxIndex; i++) {
+ /* There are a lot of different output from the ls command, depending on the arguments supported, whether we used busybox or toolbox and the versions of the binaries.
+ * We need some serious regexp help to sort through all of the different output options.
+ */
+ String[] parts = oPatternStatSplitter.split( oPatternStatSearch.matcher(lines[i]).replaceAll("$1|$3|$4|$5|$6|$8|$9") );
+
+ if (parts.length == 7) {
+ FileStat stat = new FileStat();
+
+ stat.mType = parts[0].substring(0, 1).equals("-") ? "f" : parts[0].substring(0, 1);
+ stat.mAccess = parts[0];
+ stat.mUser = Common.getUID(parts[1]);
+ stat.mGroup = Common.getUID(parts[2]);
+ stat.mSize = parts[4].equals("null") || !parts[3].equals("null") ? 0L : Long.parseLong(parts[4]);
+ stat.mMM = parts[3].equals("null") ? null : parts[3] + ":" + parts[4];
+ stat.mName = parts[5].equals("null") ? parts[6].substring( parts[6].lastIndexOf("/") + 1 ) : parts[5].substring( parts[5].lastIndexOf("/") + 1 );
+ stat.mLink = parts[5].equals("null") ? null : parts[6];
+ stat.mPermission = 0;
+
+ for (int x=1; x < stat.mAccess.length(); x++) {
+ Character ch = stat.mAccess.charAt(x);
+ Integer number = oOctals.get(x + ":" + ch);
+
+ if (number != null) {
+ stat.mPermission += number;
+ }
+ }
+
+ if (stat.mName.contains("/")) {
+ stat.mName = stat.mName.substring( stat.mName.lastIndexOf("/")+1 );
+ }
+
+ list.add(stat);
+
+ indexCount++;
+ }
+ }
+
+ return list.toArray( new FileStat[ list.size() ] );
+ }
+ }
+ }
+
+ return null;
+ }
+ }
+
+ /**
+ * This will provide a simple listing of a directory.
+ * For a more detailed listing, use {@link #getDetailedList()} instead.
+ *
+ * @return
+ * An array with the names of all the items in the directory
+ */
+ public String[] getList() {
+ synchronized (mLock) {
+ if (isDirectory()) {
+ String[] list = mFile.list();
+
+ if (list == null) {
+ String path = getAbsolutePath();
+ String[] commands = new String[]{"ls -a1 '" + path + "'", "ls -a '" + path + "'", "ls '" + path + "'"};
+
+ for (int i=0; i < commands.length; i++) {
+ Result result = mShell.createAttempts(commands[i]).execute();
+
+ if (result != null && result.wasSuccessful()) {
+ if (i == 0) {
+ result.sort(new DataSorting(){
+ @Override
+ public Boolean test(String input) {
+ return !".".equals(input) && !"..".equals(input);
+ }
+ });
+
+ return result.getArray();
+
+ } else {
+ /*
+ * Most toolbox versions supports very few flags, and 'ls -a' on toolbox might return
+ * a list, whereas busybox mostly returns columns. So we need to be able to handle both types of output.
+ * Some toolbox versions does not support any flags at all, and they differ from each version about what kind of output
+ * they return.
+ */
+ String[] lines = oPatternColumnSearch.split( result.trim().getString(" ").trim() );
+ List output = new ArrayList();
+
+ for (String line : lines) {
+ if (!".".equals(line) && !"..".equals(line)) {
+ output.add(line);
+ }
+ }
+
+ return output.toArray(new String[output.size()]);
+ }
+ }
+ }
+ }
+
+ return list;
+ }
+
+ return null;
+ }
+ }
+
+ /**
+ * Extract the first line of the file.
+ *
+ * @return
+ * The first line of the file as a string
+ */
+ public String readOneLine() {
+ synchronized (mLock) {
+ if (isFile()) {
+ try {
+ BufferedReader reader = new BufferedReader(new java.io.FileReader(mFile));
+ String line = reader.readLine();
+ reader.close();
+
+ return line;
+
+ } catch (Throwable e) {
+ String[] attemptCommands = new String[]{"sed -n '1p' '" + getAbsolutePath() + "' 2> /dev/null", "cat '" + getAbsolutePath() + "' 2> /dev/null"};
+
+ for (String command : attemptCommands) {
+ Result result = mShell.createAttempts(command).execute();
+
+ if (result != null && result.wasSuccessful()) {
+ return result.getLine(0);
+ }
+ }
+ }
+ }
+
+ return null;
+ }
+ }
+
+ /**
+ * Extract the content from the file and return it.
+ *
+ * @return
+ * The entire file content wrapped in a {@link com.spazedog.lib.rootfw4.utils.File.FileData} object
+ */
+ public FileData read() {
+ synchronized (mLock) {
+ if (isFile()) {
+ try {
+ BufferedReader reader = new BufferedReader(new java.io.FileReader(mFile));
+ List content = new ArrayList();
+ String line;
+
+ while ((line = reader.readLine()) != null) {
+ content.add(line);
+ }
+
+ reader.close();
+
+ return new FileData( content.toArray( new String[ content.size() ] ) );
+
+ } catch(Throwable e) {
+ Result result = mShell.createAttempts("cat '" + getAbsolutePath() + "' 2> /dev/null").execute();
+
+ if (result != null && result.wasSuccessful()) {
+ return new FileData( result.getArray() );
+ }
+ }
+ }
+
+ return null;
+ }
+ }
+
+ /**
+ * Search the file line by line to find a match for a specific word or sentence and return all of the matched lines or the ones not matching.
+ *
+ * @param match
+ * Word or sentence to match
+ *
+ * @param invert
+ * Whether or not to return the non-matching lines instead
+ *
+ * @return
+ * All of the matched or non-matched lines wrapped in a {@link com.spazedog.lib.rootfw4.utils.File.FileData} object
+ */
+ public FileData readMatch(final String match, final Boolean invert) {
+ synchronized (mLock) {
+ if (isFile()) {
+ try {
+ BufferedReader reader = new BufferedReader(new java.io.FileReader(mFile));
+ List content = new ArrayList();
+ String line;
+
+ while ((line = reader.readLine()) != null) {
+ if (invert != line.contains(match)) {
+ content.add(line);
+ }
+ }
+
+ reader.close();
+
+ return new FileData( content.toArray( new String[ content.size() ] ) );
+
+ } catch (Throwable e) {
+ String escapedMatch = oPatternEscape.matcher(match).replaceAll("\\\\$1");
+
+ /*
+ * 'grep' returns failed on 0 matches, which will normally make the shell continue it's attempts.
+ * So we use a validate listener to check the output. If there is no real errors, then we will have no output.
+ */
+ Result result = mShell.createAttempts("grep " + (invert ? "-v " : "") + "'" + escapedMatch + "' '" + getAbsolutePath() + "'").execute(new OnShellValidateListener(){
+ @Override
+ public Boolean onShellValidate(String command, Integer result, List output, Set resultCodes) {
+ return result.equals(0) || output.size() == 0;
+ }
+ });
+
+ if (result.wasSuccessful()) {
+ return new FileData( result.getArray() );
+
+ } else {
+ result = mShell.createAttempts("cat '" + getAbsolutePath() + "' 2> /dev/null").execute();
+
+ if (result != null && result.wasSuccessful()) {
+ result.sort(new DataSorting() {
+ @Override
+ public Boolean test(String input) {
+ return invert != input.contains(match);
+ }
+ });
+
+ return new FileData( result.getArray() );
+ }
+ }
+ }
+ }
+
+ return null;
+ }
+ }
+
+ /**
+ * @see #write(String[], Boolean)
+ */
+ public Boolean write(String input) {
+ return write(input.trim().split("\n"), false);
+ }
+
+ /**
+ * @see #write(String[], Boolean)
+ */
+ public Boolean write(String input, Boolean append) {
+ return write(input.trim().split("\n"), append);
+ }
+
+ /**
+ * @see #write(String[], Boolean)
+ */
+ public Boolean write(String[] input) {
+ return write(input, false);
+ }
+
+ /**
+ * Write data to the file. The data should be an array where each index is a line that should be written to the file.
+ *
+ * If the file does not already exist, it will be created.
+ *
+ * @param input
+ * The data that should be written to the file
+ *
+ * @param append
+ * Whether or not to append the data to the existing content in the file
+ *
+ * @return
+ * True
if the data was successfully written to the file, False
otherwise
+ */
+ public Boolean write(String[] input, Boolean append) {
+ synchronized (mLock) {
+ Boolean status = false;
+
+ if (input != null && !isDirectory()) {
+ try {
+ BufferedWriter output = new BufferedWriter(new java.io.FileWriter(mFile, append));
+
+ for (String line : input) {
+ output.write(line);
+ output.newLine();
+ }
+
+ output.close();
+ status = true;
+
+ } catch(Throwable e) {
+ String redirect = append ? ">>" : ">";
+ String path = getAbsolutePath();
+
+ for (String line : input) {
+ String escapedInput = oPatternEscape.matcher(line).replaceAll("\\\\$1");
+ Attempts attempts = mShell.createAttempts("echo '" + escapedInput + "' " + redirect + " '" + path + "' 2> /dev/null");
+
+ if (!(status = attempts.execute().wasSuccessful())) {
+ break;
+ }
+
+ redirect = ">>";
+ }
+ }
+
+ /*
+ * Alert other instances using this file, that the state might have changed.
+ */
+ if (status) {
+ Bundle bundle = new Bundle();
+ bundle.putString("action", "exists");
+ bundle.putString("location", getAbsolutePath());
+
+ Shell.sendBroadcast("file", bundle);
+ }
+ }
+
+ return status;
+ }
+ }
+
+ /**
+ * Remove the file.
+ * Folders will be recursively cleaned before deleting.
+ *
+ * @return
+ * True
if the file was deleted, False
otherwise
+ */
+ public Boolean remove() {
+ synchronized (mLock) {
+ Boolean status = false;
+
+ if (exists()) {
+ String[] fileList = getList();
+ String path = getAbsolutePath();
+
+ if (fileList != null) {
+ for (String intry : fileList) {
+ if(!getFile(path + "/" + intry).remove()) {
+ return false;
+ }
+ }
+ }
+
+ if (!(status = mFile.delete())) {
+ String rmCommand = isFile() || isLink() ? "unlink" : "rmdir";
+ String[] commands = new String[]{"rm -rf '" + path + "' 2> /dev/null", rmCommand + " '" + path + "' 2> /dev/null"};
+
+ for (String command : commands) {
+ Result result = mShell.createAttempts(command).execute();
+
+ if (result != null && (status = result.wasSuccessful())) {
+ break;
+ }
+ }
+ }
+
+ /*
+ * Alert other instances using this file, that the state might have changed.
+ */
+ if (status) {
+ Bundle bundle = new Bundle();
+ bundle.putString("action", "exists");
+ bundle.putString("location", path);
+
+ Shell.sendBroadcast("file", bundle);
+ }
+
+ } else {
+ status = true;
+ }
+
+ return status;
+ }
+ }
+
+ /**
+ * Create a new directory based on the path from this file object.
+ *
+ * @see #createDirectories()
+ *
+ * @return
+ * True
if the directory was created successfully or if it existed to begin with, False
oherwise
+ */
+ public Boolean createDirectory() {
+ synchronized (mLock) {
+ Boolean status = false;
+
+ if (!exists()) {
+ if (!(status = mFile.mkdir())) {
+ Result result = mShell.createAttempts("mkdir '" + getAbsolutePath() + "' 2> /dev/null").execute();
+
+ if (result == null || !(status = result.wasSuccessful())) {
+ return false;
+ }
+ }
+
+ /*
+ * Alert other instances using this directory, that the state might have changed.
+ */
+ if (status) {
+ Bundle bundle = new Bundle();
+ bundle.putString("action", "exists");
+ bundle.putString("location", getAbsolutePath());
+
+ Shell.sendBroadcast("file", bundle);
+ }
+
+ } else {
+ status = isDirectory();
+ }
+
+ return status;
+ }
+ }
+
+ /**
+ * Create a new directory based on the path from this file object.
+ * The method will also create any missing parent directories.
+ *
+ * @see #createDirectory()
+ *
+ * @return
+ * True
if the directory was created successfully
+ */
+ public Boolean createDirectories() {
+ synchronized (mLock) {
+ Boolean status = false;
+
+ if (!exists()) {
+ if (!(status = mFile.mkdirs())) {
+ Result result = mShell.createAttempts("mkdir -p '" + getAbsolutePath() + "' 2> /dev/null").execute();
+
+ if (result == null || !(status = result.wasSuccessful())) {
+ /*
+ * Some toolbox version does not support the '-p' flag in 'mkdir'
+ */
+ String[] dirs = getAbsolutePath().substring(1).split("/");
+ String path = "";
+
+ for (String dir : dirs) {
+ path = path + "/" + dir;
+
+ if (!(status = getFile(path).createDirectory())) {
+ return false;
+ }
+ }
+ }
+ }
+
+ /*
+ * Alert other instances using this directory, that the state might have changed.
+ */
+ if (status) {
+ Bundle bundle = new Bundle();
+ bundle.putString("action", "exists");
+ bundle.putString("location", getAbsolutePath());
+
+ Shell.sendBroadcast("file", bundle);
+ }
+
+ } else {
+ status = isDirectory();
+ }
+
+ return status;
+ }
+ }
+
+ /**
+ * Create a link to this file.
+ *
+ * @param linkPath
+ * Path (Including name) to the link which should be created
+ *
+ * @return
+ * True
on success, False
otherwise
+ */
+ public Boolean createLink(String linkPath) {
+ synchronized (mLock) {
+ File linkFile = getFile(linkPath);
+ Boolean status = false;
+
+ if (exists() && !linkFile.exists()) {
+ Result result = mShell.createAttempts("ln -s '" + getAbsolutePath() + "' '" + linkFile.getAbsolutePath() + "' 2> /dev/null").execute();
+
+ if (result == null || !(status = result.wasSuccessful())) {
+ return false;
+ }
+
+ /*
+ * Alert other instances using this directory, that the state might have changed.
+ */
+ if (status) {
+ Bundle bundle = new Bundle();
+ bundle.putString("action", "exists");
+ bundle.putString("location", linkFile.getAbsolutePath());
+
+ Shell.sendBroadcast("file", bundle);
+ }
+
+ } else if (exists() && linkFile.isLink()) {
+ status = getAbsolutePath().equals(linkFile.getCanonicalPath());
+ }
+
+ return status;
+ }
+ }
+
+ /**
+ * Create a reference from this path to another (This will become the link)
+ *
+ * @param linkPath
+ * Path (Including name) to the original location
+ *
+ * @return
+ * True
on success, False
otherwise
+ */
+ public Boolean createAsLink(String originalPath) {
+ return getFile(originalPath).createLink(getAbsolutePath());
+ }
+
+ /**
+ * @see #move(String, Boolean)
+ */
+ public Boolean move(String dstPath) {
+ return move(dstPath, false);
+ }
+
+ /**
+ * Move the file to another location.
+ *
+ * @param dstPath
+ * The destination path including the file name
+ *
+ * @return
+ * True
on success, False
otherwise
+ */
+ public Boolean move(String dstPath, Boolean overwrite) {
+ synchronized (mLock) {
+ Boolean status = false;
+
+ if (exists()) {
+ File dstFile = getFile(dstPath);
+
+ if (!dstFile.exists() || (overwrite && dstFile.remove())) {
+ if (!(status = mFile.renameTo(dstFile.mFile))) {
+ Result result = mShell.createAttempts("mv '" + getAbsolutePath() + "' '" + dstFile.getAbsolutePath() + "'").execute();
+
+ if (result == null || !(status = result.wasSuccessful())) {
+ return false;
+ }
+ }
+ }
+
+ /*
+ * Alert other instances using this file, that it has been moved.
+ */
+ if (status) {
+ Bundle bundle = new Bundle();
+ bundle.putString("action", "exists");
+ bundle.putString("location", dstFile.getAbsolutePath());
+ Shell.sendBroadcast("file", bundle);
+
+ bundle.putString("action", "moved");
+ bundle.putString("location", getAbsolutePath());
+ bundle.putString("destination", dstFile.getAbsolutePath());
+
+ mFile = dstFile.mFile;
+ Shell.sendBroadcast("file", bundle);
+ }
+ }
+
+ return status;
+ }
+ }
+
+ /**
+ * Rename the file.
+ *
+ * @param name
+ * The new name to use
+ *
+ * @return
+ * True
on success, False
otherwise
+ */
+ public Boolean rename(String name) {
+ return move( (getParentPath() == null ? "" : getParentPath()) + "/" + name, false );
+ }
+
+ /**
+ * @see #copy(String, Boolean, Boolean)
+ */
+ public Boolean copy(String dstPath) {
+ return copy(dstPath, false, false);
+ }
+
+ /**
+ * @see #copy(String, Boolean, Boolean)
+ */
+ public Boolean copy(String dstPath, Boolean overwrite) {
+ return copy(dstPath, overwrite, false);
+ }
+
+ /**
+ * Copy the file to another location.
+ *
+ * @param dstPath
+ * The destination path
+ *
+ * @param overwrite
+ * Overwrite any existing files. If false, then folders will be merged if a destination folder exist.
+ *
+ * @param preservePerms
+ * Preserve permissions
+ *
+ * @return
+ * True
on success, False
otherwise
+ */
+ public Boolean copy(String dstPath, Boolean overwrite, Boolean preservePerms) {
+ synchronized (mLock) {
+ Boolean status = false;
+
+ if (exists()) {
+ File dstFile = getFile(dstPath);
+ FileStat stat = null;
+
+ /*
+ * On overwrite, delete the destination if it exists, and make sure that we are able to recreate
+ * destination directory, if the source is one.
+ *
+ * On non-overwrite, skip files if they exists, or merge if source and destination are directories.
+ */
+ if (isLink()) {
+ if (!dstFile.exists() || (overwrite && dstFile.remove())) {
+ stat = getDetails();
+
+ if (stat == null || stat.link() == null || !(status = dstFile.createAsLink(stat.link()))) {
+ return false;
+ }
+ }
+
+ } else if (isDirectory() && (!overwrite || (!dstFile.exists() || dstFile.remove())) && ((!dstFile.exists() && dstFile.createDirectories()) || dstFile.isDirectory())) {
+ String[] list = getList();
+
+ if (list != null) {
+ status = true;
+ String srcAbsPath = getAbsolutePath();
+ String dstAbsPath = dstFile.getAbsolutePath();
+
+ for (String entry : list) {
+ File entryFile = getFile(srcAbsPath + "/" + entry);
+
+ if (!(status = entryFile.copy(dstAbsPath + "/" + entry, overwrite, preservePerms))) {
+ if (entryFile.isDirectory() || overwrite == entryFile.exists()) {
+ return false;
+
+ } else {
+ status = true;
+ }
+ }
+ }
+ }
+
+ } else if (!isDirectory() && (!dstFile.exists() || (overwrite && dstFile.remove()))) {
+ try {
+ InputStream input = new FileInputStream(mFile);
+ OutputStream output = new FileOutputStream(dstFile.mFile);
+
+ byte[] buffer = new byte[1024];
+ Integer length;
+
+ while ((length = input.read(buffer)) > 0) {
+ output.write(buffer, 0, length);
+ }
+
+ input.close();
+ output.close();
+
+ status = true;
+
+ } catch (Throwable e) {
+ Result result = mShell.createAttempts("cat '" + getAbsolutePath() + "' > '" + dstFile.getAbsolutePath() + "' 2> /dev/null").execute();
+
+ if (result == null || !(status = result.wasSuccessful())) {
+ return false;
+ }
+ }
+ }
+
+ if (status) {
+ Bundle bundle = new Bundle();
+ bundle.putString("action", "exists");
+ bundle.putString("location", dstFile.getAbsolutePath());
+
+ Shell.sendBroadcast("file", bundle);
+
+ if (preservePerms) {
+ if (stat == null) {
+ stat = getDetails();
+ }
+
+ dstFile.changeAccess(stat.user(), stat.group(), stat.permission(), false);
+ }
+ }
+ }
+
+ return status;
+ }
+ }
+
+ /**
+ * @see #changeAccess(String, String, Integer, Boolean)
+ */
+ public Boolean changeAccess(String user, String group, Integer mod) {
+ return changeAccess(Common.getUID(user), Common.getUID(group), mod, false);
+ }
+
+ /**
+ * @see #changeAccess(Integer, Integer, Integer, Boolean)
+ */
+ public Boolean changeAccess(Integer user, Integer group, Integer mod) {
+ return changeAccess(user, group, mod, false);
+ }
+
+ /**
+ * Change ownership (user and group) and permissions on a file or directory.
+ *
+ * Never use octal numbers for the permissions like '0775'. Always write it as '775', otherwise it will be converted
+ * and your permissions will not be changed to the expected value. The reason why this argument is an Integer, is to avoid
+ * things like 'a+x', '+x' and such. While this is supported in Linux normally, few Android binaries supports it as they have been
+ * stripped down to the bare minimum.
+ *
+ * @param user
+ * The user name or NULL if this should not be changed
+ *
+ * @param group
+ * The group name or NULL if this should not be changed
+ *
+ * @param mod
+ * The octal permissions or -1 if this should not be changed
+ *
+ * @param recursive
+ * Change the access recursively
+ */
+ public Boolean changeAccess(String user, String group, Integer mod, Boolean recursive) {
+ return changeAccess(Common.getUID(user), Common.getUID(group), mod, recursive);
+ }
+
+ /**
+ * Change ownership (user and group) and permissions on a file or directory.
+ *
+ * Never use octal numbers for the permissions like '0775'. Always write it as '775', otherwise it will be converted
+ * and your permissions will not be changed to the expected value. The reason why this argument is an Integer, is to avoid
+ * things like 'a+x', '+x' and such. While this is supported in Linux normally, few Android binaries supports it as they have been
+ * stripped down to the bare minimum.
+ *
+ * @param user
+ * The uid or -1 if this should not be changed
+ *
+ * @param group
+ * The gid or -1 if this should not be changed
+ *
+ * @param mod
+ * The octal permissions or -1 if this should not be changed
+ *
+ * @param recursive
+ * Change the access recursively
+ */
+ public Boolean changeAccess(Integer user, Integer group, Integer mod, Boolean recursive) {
+ synchronized (mLock) {
+ StringBuilder builder = new StringBuilder();
+
+ if ((user != null && user >= 0) || (group != null && group >= 0)) {
+ builder.append("%binary chown ");
+
+ if (recursive)
+ builder.append("-R ");
+
+ if (user != null && user >= 0)
+ builder.append("" + user);
+
+ if (group != null && group >= 0)
+ builder.append("." + user);
+ }
+
+ if (mod != null && mod > 0) {
+ if (builder.length() > 0)
+ builder.append(" && ");
+
+ builder.append("%binary chmod ");
+
+ if (recursive)
+ builder.append("-R ");
+
+ builder.append((mod <= 777 ? "0" : "") + mod);
+ }
+
+ if (builder.length() > 0) {
+ Result result = mShell.createAttempts(builder.toString()).execute();
+
+ if (result != null && result.wasSuccessful()) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Calculates the size of a file or folder.
+ *
+ * Note that on directories with a lot of sub-folders and files,
+ * this can be a slow operation.
+ *
+ * @return
+ * 0 if the file does not exist, or if it is actually 0 in size of course.
+ */
+ public Long size() {
+ synchronized (mLock) {
+ Long size = 0L;
+
+ if (exists()) {
+ if (isDirectory()) {
+ String[] list = getList();
+
+ if (list != null) {
+ String path = getAbsolutePath();
+
+ for (String entry : list) {
+ size += getFile(path + "/" + entry).size();
+ }
+ }
+
+ } else if ((size = mFile.length()) == 0) {
+ String path = getAbsolutePath();
+ String[] commands = new String[]{"wc -c < '" + path + "' 2> /dev/null", "wc < '" + path + "' 2> /dev/null"};
+ Result result = null;
+
+ for (int i=0; i < commands.length; i++) {
+ result = mShell.createAttempts(commands[i]).execute();
+
+ if (result != null && result.wasSuccessful()) {
+ try {
+ size = Long.parseLong( (i > 0 ? oPatternSpaceSearch.split(result.getLine().trim())[2] : result.getLine()) );
+
+ } catch (Throwable e) {
+ result = null;
+ }
+
+ break;
+ }
+ }
+
+ if (result == null || !result.wasSuccessful()) {
+ FileStat stat = getDetails();
+
+ if (stat != null) {
+ size = stat.size();
+ }
+ }
+ }
+ }
+
+ return size;
+ }
+ }
+
+ /**
+ * Make this file executable and run it in the shell.
+ */
+ public Result runInShell() {
+ synchronized(mLock) {
+ if (isFile() && changeAccess(-1, -1, 777)) {
+ return mShell.execute(getAbsolutePath());
+ }
+
+ return null;
+ }
+ }
+
+ /**
+ * Make this file executable and run it asynchronized in the shell.
+ *
+ * @param listener
+ * An {@link OnShellResultListener} which will receive the output
+ */
+ public void runInShell(OnShellResultListener listener) {
+ synchronized(mLock) {
+ if (isFile() && changeAccess(-1, -1, 777)) {
+ mShell.executeAsync(getAbsolutePath(), listener);
+ }
+ }
+ }
+
+ /**
+ * Reboot into recovery and run this file/package
+ *
+ * This method will add a command file in /cache/recovery which will tell the recovery the location of this
+ * package. The recovery will then run the package and then automatically reboot back into Android.
+ *
+ * Note that this will also work on ROM's that changes the cache location or device. The method will
+ * locate the real internal cache partition, and it will also mount it at a second location
+ * if it is not already mounted.
+ *
+ * @param context
+ * A {@link android.content.Context} that can be used together with the Android REBOOT
permission
+ * to use the PowerManager
to reboot into recovery. This can be set to NULL
+ * if you want to just use the toolbox reboot
command, however do note that not all
+ * toolbox versions support this command.
+ *
+ * @param args
+ * Arguments which will be parsed to the recovery package.
+ * Each argument equels one prop line.
+ *
+ * Each prop line is added to /cache/recovery/rootfw.prop and named (argument[argument number] = [value]).
+ * For an example, if first argument is "test", it will be written to rootfw.prop as (argument1 = test).
+ *
+ * @return
+ * False if it failed
+ */
+ public Boolean runInRecovery(Context context, String... args) {
+ if (isFile()) {
+ String cacheLocation = "/cache";
+ MountStat mountStat = mShell.getDisk(cacheLocation).getFsDetails();
+
+ if (mountStat != null) {
+ DiskStat diskStat = mShell.getDisk( mountStat.device() ).getDiskDetails();
+
+ if (diskStat == null || !cacheLocation.equals(diskStat.location())) {
+ if (diskStat == null) {
+ mShell.getDisk("/").mount(new String[]{"rw"});
+ cacheLocation = "/cache-int";
+
+ if (!getFile(cacheLocation).createDirectory()) {
+ return false;
+
+ } else if (!mShell.getDisk(mountStat.device()).mount(cacheLocation)) {
+ return false;
+ }
+
+ mShell.getDisk("/").mount(new String[]{"ro"});
+
+ } else {
+ cacheLocation = diskStat.location();
+ }
+ }
+ }
+
+ if (getFile(cacheLocation + "/recovery").createDirectory()) {
+ if (getFile(cacheLocation + "/recovery/command").write("--update_package=" + getResolvedPath())) {
+ if (args != null && args.length > 0) {
+ String[] lines = new String[ args.length ];
+
+ for (int i=0; i < args.length; i++) {
+ lines[i] = "argument" + (i+1) + "=" + args[i];
+ }
+
+ if (!getFile(cacheLocation + "/recovery/rootfw.prop").write(lines)) {
+ getFile(cacheLocation + "/recovery/command").remove();
+
+ return false;
+ }
+ }
+
+ if (mShell.getDevice().rebootRecovery(context)) {
+ return true;
+ }
+
+ getFile(cacheLocation + "/recovery/command").remove();
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Extract data from an Android Assets Path (files located in /assets/) and add it to the current file location.
+ * If the file already exist, it will be overwritten. Otherwise the file will be created.
+ *
+ * @param context
+ * An android Context object
+ *
+ * @param asset
+ * The assets path
+ *
+ * @return
+ * True
on success, False
otherwise
+ */
+ public Boolean extractResource(Context context, String asset) {
+ try {
+ InputStream input = context.getAssets().open(asset);
+ Boolean status = extractResource(input);
+ input.close();
+
+ return status;
+
+ } catch(Throwable e) { return false; }
+ }
+
+ /**
+ * Extract data from an Android resource id (files located in /res/) and add it to the current file location.
+ * If the file already exist, it will be overwritten. Otherwise the file will be created.
+ *
+ * @param context
+ * An android Context object
+ *
+ * @param resourceid
+ * The InputStream to read from
+ *
+ * @return
+ * True
on success, False
otherwise
+ */
+ public Boolean extractResource(Context context, Integer resourceid) {
+ try {
+ InputStream input = context.getResources().openRawResource(resourceid);
+ Boolean status = extractResource(input);
+ input.close();
+
+ return status;
+
+ } catch(Throwable e) { return false; }
+ }
+
+ /**
+ * Extract data from an InputStream and add it to the current file location.
+ * If the file already exist, it will be overwritten. Otherwise the file will be created.
+ *
+ * @param resource
+ * The InputStream to read from
+ *
+ * @return
+ * True
on success, False
otherwise
+ */
+ public Boolean extractResource(InputStream resource) {
+ synchronized(mLock) {
+ if (!isDirectory()) {
+ try {
+ FileWriter writer = getFileWriter();
+
+ if (writer != null) {
+ byte[] buffer = new byte[1024];
+ int loc = 0;
+
+ while ((loc = resource.read(buffer)) > 0) {
+ writer.write(buffer, 0, loc);
+ }
+
+ writer.close();
+ }
+
+ } catch (Throwable e) {
+ Log.e(TAG, e.getMessage(), e);
+ }
+ }
+
+ return false;
+ }
+ }
+
+ /**
+ * Get a {@link com.spazedog.lib.rootfw4.utils.io.FileWriter} pointing at this file
+ */
+ public FileWriter getFileWriter() {
+ if (isFile()) {
+ try {
+ return new FileWriter(mShell, getAbsolutePath(), false);
+
+ } catch (Throwable e) {
+ Log.e(TAG, e.getMessage(), e);
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Get a {@link com.spazedog.lib.rootfw4.utils.io.FileReader} pointing at this file
+ */
+ public FileReader getFileReader() {
+ if (isFile()) {
+ try {
+ return new FileReader(mShell, getAbsolutePath());
+
+ } catch (Throwable e) {
+ Log.e(TAG, e.getMessage(), e);
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * @return
+ * True
if the file exists, False
otherwise
+ */
+ public Boolean exists() {
+ synchronized(mLock) {
+ if (mExistsLevel < 0) {
+ mExistsLevel = 0;
+
+ /*
+ * We cannot trust a false value, since restricted files will return false.
+ * But we can trust a true value, so we only do a shell check on false return.
+ */
+ if (!mFile.exists()) {
+ Attempts attempts = mShell.createAttempts("( %binary test -e '" + getAbsolutePath() + "' && echo true ) || ( %binary test ! -e '" + getAbsolutePath() + "' && echo false )");
+ Result result = attempts.execute();
+
+ if (result != null && result.wasSuccessful()) {
+ mExistsLevel = "true".equals(result.getLine()) ? 1 : 0;
+
+ } else {
+ /*
+ * Some toolsbox version does not have the 'test' command.
+ * Instead we try 'ls' on the file and check for errors, not pretty but affective.
+ */
+ result = mShell.createAttempts("ls '" + getAbsolutePath() + "' > /dev/null 2>&1").execute();
+
+ if (result != null && result.wasSuccessful()) {
+ mExistsLevel = 1;
+ }
+ }
+
+ } else {
+ mExistsLevel = 1;
+ }
+ }
+
+ return mExistsLevel > 0;
+ }
+ }
+
+ /**
+ * @return
+ * True
if the file exists and if it is an folder, False
otherwise
+ */
+ public Boolean isDirectory() {
+ synchronized (mLock) {
+ if (mFolderLevel < 0) {
+ mFolderLevel = 0;
+
+ if (exists()) {
+ /*
+ * If it exists, but is neither a file nor a directory, then we better do a shell check
+ */
+ if (!mFile.isDirectory() && !mFile.isFile()) {
+ Attempts attempts = mShell.createAttempts("( %binary test -d '" + getAbsolutePath() + "' && echo true ) || ( %binary test ! -d '" + getAbsolutePath() + "' && echo false )");
+ Result result = attempts.execute();
+
+ if (result != null && result.wasSuccessful()) {
+ mFolderLevel = "true".equals(result.getLine()) ? 1 : 0;
+
+ } else {
+ /*
+ * A few toolbox versions does not include the 'test' command
+ */
+ FileStat stat = getCanonicalFile().getDetails();
+
+ if (stat != null) {
+ mFolderLevel = "d".equals(stat.type()) ? 1 : 0;
+ }
+ }
+
+ } else {
+ mFolderLevel = mFile.isDirectory() ? 1 : 0;
+ }
+ }
+ }
+
+ return mFolderLevel > 0;
+ }
+ }
+
+ /**
+ * @return
+ * True
if the file is a link, False
otherwise
+ */
+ public Boolean isLink() {
+ synchronized (mLock) {
+ if (mLinkLevel < 0) {
+ mLinkLevel = 0;
+
+ if (exists()) {
+ Attempts attempts = mShell.createAttempts("( %binary test -L '" + getAbsolutePath() + "' && echo true ) || ( %binary test ! -L '" + getAbsolutePath() + "' && echo false )");
+ Result result = attempts.execute();
+
+ if (result != null && result.wasSuccessful()) {
+ mLinkLevel = "true".equals(result.getLine()) ? 1 : 0;
+
+ } else {
+ /*
+ * A few toolbox versions does not include the 'test' command
+ */
+ FileStat stat = getDetails();
+
+ if (stat != null) {
+ mLinkLevel = "l".equals(stat.type()) ? 1 : 0;
+ }
+ }
+ }
+ }
+
+ return mLinkLevel > 0;
+ }
+ }
+
+
+ /**
+ * @return
+ * True
if the item exists and if it is an file, False
otherwise
+ */
+ public Boolean isFile() {
+ synchronized (mLock) {
+ return exists() && !isDirectory();
+ }
+ }
+
+ /**
+ * Returns the absolute path. An absolute path is a path that starts at a root of the file system.
+ *
+ * @return
+ * The absolute path
+ */
+ public String getAbsolutePath() {
+ return mFile.getAbsolutePath();
+ }
+
+ /**
+ * Returns the path used to create this object.
+ *
+ * @return
+ * The parsed path
+ */
+ public String getPath() {
+ return mFile.getPath();
+ }
+
+ /**
+ * Returns the parent path. Note that on folders, this means the parent folder.
+ * However, on files, it will return the folder path that the file resides in.
+ *
+ * @return
+ * The parent path
+ */
+ public String getParentPath() {
+ return mFile.getParent();
+ }
+
+ /**
+ * Get a real absolute path.
+ *
+ * Java's getAbsolutePath
is not a fully resolved path. Something like ./file
could be returned as /folder/folder2/.././file
or simular.
+ * This method however will resolve a path and return a fully absolute path /folder/file
. It is a bit slower, so only use it when this is a must.
+ */
+ public String getResolvedPath() {
+ synchronized (mLock) {
+ String path = getAbsolutePath();
+
+ if (path.contains(".")) {
+ String[] directories = ("/".equals(path) ? path : path.endsWith("/") ? path.substring(1, path.length() - 1) : path.substring(1)).split("/");
+ List resolved = new ArrayList();
+
+ for (int i=0; i < directories.length; i++) {
+ if (directories[i].equals("..")) {
+ if (resolved.size() > 0) {
+ resolved.remove( resolved.size()-1 );
+ }
+
+ } else if (!directories[i].equals(".")) {
+ resolved.add(directories[i]);
+ }
+ }
+
+ path = resolved.size() > 0 ? "/" + TextUtils.join("/", resolved) : "/";
+ }
+
+ return path;
+ }
+ }
+
+ /**
+ * Get the canonical path of this file or folder.
+ * This means that if this is a link, you will get the path to the target, no matter how many links are in between.
+ * It also means that things like /folder1/../folder2
will be resolved to /folder2
.
+ *
+ * @return
+ * The canonical path
+ */
+ public String getCanonicalPath() {
+ synchronized (mLock) {
+ if (exists()) {
+ try {
+ /*
+ * First let's try using the native tools
+ */
+ String canonical = mFile.getCanonicalPath();
+
+ if (canonical != null) {
+ return canonical;
+ }
+
+ } catch(Throwable e) {}
+
+ /*
+ * Second we try using readlink, if the first failed.
+ */
+ Result result = mShell.createAttempts("readlink -f '" + getAbsolutePath() + "' 2> /dev/null").execute();
+
+ if (result.wasSuccessful()) {
+ return result.getLine();
+
+ } else {
+ /*
+ * And third we fallback to a slower but affective method
+ */
+ FileStat stat = getDetails();
+
+ if (stat != null && stat.link() != null) {
+ String realPath = stat.link();
+
+ while ((stat = getFile(realPath).getDetails()) != null && stat.link() != null) {
+ realPath = stat.link();
+ }
+
+ return realPath;
+ }
+
+ return getAbsolutePath();
+ }
+ }
+
+ return null;
+ }
+ }
+
+ /**
+ * Open a new {@link com.spazedog.lib.rootfw4.utils.File} object pointed at another file.
+ *
+ * @param fileName
+ * The file to point at
+ *
+ * @return
+ * A new instance of this class representing another file
+ */
+ public File getFile(String file) {
+ return new File(mShell, file);
+ }
+
+ /**
+ * Open a new {@link com.spazedog.lib.rootfw4.utils.File} object with the parent of this file.
+ *
+ * @return
+ * A new instance of this class representing the parent directory
+ */
+ public File getParentFile() {
+ return new File(mShell, getParentPath());
+ }
+
+ /**
+ * If this is a link, this method will return a new {@link com.spazedog.lib.rootfw4.utils.File} object with the real path attached.
+ *
+ * @return
+ * A new instance of this class representing the real path of a possible link
+ */
+ public File getCanonicalFile() {
+ return new File(mShell, getCanonicalPath());
+ }
+
+ /**
+ * @return
+ * The name of the file
+ */
+ public String getName() {
+ return mFile.getName();
+ }
+}
diff --git a/libraries/sharedCode/src/main/java/com/spazedog/lib/rootfw4/utils/Filesystem.java b/libraries/sharedCode/src/main/java/com/spazedog/lib/rootfw4/utils/Filesystem.java
new file mode 100644
index 0000000..4807ea3
--- /dev/null
+++ b/libraries/sharedCode/src/main/java/com/spazedog/lib/rootfw4/utils/Filesystem.java
@@ -0,0 +1,693 @@
+/*
+ * Copyright (C) 2014 Vlad Mihalachi
+ *
+ * This file is part of Turbo Editor.
+ *
+ * Turbo Editor is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Turbo Editor is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.spazedog.lib.rootfw4.utils;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+import java.util.regex.Pattern;
+
+import android.text.TextUtils;
+
+import com.spazedog.lib.rootfw4.Common;
+import com.spazedog.lib.rootfw4.Shell;
+import com.spazedog.lib.rootfw4.Shell.Result;
+import com.spazedog.lib.rootfw4.containers.BasicContainer;
+import com.spazedog.lib.rootfw4.utils.File.FileData;
+
+public class Filesystem {
+ public static final String TAG = Common.TAG + ".Filesystem";
+
+ protected final static Pattern oPatternSpaceSearch = Pattern.compile("[ \t]+");
+ protected final static Pattern oPatternSeparatorSearch = Pattern.compile(",");
+ protected final static Pattern oPatternPrefixSearch = Pattern.compile("^.*[A-Za-z]$");
+
+ protected static MountStat[] oFstabList;
+ protected static final Object oFstabLock = new Object();
+
+ protected Shell mShell;
+ protected Object mLock = new Object();
+
+ /**
+ * This is a container used to store disk information.
+ */
+ public static class DiskStat extends BasicContainer {
+ private String mDevice;
+ private String mLocation;
+ private Long mSize;
+ private Long mUsage;
+ private Long mAvailable;
+ private Integer mPercentage;
+
+ /**
+ * @return
+ * Device path
+ */
+ public String device() {
+ return mDevice;
+ }
+
+ /**
+ * @return
+ * Mount location
+ */
+ public String location() {
+ return mLocation;
+ }
+
+ /**
+ * @return
+ * Disk size in bytes
+ */
+ public Long size() {
+ return mSize;
+ }
+
+ /**
+ * @return
+ * Disk usage size in bytes
+ */
+ public Long usage() {
+ return mUsage;
+ }
+
+ /**
+ * @return
+ * Disk available size in bytes
+ */
+ public Long available() {
+ return mAvailable;
+ }
+
+ /**
+ * @return
+ * Disk usage percentage
+ */
+ public Integer percentage() {
+ return mPercentage;
+ }
+ }
+
+ /**
+ * This is a container used to store mount information.
+ */
+ public static class MountStat extends BasicContainer {
+ private String mDevice;
+ private String mLocation;
+ private String mFstype;
+ private String[] mOptions;
+
+ /**
+ * @return
+ * The device path
+ */
+ public String device() {
+ return mDevice;
+ }
+
+ /**
+ * @return
+ * The mount location
+ */
+ public String location() {
+ return mLocation;
+ }
+
+ /**
+ * @return
+ * The device file system type
+ */
+ public String fstype() {
+ return mFstype;
+ }
+
+ /**
+ * @return
+ * The options used at mount time
+ */
+ public String[] options() {
+ return mOptions;
+ }
+ }
+
+ public Filesystem(Shell shell) {
+ mShell = shell;
+ }
+
+ /**
+ * Just like {@link #getMountList} this will provide a list of mount points and disks. The difference is that this list will not be of
+ * all the currently mounted partitions, but a list of all defined mount point in each fstab and init.*.rc file on the device.
+ *
+ * It can be useful in situations where a file system might have been moved by a script, and you need the original defined location.
+ * Or perhaps you need the original device of a specific mount location.
+ *
+ * @return
+ * An array of {@link com.spazedog.lib.rootfw4.utils.Filesystem.MountStat} objects
+ */
+ public MountStat[] getFsList() {
+ synchronized(oFstabLock) {
+ if (oFstabList == null) {
+ Result result = mShell.execute("for DIR in /fstab.* /fstab /init.*.rc /init.rc; do echo $DIR; done");
+
+ if (result != null && result.wasSuccessful()) {
+ Set cache = new HashSet();
+ List list = new ArrayList();
+ String[] dirs = result.trim().getArray();
+
+ for (int i=0; i < dirs.length; i++) {
+ if (!Common.isEmulator() && dirs[i].contains("goldfish")) {
+ continue;
+ }
+
+ Boolean isFstab = dirs[i].contains("fstab");
+ FileData data = mShell.getFile(dirs[i]).readMatch( (isFstab ? "/dev/" : "mount "), false );
+
+ if (data != null) {
+ String[] lines = data.assort("#").getArray();
+
+ if (lines != null) {
+ for (int x=0; x < lines.length; x++) {
+ try {
+ String[] parts = oPatternSpaceSearch.split(lines[x].trim(), 5);
+ String options = isFstab || parts.length > 4 ? parts[ isFstab ? 3 : 4 ].replaceAll(",", " ") : "";
+
+ if (parts.length > 3 && !cache.contains(parts[ isFstab ? 1 : 3 ])) {
+ if (!isFstab && parts[2].contains("mtd@")) {
+
+ FileData mtd = mShell.getFile("/proc/mtd").readMatch( ("\"" + parts[2].substring(4) + "\""), false );
+
+ if (mtd != null && mtd.size() > 0) {
+ parts[2] = "/dev/block/mtdblock" + mtd.getLine().substring(3, mtd.getLine().indexOf(":"));
+ }
+
+ } else if (!isFstab && parts[2].contains("loop@")) {
+ parts[2] = parts[2].substring(5);
+ options += " loop";
+ }
+
+ MountStat stat = new MountStat();
+
+ stat.mDevice = parts[ isFstab ? 0 : 2 ];
+ stat.mFstype = parts[ isFstab ? 2 : 1 ];
+ stat.mLocation = parts[ isFstab ? 1 : 3 ];
+ stat.mOptions = oPatternSpaceSearch.split(options);
+
+ list.add(stat);
+ cache.add(parts[ isFstab ? 1 : 3 ]);
+ }
+
+ } catch(Throwable e) {}
+ }
+ }
+ }
+ }
+
+ oFstabList = list.toArray( new MountStat[ list.size() ] );
+ }
+ }
+
+ return oFstabList;
+ }
+ }
+
+ /**
+ * This will return a list of all currently mounted file systems, with information like
+ * device path, mount location, file system type and mount options.
+ *
+ * @return
+ * An array of {@link com.spazedog.lib.rootfw4.utils.Filesystem.MountStat} objects
+ */
+ public MountStat[] getMountList() {
+ FileData data = mShell.getFile("/proc/mounts").read();
+
+ if (data != null) {
+ String[] lines = data.trim().getArray();
+ MountStat[] list = new MountStat[ lines.length ];
+
+ for (int i=0; i < lines.length; i++) {
+ try {
+ String[] parts = oPatternSpaceSearch.split(lines[i].trim());
+
+ list[i] = new MountStat();
+ list[i].mDevice = parts[0];
+ list[i].mFstype = parts[2];
+ list[i].mLocation = parts[1];
+ list[i].mOptions = oPatternSeparatorSearch.split(parts[3]);
+
+ } catch(Throwable e) {}
+ }
+
+ return list;
+ }
+
+ return null;
+ }
+
+ /**
+ * Get an instance of the {@link com.spazedog.lib.rootfw4.utils.Filesystem.Disk} class.
+ *
+ * @param disk
+ * The location to the disk, partition or folder
+ */
+ public Disk getDisk(String disk) {
+ return new Disk(mShell, disk);
+ }
+
+ public static class Disk extends Filesystem {
+
+ protected File mFile;
+
+ public Disk(Shell shell, String disk) {
+ super(shell);
+
+ mFile = shell.getFile(disk);
+ }
+
+ /**
+ * This is a short method for adding additional options to a mount location or device.
+ * For an example, parsing remount instructions.
+ *
+ * @see #mount(String, String, String[])
+ *
+ * @param options
+ * A string array containing all of the mount options to parse
+ *
+ * @return
+ * True
on success, False
otherwise
+ */
+ public Boolean mount(String[] options) {
+ return mount(null, null, options);
+ }
+
+ /**
+ * This is a short method for attaching a device or folder to a location, without any options or file system type specifics.
+ *
+ * @see #mount(String, String, String[])
+ *
+ * @param location
+ * The location where the device or folder should be attached to
+ *
+ * @return
+ * True
on success, False
otherwise
+ */
+ public Boolean mount(String location) {
+ return mount(location, null, null);
+ }
+
+ /**
+ * This is a short method for attaching a device or folder to a location, without any file system type specifics.
+ *
+ * @see #mount(String, String, String[])
+ *
+ * @param location
+ * The location where the device or folder should be attached to
+ *
+ * @param options
+ * A string array containing all of the mount options to parse
+ *
+ * @return
+ * True
on success, False
otherwise
+ */
+ public Boolean mount(String location, String[] options) {
+ return mount(location, null, options);
+ }
+
+ /**
+ * This is a short method for attaching a device or folder to a location, without any options.
+ *
+ * @see #mount(String, String, String[])
+ *
+ * @param location
+ * The location where the device or folder should be attached to
+ *
+ * @param type
+ * The file system type to mount a device as
+ *
+ * @return
+ * True
on success, False
otherwise
+ */
+ public Boolean mount(String location, String type) {
+ return mount(location, type, null);
+ }
+
+ /**
+ * This is used for attaching a device or folder to a location,
+ * or to change any mount options on a current mounted file system.
+ *
+ * Note that if the device parsed to the constructor {@link #Disk(Shell, String)}
+ * is a folder, this method will use the --bind
option to attach it to the location. Also note that when attaching folders to a location,
+ * the type
and options
arguments will not be used and should just be parsed as NULL
.
+ *
+ * @param location
+ * The location where the device or folder should be attached to
+ *
+ * @param type
+ * The file system type to mount a device as
+ *
+ * @param options
+ * A string array containing all of the mount options to parse
+ *
+ * @return
+ * True
on success, False
otherwise
+ */
+ public Boolean mount(String location, String type, String[] options) {
+ String cmd = location != null && mFile.isDirectory() ?
+ "mount --bind '" + mFile.getAbsolutePath() + "' '" + location + "'" :
+ "mount" + (type != null ? " -t '" + type + "'" : "") + (options != null ? " -o '" + (location == null ? "remount," : "") + TextUtils.join(",", Arrays.asList(options)) + "'" : "") + " '" + mFile.getAbsolutePath() + "'" + (location != null ? " '" + location + "'" : "");
+
+ /*
+ * On some devices, some partitions has been made read-only by writing to the block device ioctls.
+ * This means that even mounting them as read/write will not work by itself, we need to change the ioctls as well.
+ */
+ if (options != null && !"/".equals(mFile.getAbsolutePath())) {
+ for (String option : options) {
+ if ("rw".equals(option)) {
+ String blockdevice = null;
+
+ if (mFile.isDirectory()) {
+ MountStat stat = getMountDetails();
+
+ if (stat != null) {
+ blockdevice = stat.device();
+
+ } else if ((stat = getFsDetails()) != null) {
+ blockdevice = stat.device();
+ }
+
+ } else {
+ blockdevice = mFile.getAbsolutePath();
+ }
+
+ if (blockdevice != null && blockdevice.startsWith("/dev/")) {
+ mShell.createAttempts("blockdev --setrw '" + blockdevice + "' 2> /dev/null").execute();
+ }
+
+ break;
+ }
+ }
+ }
+
+ Result result = mShell.createAttempts(cmd).execute();
+
+ return result != null && result.wasSuccessful();
+ }
+
+ /**
+ * This method is used to remove an attachment of a device or folder (unmount).
+ *
+ * @return
+ * True
on success, False
otherwise
+ */
+ public Boolean unmount() {
+ String[] commands = new String[]{"umount '" + mFile.getAbsolutePath() + "'", "umount -f '" + mFile.getAbsolutePath() + "'"};
+
+ for (String command : commands) {
+ Result result = mShell.createAttempts(command).execute();
+
+ if (result != null && result.wasSuccessful()) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * This method is used to move a mount location to another location.
+ *
+ * @return
+ * True
on success, False
otherwise
+ */
+ public Boolean move(String destination) {
+ Result result = mShell.createAttempts("mount --move '" + mFile.getAbsolutePath() + "' '" + destination + "'").execute();
+
+ if (result == null || !result.wasSuccessful()) {
+ /*
+ * Not all toolbox versions support moving mount points.
+ * So in these cases, we fallback to a manual unmount/remount.
+ */
+ MountStat stat = getMountDetails();
+
+ if (stat != null && unmount()) {
+ return getDisk(stat.device()).mount(stat.location(), stat.fstype(), stat.options());
+ }
+ }
+
+ return result != null && result.wasSuccessful();
+ }
+
+ /**
+ * This is used to check whether the current device or folder is attached to a location (Mounted).
+ *
+ * @return
+ * True
if mounted, False
otherwise
+ */
+ public Boolean isMounted() {
+ return getMountDetails() != null;
+ }
+
+ /**
+ * This is used to check if a mounted file system was mounted with a specific mount option.
+ * Note that options like mode=xxxx
can also be checked by just parsing mode
as the argument.
+ *
+ * @param option
+ * The name of the option to find
+ *
+ * @return
+ * True
if the options was used to attach the device, False
otherwise
+ */
+ public Boolean hasOption(String option) {
+ MountStat stat = getMountDetails();
+
+ if (stat != null) {
+ String[] options = stat.options();
+
+ if (options != null && options.length > 0) {
+ for (int i=0; i < options.length; i++) {
+ if (options[i].equals(option) || options[i].startsWith(option + "=")) {
+ return true;
+ }
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * This can be used to get the value of a specific mount option that was used to attach the file system.
+ * Note that options like noexec
, nosuid
and nodev
does not have any values and will return NULL
.
+ * This method is used to get values from options like gid=xxxx
, mode=xxxx
and size=xxxx
where xxxx
is the value.
+ *
+ * @param option
+ * The name of the option to find
+ *
+ * @return
+ * True
if the options was used to attach the device, False
otherwise
+ */
+ public String getOption(String option) {
+ MountStat stat = getMountDetails();
+
+ if (stat != null) {
+ String[] options = stat.options();
+
+ if (options != null && options.length > 0) {
+ for (int i=0; i < options.length; i++) {
+ if (options[i].startsWith(option + "=")) {
+ return options[i].substring( options[i].indexOf("=")+1 );
+ }
+ }
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * This is the same as {@link #getMountList()},
+ * only this method will just return the mount information for this specific device or mount location.
+ *
+ * @return
+ * A single {@link com.spazedog.lib.rootfw4.utils.Filesystem.MountStat} object
+ */
+ public MountStat getMountDetails() {
+ MountStat[] list = getMountList();
+
+ if (list != null) {
+ String path = mFile.getAbsolutePath();
+
+ if (!mFile.isDirectory()) {
+ for (int i=0; i < list.length; i++) {
+ if (list[i].device().equals(path)) {
+ return list[i];
+ }
+ }
+
+ } else {
+ do {
+ for (int i=0; i < list.length; i++) {
+ if (list[i].location().equals(path)) {
+ return list[i];
+ }
+ }
+
+ } while (path.lastIndexOf("/") > 0 && !(path = path.substring(0, path.lastIndexOf("/"))).equals(""));
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * This is the same as {@link #getFsList()},
+ * only this method will just return the mount information for this specific device or mount location.
+ *
+ * @return
+ * A single {@link com.spazedog.lib.rootfw4.utils.Filesystem.MountStat} object
+ */
+ public MountStat getFsDetails() {
+ MountStat[] list = getFsList();
+
+ if (list != null) {
+ String path = mFile.getAbsolutePath();
+
+ if (!mFile.isDirectory()) {
+ for (int i=0; i < list.length; i++) {
+ if (list[i].device().equals(path)) {
+ return list[i];
+ }
+ }
+
+ } else {
+ do {
+ for (int i=0; i < list.length; i++) {
+ if (list[i].location().equals(path)) {
+ return list[i];
+ }
+ }
+
+ } while (path.lastIndexOf("/") > 0 && !(path = path.substring(0, path.lastIndexOf("/"))).equals(""));
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Like with {@link #getMountList()}, this will also return information like device path and mount location.
+ * However, it will not return information like file system type or mount options, but instead
+ * information about the disk size, remaining bytes, used bytes and usage percentage.
+ *
+ * @return
+ * A single {@link com.spazedog.lib.rootfw4.utils.Filesystem.DiskStat} object
+ */
+ public DiskStat getDiskDetails() {
+ String[] commands = new String[]{"df -k '" + mFile.getAbsolutePath() + "'", "df '" + mFile.getAbsolutePath() + "'"};
+
+ for (String command : commands) {
+ Result result = mShell.createAttempts(command).execute();
+
+ if (result != null && result.wasSuccessful() && result.size() > 1) {
+ /* Depending on how long the line is, the df command some times breaks a line into two */
+ String[] parts = oPatternSpaceSearch.split(result.sort(1).trim().getString(" ").trim());
+
+ /*
+ * Any 'df' output, no mater which toolbox or busybox version, should contain at least
+ * 'device or mount location', 'size', 'used' and 'available'
+ */
+ if (parts.length > 3) {
+ String pDevice=null, pLocation=null, prefix, prefixList[] = {"k", "m", "g", "t"};
+ Integer pPercentage=null;
+ Long pUsage, pSize, pRemaining;
+ Double[] pUsageSections = new Double[3];
+
+ if (parts.length > 5) {
+ /* Busybox output */
+
+ pDevice = parts[0];
+ pLocation = parts[5];
+ pPercentage = Integer.parseInt(parts[4].substring(0, parts[4].length()-1));
+
+ } else {
+ /* Toolbox output */
+
+ /* Depending on Toolbox version, index 0 can be both the device or the mount location */
+ MountStat stat = getMountDetails();
+
+ if (stat != null) {
+ pDevice = stat.device();
+ pLocation = stat.location();
+ }
+ }
+
+ /* Make sure that the sizes of usage, capacity etc does not have a prefix. Not all toolbox and busybox versions supports the '-k' argument,
+ * and some does not produce an error when parsed */
+ for (int i=1; i < 4; i++) {
+ if (i < parts.length) {
+ if (oPatternPrefixSearch.matcher(parts[i]).matches()) {
+ pUsageSections[i-1] = Double.parseDouble( parts[i].substring(0, parts[i].length()-1) );
+ prefix = parts[i].substring(parts[i].length()-1).toLowerCase(Locale.US);
+
+ for (int x=0; x < prefixList.length; x++) {
+ pUsageSections[i-1] = pUsageSections[i-1] * 1024D;
+
+ if (prefixList[x].equals(prefix)) {
+ break;
+ }
+ }
+
+ } else {
+ pUsageSections[i-1] = Double.parseDouble(parts[i]) * 1024D;
+ }
+
+ } else {
+ pUsageSections[i-1] = 0D;
+ }
+ }
+
+ pSize = pUsageSections[0].longValue();
+ pUsage = pUsageSections[1].longValue();
+ pRemaining = pUsageSections[2].longValue();
+
+ if (pPercentage == null) {
+ /* You cannot divide by zero */
+ pPercentage = pSize != 0 ? ((Long) ((pUsage * 100L) / pSize)).intValue() : 0;
+ }
+
+ DiskStat info = new DiskStat();
+ info.mDevice = pDevice;
+ info.mLocation = pLocation;
+ info.mSize = pSize;
+ info.mUsage = pUsage;
+ info.mAvailable = pRemaining;
+ info.mPercentage = pPercentage;
+
+ return info;
+ }
+ }
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/libraries/sharedCode/src/main/java/com/spazedog/lib/rootfw4/utils/Memory.java b/libraries/sharedCode/src/main/java/com/spazedog/lib/rootfw4/utils/Memory.java
new file mode 100644
index 0000000..06667cd
--- /dev/null
+++ b/libraries/sharedCode/src/main/java/com/spazedog/lib/rootfw4/utils/Memory.java
@@ -0,0 +1,686 @@
+/*
+ * Copyright (C) 2014 Vlad Mihalachi
+ *
+ * This file is part of Turbo Editor.
+ *
+ * Turbo Editor is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Turbo Editor is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.spazedog.lib.rootfw4.utils;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Pattern;
+
+import com.spazedog.lib.rootfw4.Common;
+import com.spazedog.lib.rootfw4.Shell;
+import com.spazedog.lib.rootfw4.Shell.Result;
+import com.spazedog.lib.rootfw4.containers.BasicContainer;
+import com.spazedog.lib.rootfw4.utils.File.FileData;
+
+/**
+ * This class is used to get information about the device memory.
+ * It can provide information about total memory and swap space, free memory and swap etc.
+ * It can also be used to check the device support for CompCache/ZRam and Swap along with
+ * providing a list of active Swap and CompCache/ZRam devices.
+ */
+public class Memory {
+ public static final String TAG = Common.TAG + ".Memory";
+
+ protected final static Pattern oPatternSpaceSearch = Pattern.compile("[ \t]+");
+
+ protected static Boolean oCompCacheSupport;
+ protected static Boolean oSwapSupport;
+
+ protected Shell mShell;
+
+ /**
+ * This is a container which is used to store information about a SWAP device
+ */
+ public static class SwapStat extends BasicContainer {
+ private String mDevice;
+ private Long mSize;
+ private Long mUsage;
+
+ /**
+ * @return
+ * Path to the SWAP device
+ */
+ public String device() {
+ return mDevice;
+ }
+
+ /**
+ * @return
+ * SWAP size in bytes
+ */
+ public Long size() {
+ return mSize;
+ }
+
+ /**
+ * @return
+ * SWAP usage in bytes
+ */
+ public Long usage() {
+ return mUsage;
+ }
+ }
+
+ /**
+ * This container is used to store all memory information from /proc/meminfo.
+ */
+ public static class MemStat extends BasicContainer {
+ private Long mMemTotal = 0L;
+ private Long mMemFree = 0L;
+ private Long mMemCached = 0L;
+ private Long mSwapTotal = 0L;
+ private Long mSwapFree = 0L;
+ private Long mSwapCached = 0L;
+
+ /**
+ * @return
+ * Total amount of memory in bytes, including SWAP space
+ */
+ public Long total() {
+ return mMemTotal + mSwapTotal;
+ }
+
+ /**
+ * @return
+ * Free amount of memory in bytes, including SWAP space and cached memory
+ */
+ public Long free() {
+ return mMemFree + mSwapFree + (mMemCached + mSwapCached);
+ }
+
+ /**
+ * @return
+ * Amount of cached memory including SWAP space
+ */
+ public Long cached() {
+ return mMemCached + mSwapCached;
+ }
+
+ /**
+ * @return
+ * Amount of used memory including SWAP (Cached memory not included)
+ */
+ public Long usage() {
+ return total() - free();
+ }
+
+ /**
+ * @return
+ * Memory usage in percentage, including SWAP space (Cached memory not included)
+ */
+ public Integer percentage() {
+ return ((Long) ((usage() * 100L) / total())).intValue();
+ }
+
+ /**
+ * @return
+ * Total amount of memory in bytes
+ */
+ public Long memTotal() {
+ return mMemTotal;
+ }
+
+ /**
+ * @return
+ * Free amount of memory in bytes, including cached memory
+ */
+ public Long memFree() {
+ return mMemFree + mMemCached;
+ }
+
+ /**
+ * @return
+ * Amount of cached memory
+ */
+ public Long memCached() {
+ return mMemCached;
+ }
+
+ /**
+ * @return
+ * Amount of used memory (Cached memory not included)
+ */
+ public Long memUsage() {
+ return memTotal() - memFree();
+ }
+
+ /**
+ * @return
+ * Memory usage in percentage (Cached memory not included)
+ */
+ public Integer memPercentage() {
+ try {
+ return ((Long) ((memUsage() * 100L) / memTotal())).intValue();
+
+ } catch (Throwable e) {
+ return 0;
+ }
+ }
+
+ /**
+ * @return
+ * Total amount of SWAP space in bytes
+ */
+ public Long swapTotal() {
+ return mSwapTotal;
+ }
+
+ /**
+ * @return
+ * Free amount of SWAP space in bytes, including cached memory
+ */
+ public Long swapFree() {
+ return mSwapFree + mSwapCached;
+ }
+
+ /**
+ * @return
+ * Amount of cached SWAP space
+ */
+ public Long swapCached() {
+ return mSwapCached;
+ }
+
+ /**
+ * @return
+ * Amount of used SWAP space (Cached memory not included)
+ */
+ public Long swapUsage() {
+ return swapTotal() - swapFree();
+ }
+
+ /**
+ * @return
+ * SWAP space usage in percentage (Cached memory not included)
+ */
+ public Integer swapPercentage() {
+ try {
+ return ((Long) ((swapUsage() * 100L) / swapTotal())).intValue();
+
+ } catch (Throwable e) {
+ return 0;
+ }
+ }
+ }
+
+ public Memory(Shell shell) {
+ mShell = shell;
+ }
+
+ /**
+ * Get memory information like ram usage, ram total, cached memory, swap total etc.
+ */
+ public MemStat getUsage() {
+ FileData data = mShell.getFile("/proc/meminfo").read();
+
+ if (data != null && data.size() > 0) {
+ String[] lines = data.getArray();
+ MemStat stat = new MemStat();
+
+ for (int i=0; i < lines.length; i++) {
+ String[] parts = oPatternSpaceSearch.split(lines[i]);
+
+ if (parts[0].equals("MemTotal:")) {
+ stat.mMemTotal = Long.parseLong(parts[1]) * 1024L;
+
+ } else if (parts[0].equals("MemFree:")) {
+ stat.mMemFree = Long.parseLong(parts[1]) * 1024L;
+
+ } else if (parts[0].equals("Cached:")) {
+ stat.mMemCached = Long.parseLong(parts[1]) * 1024L;
+
+ } else if (parts[0].equals("SwapTotal:")) {
+ stat.mSwapTotal = Long.parseLong(parts[1]) * 1024L;
+
+ } else if (parts[0].equals("SwapFree:")) {
+ stat.mSwapFree = Long.parseLong(parts[1]) * 1024L;
+
+ } else if (parts[0].equals("SwapCached:")) {
+ stat.mSwapCached = Long.parseLong(parts[1]) * 1024L;
+
+ }
+ }
+
+ return stat;
+ }
+
+ return null;
+ }
+
+ /**
+ * Get a list of all active SWAP devices.
+ *
+ * @return
+ * An SwapStat array of all active SWAP devices
+ */
+ public SwapStat[] getSwapList() {
+ File file = mShell.getFile("/proc/swaps");
+
+ if (file.exists()) {
+ String[] data = file.readMatch("/dev/", false).trim().getArray();
+ List statList = new ArrayList();
+
+ if (data != null && data.length > 0) {
+ for (int i=0; i < data.length; i++) {
+ try {
+ String[] sections = oPatternSpaceSearch.split(data[i].trim());
+
+ SwapStat stat = new SwapStat();
+ stat.mDevice = sections[0];
+ stat.mSize = Long.parseLong(sections[2]) * 1024L;
+ stat.mUsage = Long.parseLong(sections[3]) * 1024L;
+
+ statList.add(stat);
+
+ } catch(Throwable e) {}
+ }
+
+ return statList.size() > 0 ? statList.toArray( new SwapStat[ statList.size() ] ) : null;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Check whether or not CompCache/ZRam is supported by the kernel.
+ * This also checks for Swap support. If this returns FALSE, then none of them is supported.
+ */
+ public Boolean hasCompCacheSupport() {
+ if (oCompCacheSupport == null) {
+ oCompCacheSupport = false;
+
+ if (hasSwapSupport()) {
+ String[] files = new String[]{"/dev/block/ramzswap0", "/dev/block/zram0", "/system/lib/modules/ramzswap.ko", "/system/lib/modules/zram.ko"};
+
+ for (String file : files) {
+ if (mShell.getFile(file).exists()) {
+ oCompCacheSupport = true; break;
+ }
+ }
+ }
+ }
+
+ return oCompCacheSupport;
+ }
+
+ /**
+ * Check whether or not Swap is supported by the kernel.
+ */
+ public Boolean hasSwapSupport() {
+ if (oSwapSupport == null) {
+ oSwapSupport = mShell.getFile("/proc/swaps").exists();
+ }
+
+ return oSwapSupport;
+ }
+
+ /**
+ * Get a new instance of {@link com.spazedog.lib.rootfw4.utils.Memory.Swap}
+ *
+ * @param device
+ * The Swap block device
+ */
+ public Swap getSwap(String device) {
+ return new Swap(mShell, device);
+ }
+
+ /**
+ * Get a new instance of {@link com.spazedog.lib.rootfw4.utils.Memory.CompCache}
+ */
+ public CompCache getCompCache() {
+ return new CompCache(mShell);
+ }
+
+ /**
+ * Change the swappiness level.
+ *
+ * The level should be between 0 for low swap usage and 100 for high swap usage.
+ *
+ * @param level
+ * The swappiness level
+ */
+ public Boolean setSwappiness(Integer level) {
+ Result result = null;
+
+ if (level >= 0 && level <= 100 && hasSwapSupport()) {
+ result = mShell.execute("echo '" + level + "' > /proc/sys/vm/swappiness");
+ }
+
+ return result != null && result.wasSuccessful();
+ }
+
+ /**
+ * Get the current swappiness level.
+ */
+ public Integer getSwappiness() {
+ if (hasSwapSupport()) {
+ String output = mShell.getFile("/proc/sys/vm/swappiness").readOneLine();
+
+ if (output != null) {
+ try {
+ return Integer.parseInt(output);
+
+ } catch (Throwable e) {}
+ }
+ }
+
+ return 0;
+ }
+
+ /**
+ * This class is an extension of the {@link com.spazedog.lib.rootfw4.utils.Memory} class.
+ * This can be used to get information about-, and handle swap and CompCache/ZRam devices.
+ */
+ public static class Swap extends Memory {
+
+ protected File mSwapDevice;
+
+ /**
+ * Create a new instance of this class.
+ *
+ * @param shell
+ * An instance of the {@link Shell} class
+ *
+ * @param device
+ * The swap/zram block device
+ */
+ public Swap(Shell shell, String device) {
+ super(shell);
+
+ if (device != null) {
+ mSwapDevice = mShell.getFile(device);
+
+ if (!mSwapDevice.getAbsolutePath().startsWith("/dev/")) {
+ mSwapDevice = null;
+ }
+ }
+ }
+
+ /**
+ * Get information like size and usage of a specific SWAP device. This method will return null if the device does not exist, or if it has not been activated.
+ *
+ * @param device
+ * The specific SWAP device path to get infomation about
+ *
+ * @return
+ * An SwapStat object containing information about the requested SWAP device
+ */
+ public SwapStat getSwapDetails() {
+ if (exists()) {
+ File file = mShell.getFile("/proc/swaps");
+
+ if (file.exists()) {
+ String data = file.readMatch(mSwapDevice.getAbsolutePath(), false).getLine();
+
+ if (data != null && data.length() > 0) {
+ try {
+ String[] sections = oPatternSpaceSearch.split(data);
+
+ SwapStat stat = new SwapStat();
+ stat.mDevice = sections[0];
+ stat.mSize = Long.parseLong(sections[2]) * 1024L;
+ stat.mUsage = Long.parseLong(sections[3]) * 1024L;
+
+ return stat;
+
+ } catch(Throwable e) {}
+ }
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Check whether or not this block device exists.
+ * This will also return FALSE if this is not a /dev/ device.
+ */
+ public Boolean exists() {
+ return mSwapDevice != null && mSwapDevice.exists();
+ }
+
+ /**
+ * Check whether or not this Swap device is currently active.
+ */
+ public Boolean isActive() {
+ return getSwapDetails() != null;
+ }
+
+ /**
+ * Get the path to the Swap block device.
+ */
+ public String getPath() {
+ return mSwapDevice != null ? mSwapDevice.getResolvedPath() : null;
+ }
+
+ /**
+ * @see #setSwapOn(Integer)
+ */
+ public Boolean setSwapOn() {
+ return setSwapOn(0);
+ }
+
+ /**
+ * Enable this Swap device.
+ *
+ * @param priority
+ * Priority (Highest number is used first) or use '0' for auto
+ */
+ public Boolean setSwapOn(Integer priority) {
+ if (exists()) {
+ Boolean status = isActive();
+
+ if (!status) {
+ String[] commands = null;
+
+ if (priority > 0) {
+ commands = new String[]{"swapon -p '" + priority + "' '" + mSwapDevice.getAbsolutePath() + "'", "swapon '" + mSwapDevice.getAbsolutePath() + "'"};
+
+ } else {
+ commands = new String[]{"swapon '" + mSwapDevice.getAbsolutePath() + "'"};
+ }
+
+ for (String command : commands) {
+ Result result = mShell.createAttempts(command).execute();
+
+ if (result != null && result.wasSuccessful()) {
+ return true;
+ }
+ }
+ }
+
+ return status;
+ }
+
+ return false;
+ }
+
+ /**
+ * Disable this Swap device.
+ */
+ public Boolean setSwapOff() {
+ if (exists()) {
+ Boolean status = isActive();
+
+ if (status) {
+ Result result = mShell.createAttempts("swapoff '" + mSwapDevice.getAbsolutePath() + "'").execute();
+
+ return result != null && result.wasSuccessful();
+ }
+
+ return status;
+ }
+
+ return true;
+ }
+ }
+
+ /**
+ * This is an extension of the {@link com.spazedog.lib.rootfw4.utils.Memory.Swap} class. It's job is more CompCache/ZRam orientated.
+ * Unlike it's parent, this class can not only switch a CompCache/ZRam device on and off, it can also
+ * locate the proper supported type and load it's kernel module if not already done during boot.
+ *
+ * It is advised to use this when working with CompCache/ZRam specifically.
+ */
+ public static class CompCache extends Swap {
+
+ protected static String oCachedDevice;
+
+ /**
+ * Create a new instance of this class.
+ *
+ * This constructor will automatically look for the CompCache/ZRam block device, and
+ * enable the feature (loading modules) if not already done by the kernel during boot.
+ *
+ * @param shell
+ * An instance of the {@link Shell} class
+ */
+ public CompCache(Shell shell) {
+ super(shell, oCachedDevice);
+
+ if (oCachedDevice == null) {
+ String[] blockDevices = new String[]{"/dev/block/ramzswap0", "/dev/block/zram0"};
+ String[] libraries = new String[]{"/system/lib/modules/ramzswap.ko", "/system/lib/modules/zram.ko"};
+
+ for (int i=0; i < blockDevices.length; i++) {
+ if (mShell.getFile(blockDevices[i]).exists()) {
+ oCachedDevice = blockDevices[i]; break;
+
+ } else if (mShell.getFile(libraries[i]).exists()) {
+ Result result = mShell.createAttempts("insmod '" + libraries[i] + "'").execute();
+
+ if (result != null && result.wasSuccessful()) {
+ oCachedDevice = blockDevices[i]; break;
+ }
+ }
+ }
+
+ if (oCachedDevice != null) {
+ mSwapDevice = mShell.getFile(oCachedDevice);
+ }
+ }
+ }
+
+ /**
+ * Enable this Swap device.
+ *
+ * This overwrites {@link com.spazedog.lib.rootfw4.utils.Memory.Swap#setSwapOn()} to enable to feature of
+ * setting a cache size for the CompCache/ZRam. This method sets the size to 18% of the total device memory.
+ *
+ * If you are sure that CompCache/ZRam is loaded and the device has been setup with size and swap partition and you don't want to change this,
+ * then use {@link com.spazedog.lib.rootfw4.utils.Memory.Swap#setSwapOn()} instead. But if nothing has been setup yet, it could fail as it does nothing else but try to activate the device as Swap.
+ *
+ * @see #setSwapOn(Integer)
+ */
+ @Override
+ public Boolean setSwapOn(Integer priority) {
+ return setSwapOn(priority, 18);
+ }
+
+ /**
+ * Enable this Swap device.
+ *
+ * The total CompCache/ZRam size will be the chosen percentage
+ * of the total device memory, although max 35%. If a greater value is chosen, 35 will be used.
+ *
+ * @param cacheSize
+ * The percentage value of the total device memory
+ */
+ public Boolean setSwapOn(Integer priority, Integer cacheSize) {
+ cacheSize = cacheSize > 35 ? 35 : (cacheSize <= 0 ? 18 : cacheSize);
+
+ if (exists()) {
+ Boolean status = isActive();
+
+ if (!status) {
+ Result result = null;
+ MemStat stat = getUsage();
+
+ if (stat != null) {
+ if (oCachedDevice.endsWith("/zram0")) {
+ result = mShell.createAttempts(
+ "echo 1 > /sys/block/zram0/reset && " +
+ "echo '" + ((stat.memTotal() * cacheSize) / 100) + "' > /sys/block/zram0/disksize && " +
+ "%binary mkswap '" + mSwapDevice.getAbsolutePath() + "'"
+
+ ).execute();
+
+ } else {
+ result = mShell.execute("rzscontrol '" + mSwapDevice.getAbsolutePath() + "' --disksize_kb='" + (((stat.memTotal() * cacheSize) / 100) * 1024) + "' --init");
+ }
+
+ if (result != null && result.wasSuccessful()) {
+ String[] commands = null;
+
+ if (priority > 0) {
+ commands = new String[]{"swapon -p '" + priority + "' '" + mSwapDevice.getAbsolutePath() + "'", "swapon '" + mSwapDevice.getAbsolutePath() + "'"};
+
+ } else {
+ commands = new String[]{"swapon '" + mSwapDevice.getAbsolutePath() + "'"};
+ }
+
+ for (String command : commands) {
+ result = mShell.createAttempts(command).execute();
+
+ if (result != null && result.wasSuccessful()) {
+ return true;
+ }
+ }
+ }
+ }
+ }
+
+ return status;
+ }
+
+ return false;
+ }
+
+ /**
+ * Disable this Swap device.
+ *
+ * This overwrites {@link com.spazedog.lib.rootfw4.utils.Memory.Swap#setSwapOff()} as this will also release the CompCache/ZRam from memory.
+ */
+ @Override
+ public Boolean setSwapOff() {
+ if (exists()) {
+ Boolean status = isActive();
+
+ if (status) {
+ Result result = null;
+
+ if (oCachedDevice.endsWith("/zram0")) {
+ result = mShell.createAttempts("swapoff '" + mSwapDevice.getAbsolutePath() + "' && echo 1 > /sys/block/zram0/reset").execute();
+
+ } else {
+ result = mShell.createAttempts("swapoff '" + mSwapDevice.getAbsolutePath() + "' && rzscontrol '" + mSwapDevice.getAbsolutePath() + "' --reset").execute();
+ }
+
+ return result != null && result.wasSuccessful();
+ }
+
+ return status;
+ }
+
+ return true;
+ }
+ }
+}
diff --git a/libraries/sharedCode/src/main/java/com/spazedog/lib/rootfw4/utils/io/FileReader.java b/libraries/sharedCode/src/main/java/com/spazedog/lib/rootfw4/utils/io/FileReader.java
new file mode 100644
index 0000000..32410ac
--- /dev/null
+++ b/libraries/sharedCode/src/main/java/com/spazedog/lib/rootfw4/utils/io/FileReader.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2014 Vlad Mihalachi
+ *
+ * This file is part of Turbo Editor.
+ *
+ * Turbo Editor is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Turbo Editor is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.spazedog.lib.rootfw4.utils.io;
+
+import com.spazedog.lib.rootfw4.Common;
+import com.spazedog.lib.rootfw4.Shell;
+import com.spazedog.lib.rootfw4.ShellStream;
+
+import java.io.DataOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.nio.CharBuffer;
+
+/**
+ * This class allows you to open a file as root, if needed.
+ * Files that are not protected will be handled by a regular {@link java.io.FileReader} while protected files
+ * will use a shell streamer instead. Both of which will act as a normal reader that can be used together with other classes like {@link java.io.BufferedReader} and such.
+ *
+ * Note that this should not be used for unending streams. This is only meant for regular files. If you need unending streams, like /dev/input/event*
,
+ * you should use {@link ShellStream} instead.
+ */
+public class FileReader extends Reader {
+ public static final String TAG = Common.TAG + ".FileReader";
+
+ protected InputStreamReader mStream;
+
+ /**
+ * Create a new {@link java.io.InputStreamReader}. However {@link com.spazedog.lib.rootfw4.utils.io.FileReader#FileReader(Shell, String)} is a better option.
+ */
+ public FileReader(String file) throws FileNotFoundException {
+ this(null, file);
+ }
+
+ /**
+ * Create a new {@link java.io.InputStreamReader}. If shell
is not NULL
, then
+ * the best match for cat
will be located whenever a SuperUser connection is needed. This will be the best
+ * option for multiple environments.
+ */
+ public FileReader(Shell shell, String file) throws FileNotFoundException {
+ String filePath = new File(file).getAbsolutePath();
+
+ try {
+ mStream = new InputStreamReader(new FileInputStream(filePath));
+
+ } catch (FileNotFoundException e) {
+ String binary = shell != null ? shell.findCommand("cat") : "toolbox cat";
+
+ try {
+ ProcessBuilder builder = new ProcessBuilder("su");
+ builder.redirectErrorStream(true);
+
+ Process process = builder.start();
+ mStream = new InputStreamReader(process.getInputStream());
+
+ DataOutputStream stdIn = new DataOutputStream(process.getOutputStream());
+ stdIn.write( (binary + " '" + filePath + "'\n").getBytes() );
+ stdIn.write( ("exit $?\n").getBytes() );
+ stdIn.flush();
+ stdIn.close();
+
+ Integer resultCode = process.waitFor();
+
+ if (!resultCode.equals(0)) {
+ throw new FileNotFoundException(e.getMessage());
+ }
+
+ } catch (Throwable te) {
+ throw new FileNotFoundException(te.getMessage());
+ }
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void mark(int readLimit) throws IOException {
+ mStream.mark(readLimit);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean markSupported() {
+ return mStream.markSupported();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void close() throws IOException {
+ mStream.close();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int read(char[] buffer, int offset, int count) throws IOException {
+ return mStream.read(buffer, offset, count);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int read(CharBuffer target) throws IOException {
+ return mStream.read(target);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int read(char[] buffer) throws IOException {
+ return mStream.read(buffer);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int read() throws IOException {
+ return mStream.read();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public long skip(long charCount) throws IOException {
+ return mStream.skip(charCount);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void reset() throws IOException {
+ mStream.reset();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean ready() throws IOException {
+ return mStream.ready();
+ }
+}
diff --git a/libraries/sharedCode/src/main/java/com/spazedog/lib/rootfw4/utils/io/FileWriter.java b/libraries/sharedCode/src/main/java/com/spazedog/lib/rootfw4/utils/io/FileWriter.java
new file mode 100644
index 0000000..bda8913
--- /dev/null
+++ b/libraries/sharedCode/src/main/java/com/spazedog/lib/rootfw4/utils/io/FileWriter.java
@@ -0,0 +1,150 @@
+/*
+ * This file is part of the RootFW Project: https://github.com/spazedog/rootfw
+ *
+ * Copyright (c) 2015 Daniel Bergløv
+ *
+ * RootFW is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+
+ * RootFW is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with RootFW. If not, see
+ */
+
+package com.spazedog.lib.rootfw4.utils.io;
+
+import java.io.DataOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.Writer;
+
+import android.os.Bundle;
+
+import com.spazedog.lib.rootfw4.Common;
+import com.spazedog.lib.rootfw4.Shell;
+
+/**
+ * This class is used to write to a file. Unlike {@link java.io.FileWriter}, this class
+ * will fallback on a SuperUser shell stream whenever a write action is not allowed by the application.
+ */
+public class FileWriter extends Writer {
+ public static final String TAG = Common.TAG + ".FileReader";
+
+ protected DataOutputStream mStream;
+ protected Process mProcess;
+
+ public FileWriter(String file) throws IOException {
+ this(null, file, false);
+ }
+
+ public FileWriter(String file, boolean append) throws IOException {
+ this(null, file, append);
+ }
+
+ public FileWriter(Shell shell, String file, boolean append) throws IOException {
+ super();
+
+ String filePath = new File(file).getAbsolutePath();
+
+ try {
+ mStream = new DataOutputStream(new FileOutputStream(filePath, append));
+
+ } catch (IOException e) {
+ String binary = shell != null ? shell.findCommand("cat") : "toolbox cat";
+
+ try {
+ mProcess = new ProcessBuilder("su").start();
+ mStream = new DataOutputStream(mProcess.getOutputStream());
+
+ mStream.write( (binary + (append ? " >> " : " > ") + "'" + filePath + "' || exit 1\n").getBytes() );
+ mStream.flush();
+
+ try {
+ synchronized(mStream) {
+ /*
+ * The only way to check for errors, is by giving the shell a bit of time to fail.
+ * This can either be an error caused by a missing binary for 'cat', or caused by something
+ * like writing to a read-only fileystem.
+ */
+ mStream.wait(100);
+ }
+
+ } catch (Throwable ignore) {}
+
+ try {
+ if (mProcess.exitValue() == 1) {
+ throw new IOException(e.getMessage());
+ }
+
+ } catch (IllegalThreadStateException ignore) {}
+
+ } catch (Throwable te) {
+ throw new IOException(te.getMessage());
+ }
+ }
+
+ Bundle bundle = new Bundle();
+ bundle.putString("action", "exists");
+ bundle.putString("location", filePath);
+
+ Shell.sendBroadcast("file", bundle);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void close() throws IOException {
+ mStream.flush();
+ mStream.close();
+ mStream = null;
+
+ if (mProcess != null) {
+ mProcess.destroy();
+ mProcess = null;
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void flush() throws IOException {
+ mStream.flush();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void write(char[] buf, int offset, int count) throws IOException {
+ synchronized(lock) {
+ byte[] bytes = new byte[buf.length];
+
+ for (int i=0; i < bytes.length; i++) {
+ bytes[i] = (byte) buf[i];
+ }
+
+ mStream.write(bytes, offset, count);
+ }
+ }
+
+ public void write(byte[] buf, int offset, int count) throws IOException {
+ synchronized(lock) {
+ mStream.write(buf, offset, count);
+ }
+ }
+
+ public void write(byte[] buf) throws IOException {
+ synchronized(lock) {
+ mStream.write(buf);
+ }
+ }
+}
diff --git a/libraries/sharedCode/src/main/java/sharedcode/turboeditor/activity/MainActivity.java b/libraries/sharedCode/src/main/java/sharedcode/turboeditor/activity/MainActivity.java
index d76a657..0a11f4b 100644
--- a/libraries/sharedCode/src/main/java/sharedcode/turboeditor/activity/MainActivity.java
+++ b/libraries/sharedCode/src/main/java/sharedcode/turboeditor/activity/MainActivity.java
@@ -34,6 +34,7 @@ import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.preference.PreferenceManager;
+import android.provider.DocumentsContract;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.view.MenuItemCompat;
@@ -65,13 +66,17 @@ import android.widget.ListView;
import android.widget.Toast;
import com.faizmalkani.floatingactionbutton.FloatingActionButton;
+import com.spazedog.lib.rootfw4.RootFW;
+import com.spazedog.lib.rootfw4.utils.io.FileReader;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang3.ArrayUtils;
-import org.sufficientlysecure.rootcommands.Shell;
-import org.sufficientlysecure.rootcommands.Toolbox;
+import java.io.BufferedReader;
import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Arrays;
@@ -83,6 +88,7 @@ import java.util.regex.Pattern;
import sharedcode.turboeditor.R;
import sharedcode.turboeditor.adapter.AdapterDrawer;
import sharedcode.turboeditor.dialogfragment.ChangelogDialog;
+import sharedcode.turboeditor.dialogfragment.EditTextDialog;
import sharedcode.turboeditor.dialogfragment.FileInfoDialog;
import sharedcode.turboeditor.dialogfragment.FindTextDialog;
import sharedcode.turboeditor.dialogfragment.NewFileDetailsDialog;
@@ -102,10 +108,13 @@ import sharedcode.turboeditor.util.AccessStorageApi;
import sharedcode.turboeditor.util.AccessoryView;
import sharedcode.turboeditor.util.AnimationUtils;
import sharedcode.turboeditor.util.AppInfoHelper;
+import sharedcode.turboeditor.util.Device;
+import sharedcode.turboeditor.util.GreatUri;
import sharedcode.turboeditor.util.IHomeActivity;
import sharedcode.turboeditor.util.MimeTypes;
import sharedcode.turboeditor.util.ProCheckUtils;
import sharedcode.turboeditor.util.ThemeUtils;
+import sharedcode.turboeditor.util.ViewUtils;
import sharedcode.turboeditor.views.CustomDrawerLayout;
import sharedcode.turboeditor.views.DialogHelper;
import sharedcode.turboeditor.views.GoodScrollView;
@@ -113,63 +122,64 @@ import sharedcode.turboeditor.views.GoodScrollView;
public abstract class MainActivity extends ActionBarActivity implements IHomeActivity, FindTextDialog
.SearchDialogInterface, GoodScrollView.ScrollInterface, PageSystem.PageSystemInterface,
PageSystemButtons.PageButtonsInterface, NumberPickerDialog.INumberPickerDialog, SaveFileDialog.ISaveDialog,
- AdapterView.OnItemClickListener, AdapterDrawer.Callbacks, AccessoryView.IAccessoryView{
+ AdapterView.OnItemClickListener, AdapterDrawer.Callbacks, AccessoryView.IAccessoryView, EditTextDialog.EditDialogListener{
//region VARIABLES
- private static final int READ_REQUEST_CODE = 42;
- private static final int
- ID_SELECT_ALL = android.R.id.selectAll;
- private static final int ID_CUT = android.R.id.cut;
- private static final int ID_COPY = android.R.id.copy;
- private static final int ID_PASTE = android.R.id.paste;
- private static final int SELECT_FILE_CODE = 121;
- private static final int SYNTAX_DELAY_MILLIS_SHORT = 250;
- private static final int SYNTAX_DELAY_MILLIS_LONG = 1500;
- private static final int ID_UNDO = R.id.im_undo;
- private static final int ID_REDO = R.id.im_redo;
- private static final int CHARS_TO_COLOR = 2500;
- private static final Handler updateHandler = new Handler();
- private static final Runnable colorRunnable_duringEditing =
+ private static final int READ_REQUEST_CODE = 42,
+ CREATE_REQUEST_CODE = 43,
+ SAVE_AS_REQUEST_CODE = 44,
+ ID_SELECT_ALL = android.R.id.selectAll,
+ ID_CUT = android.R.id.cut,
+ ID_COPY = android.R.id.copy,
+ ID_PASTE = android.R.id.paste,
+ SELECT_FILE_CODE = 121,
+ SYNTAX_DELAY_MILLIS_SHORT = 250,
+ SYNTAX_DELAY_MILLIS_LONG = 1500,
+ ID_UNDO = R.id.im_undo,
+ ID_REDO = R.id.im_redo,
+ CHARS_TO_COLOR = 2500;
+ private final Handler updateHandler = new Handler();
+ private final Runnable colorRunnable_duringEditing =
new Runnable() {
@Override
public void run() {
mEditor.replaceTextKeepCursor(null);
}
};
- private static final Runnable colorRunnable_duringScroll =
+ private final Runnable colorRunnable_duringScroll =
new Runnable() {
@Override
public void run() {
mEditor.replaceTextKeepCursor(null);
}
};
- private static boolean fileOpened = false;
- private static String fileExtension;
+ private boolean fileOpened = false;
+ private static String fileExtension = "";
/*
* This class provides a handy way to tie together the functionality of
* {@link DrawerLayout} and the framework ActionBar
to implement the recommended
* design for navigation drawers.
*/
- private static ActionBarDrawerToggle mDrawerToggle;
+ private ActionBarDrawerToggle mDrawerToggle;
/*
* The Drawer Layout
*/
- private static CustomDrawerLayout mDrawerLayout;
+ private CustomDrawerLayout mDrawerLayout;
private static GoodScrollView verticalScroll;
- private static String sFilePath = "";
- private static Editor mEditor;
- private static HorizontalScrollView horizontalScroll;
+ private static GreatUri greatUri = new GreatUri(Uri.EMPTY, "", "", false);
+ private Editor mEditor;
+ private HorizontalScrollView horizontalScroll;
private static SearchResult searchResult;
private static PageSystem pageSystem;
- private static PageSystemButtons pageSystemButtons;
+ private PageSystemButtons pageSystemButtons;
private static String currentEncoding = "UTF-16";
- private static Toolbar toolbar;
+ private Toolbar toolbar;
/*
Navigation Drawer
*/
- private static AdapterDrawer arrayAdapter;
- private static LinkedList files;
+ private AdapterDrawer arrayAdapter;
+ private LinkedList greatUris;
//endregion
//region Activity facts
@@ -291,7 +301,7 @@ public abstract class MainActivity extends ActionBarActivity implements IHomeAct
} else if (mDrawerLayout.isDrawerOpen(Gravity.END) && fileOpened) {
mDrawerLayout.closeDrawer(Gravity.END);
} else if (fileOpened && mEditor.canSaveFile()) {
- SaveFileDialog.newInstance(sFilePath, pageSystem.getAllText(mEditor
+ new SaveFileDialog(greatUri, pageSystem.getAllText(mEditor
.getText().toString()), currentEncoding).show(getFragmentManager(),
"dialog");
} else if (fileOpened) {
@@ -316,37 +326,50 @@ public abstract class MainActivity extends ActionBarActivity implements IHomeAct
}
@Override
- protected void onActivityResult(int requestCode, int resultCode, Intent data) {
- super.onActivityResult(requestCode, resultCode, data);
+ protected void onActivityResult(int requestCode, int resultCode, final Intent intent) {
+ super.onActivityResult(requestCode, resultCode, intent);
if (resultCode == RESULT_OK) {
- String path = "";
if (requestCode == SELECT_FILE_CODE) {
- path = data.getStringExtra("path");
- if(TextUtils.isEmpty(path))
- path = AccessStorageApi.getPath(getBaseContext(), data.getData());
- }
- if (requestCode == READ_REQUEST_CODE) {
- path = AccessStorageApi.getPath(getBaseContext(), data.getData());
- }
+ final Uri data = intent.getData();
+ final GreatUri newUri = new GreatUri(data, AccessStorageApi.getPath(this, data), AccessStorageApi.getName(this, data), false);
- if (!TextUtils.isEmpty(path)) {
- File file = new File(path);
- if (file.isFile() && file.exists()) {
- newFileToOpen(new File
- (path), "");
+ newFileToOpen(newUri, "");
+ } else {
+
+ final Uri data = intent.getData();
+ final GreatUri newUri = new GreatUri(data, AccessStorageApi.getPath(this, data), AccessStorageApi.getName(this, data), false);
+
+ // grantUriPermission(getPackageName(), data, Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ final int takeFlags = intent.getFlags()
+ & (Intent.FLAG_GRANT_READ_URI_PERMISSION
+ | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
+ // Check for the freshest data.
+ getContentResolver().takePersistableUriPermission(data, takeFlags);
+
+ if (requestCode == READ_REQUEST_CODE || requestCode == CREATE_REQUEST_CODE) {
+
+ newFileToOpen(newUri, "");
+ }
+
+ if (requestCode == SAVE_AS_REQUEST_CODE) {
+
+ new SaveFileTask(this, newUri, pageSystem.getAllText(mEditor.getText().toString()), currentEncoding, new SaveFileTask.SaveFileInterface() {
+ @Override
+ public void fileSaved(Boolean success) {
+ savedAFile(greatUri, false);
+ newFileToOpen(newUri, "");
+ }
+ }).execute();
}
}
-
}
}
@Override
public void onItemClick(AdapterView> parent, View view, int position, long id) {
- // Path of the file selected
- String filePath = files.get(position).getAbsolutePath();
// Send the event that a file was selected
- newFileToOpen(new File(filePath), "");
+ newFileToOpen(greatUris.get(position), "");
}
//endregion
@@ -403,19 +426,20 @@ public abstract class MainActivity extends ActionBarActivity implements IHomeAct
}
MenuItem imMarkdown = menu.findItem(R.id.im_view_markdown);
- boolean isMarkdown = Arrays.asList(MimeTypes.MIME_MARKDOWN).contains(FilenameUtils.getExtension(sFilePath));
+ boolean isMarkdown = Arrays.asList(MimeTypes.MIME_MARKDOWN).contains(FilenameUtils.getExtension(greatUri.getFileName()));
if (imMarkdown != null)
imMarkdown.setVisible(isMarkdown);
MenuItem imShare = menu.findItem(R.id.im_share);
- ShareActionProvider shareAction = (ShareActionProvider) MenuItemCompat
- .getActionProvider(imShare);
- File f = new File(sFilePath);
- Intent shareIntent = new Intent();
- shareIntent.setAction(Intent.ACTION_SEND);
- shareIntent.putExtra(Intent.EXTRA_STREAM, Uri.fromFile(f));
- shareIntent.setType("text/plain");
- shareAction.setShareIntent(shareIntent);
+ if (imMarkdown != null) {
+ ShareActionProvider shareAction = (ShareActionProvider) MenuItemCompat
+ .getActionProvider(imShare);
+ Intent shareIntent = new Intent();
+ shareIntent.setAction(Intent.ACTION_SEND);
+ shareIntent.putExtra(Intent.EXTRA_STREAM, greatUri.getUri());
+ shareIntent.setType("text/plain");
+ shareAction.setShareIntent(shareIntent);
+ }
}
MenuItem imDonate = menu.findItem(R.id.im_donate);
@@ -439,6 +463,8 @@ public abstract class MainActivity extends ActionBarActivity implements IHomeAct
} else if (i == R.id.im_save_as) {
saveTheFile(true);
+ } else if (i == R.id.im_rename) {
+ EditTextDialog.newInstance(EditTextDialog.Actions.Rename, greatUri.getFileName()).show(getFragmentManager().beginTransaction(), "dialog");
} else if (i == R.id.im_undo) {
mEditor.onTextContextMenuItem(ID_UNDO);
@@ -473,7 +499,7 @@ public abstract class MainActivity extends ActionBarActivity implements IHomeAct
Intent browserIntent;
try {
browserIntent = new Intent(Intent.ACTION_VIEW);
- browserIntent.setDataAndType(Uri.fromFile(new File(sFilePath)), "text/*");
+ browserIntent.setDataAndType(greatUri.getUri(), "*/*");
startActivity(browserIntent);
} catch (ActivityNotFoundException ex2) {
//
@@ -481,10 +507,17 @@ public abstract class MainActivity extends ActionBarActivity implements IHomeAct
} else if (i == R.id.im_view_markdown) {
Intent browserIntent = new Intent(MainActivity.this, MarkdownActivity.class);
- browserIntent.putExtra("text", mEditor.getText().toString());
+ browserIntent.putExtra("text", pageSystem.getAllText(mEditor.getText().toString()));
startActivity(browserIntent);
} else if (i == R.id.im_info) {
- FileInfoDialog.newInstance(sFilePath).show(getFragmentManager().beginTransaction(), "dialog");
+ FileInfoDialog.newInstance(greatUri.getUri()).show(getFragmentManager().beginTransaction(), "dialog");
+ } else if (i == R.id.im_donate) {
+ final String appPackageName = "com.maskyn.fileeditorpro";
+ try {
+ startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=" + appPackageName)));
+ } catch (android.content.ActivityNotFoundException anfe) {
+ startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("https://play.google.com/store/apps/details?id=" + appPackageName)));
+ }
}
return super.onOptionsItemSelected(item);
}
@@ -493,7 +526,7 @@ public abstract class MainActivity extends ActionBarActivity implements IHomeAct
// region OTHER THINGS
void replaceText(boolean all) {
if (all) {
- mEditor.setText(mEditor.getText().toString().replaceAll(searchResult.whatToSearch, searchResult.textToReplace));
+ mEditor.setText(pageSystem.getAllText(mEditor.getText().toString()).replaceAll(searchResult.whatToSearch, searchResult.textToReplace));
searchResult = null;
invalidateOptionsMenu();
@@ -576,17 +609,28 @@ public abstract class MainActivity extends ActionBarActivity implements IHomeAct
}
private void saveTheFile(boolean saveAs) {
- File file = new File(sFilePath);
- if (!file.getName().isEmpty() && !saveAs)
- new SaveFileTask(this, sFilePath, pageSystem.getAllText(mEditor.getText()
- .toString()), currentEncoding).execute();
+ if (!saveAs && greatUri != null && greatUri.getUri() != null && greatUri.getUri() != Uri.EMPTY)
+ new SaveFileTask(this, greatUri, pageSystem.getAllText(mEditor.getText()
+ .toString()), currentEncoding, new SaveFileTask.SaveFileInterface() {
+ @Override
+ public void fileSaved(Boolean success) {
+ savedAFile(greatUri, true);
+ }
+ }).execute();
else {
- NewFileDetailsDialog.newInstance(
- file.getParent(),
- file.getName(),
- pageSystem.getAllText(mEditor.getText().toString()),
- currentEncoding
- ).show(getFragmentManager().beginTransaction(), "dialog");
+ if (Device.hasKitKatApi() && PreferenceHelper.getUseStorageAccessFramework(this)) {
+ Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
+ intent.setType("*/*");
+ intent.putExtra(Intent.EXTRA_TITLE, greatUri.getFileName());
+ startActivityForResult(intent, SAVE_AS_REQUEST_CODE);
+ } else {
+ new NewFileDetailsDialog(
+ greatUri,
+ pageSystem.getAllText(mEditor.getText().toString()),
+ currentEncoding
+ ).show(getFragmentManager().beginTransaction(), "dialog");
+ }
+
}
}
@@ -629,8 +673,8 @@ public abstract class MainActivity extends ActionBarActivity implements IHomeAct
ListView listView = (ListView) findViewById(android.R.id.list);
listView.setEmptyView(findViewById(android.R.id.empty));
- files = new LinkedList<>();
- arrayAdapter = new AdapterDrawer(this, files, this);
+ greatUris = new LinkedList<>();
+ arrayAdapter = new AdapterDrawer(this, greatUris, this);
listView.setAdapter(arrayAdapter);
listView.setOnItemClickListener(this);
}
@@ -643,7 +687,10 @@ public abstract class MainActivity extends ActionBarActivity implements IHomeAct
AccessoryView accessoryView = (AccessoryView) findViewById(R.id.accessoryView);
accessoryView.setInterface(this);
- //mEditor.setLayerType(View.LAYER_TYPE_NONE, null);
+
+ HorizontalScrollView parentAccessoryView = (HorizontalScrollView) findViewById(R.id.parent_accessory_view);
+ ViewUtils.setVisible(parentAccessoryView, PreferenceHelper.getUseAccessoryView(this));
+
if (PreferenceHelper.getWrapContent(this)) {
horizontalScroll.removeView(mEditor);
@@ -653,11 +700,13 @@ public abstract class MainActivity extends ActionBarActivity implements IHomeAct
verticalScroll.setScrollInterface(this);
- pageSystem = new PageSystem(this, this, "", null);
+ pageSystem = new PageSystem(this, this, "");
pageSystemButtons = new PageSystemButtons(this, this,
(FloatingActionButton) findViewById(R.id.fabPrev),
(FloatingActionButton) findViewById(R.id.fabNext));
+
+ mEditor.setupEditor();
}
private void showTextEditor() {
@@ -705,11 +754,14 @@ public abstract class MainActivity extends ActionBarActivity implements IHomeAct
|| Intent.ACTION_PICK.equals(action)
&& type != null) {
// Post event
- newFileToOpen(new File(intent
- .getData().getPath()), "");
+ //newFileToOpen(new File(intent
+ // .getData().getPath()), "");
+ Uri uri = intent.getData();
+ GreatUri newUri = new GreatUri(uri, AccessStorageApi.getPath(this, uri), AccessStorageApi.getName(this, uri), false);
+ newFileToOpen(newUri, "");
} else if (Intent.ACTION_SEND.equals(action) && type != null) {
if ("text/plain".equals(type)) {
- newFileToOpen(null, intent.getStringExtra(Intent.EXTRA_TEXT));
+ newFileToOpen(new GreatUri(Uri.EMPTY, "", "", false), intent.getStringExtra(Intent.EXTRA_TEXT));
}
}
}
@@ -757,7 +809,7 @@ public abstract class MainActivity extends ActionBarActivity implements IHomeAct
refreshList(null, false, false);
}
- private void refreshList(@Nullable String path, boolean add, boolean delete) {
+ private void refreshList(@Nullable GreatUri thisUri, boolean add, boolean delete) {
int max_recent_files = 15;
if(add)
max_recent_files--;
@@ -767,27 +819,40 @@ public abstract class MainActivity extends ActionBarActivity implements IHomeAct
int first_index_of_array = savedPaths.length > max_recent_files ? savedPaths.length - max_recent_files : 0;
savedPaths = ArrayUtils.subarray(savedPaths, first_index_of_array, savedPaths.length);
// File names for the list
- files.clear();
+ greatUris.clear();
// StringBuilder that will contain the file paths
StringBuilder sb = new StringBuilder();
- // for cycle to convert paths to names
+ // for cycle to convert paths to names
for(int i = 0; i < savedPaths.length; i++){
- String savedPath = savedPaths[i];
- File file = new File(savedPath);
+ Uri particularUri = Uri.parse(savedPaths[i]);
+ String name = AccessStorageApi.getName(this, particularUri);
// Check that the file exist
- if (file.exists()) {
- if(path != null && path.equals(savedPath) && delete)
- continue;
+ // if is null or empty the particular url we dont use it
+ if (particularUri != null && !particularUri.equals(Uri.EMPTY) && !TextUtils.isEmpty(name)) {
+ // if the particular uri is good
+ boolean good = false;
+ if (thisUri == null || thisUri.getUri() == null || thisUri.getUri() == Uri.EMPTY)
+ good = true;
else {
- files.addFirst(file);
- sb.append(savedPath).append(",");
+ if (delete == false)
+ good = true;
+ else if (!thisUri.getUri().equals(particularUri))
+ good = true;
+ else
+ good = false;
+ }
+ if (good) {
+ greatUris.addFirst(new GreatUri(particularUri, AccessStorageApi.getPath(this, particularUri), name, false));
+ sb.append(savedPaths[i]).append(",");
}
}
+ //}
}
- if(path != null && !path.isEmpty() && add && !ArrayUtils.contains(savedPaths, path)) {
- sb.append(path).append(",");
- files.addFirst(new File(path));
+ // if is not null, empty, we have to add something and we dont already have this uri
+ if(thisUri != null && !thisUri.getUri().equals(Uri.EMPTY) && add && !ArrayUtils.contains(savedPaths, thisUri.getUri().toString())) {
+ sb.append(thisUri.getUri().toString()).append(",");
+ greatUris.addFirst(thisUri);
}
// save list without empty or non existed files
PreferenceHelper.setSavedPaths(this, sb);
@@ -797,21 +862,23 @@ public abstract class MainActivity extends ActionBarActivity implements IHomeAct
//endregion
//region EVENTBUS
- public void newFileToOpen(final File newFile, final String newFileText) {
+ void newFileToOpen(final GreatUri newUri, final String newFileText) {
- if (fileOpened && mEditor != null && mEditor.canSaveFile()) {
- SaveFileDialog.newInstance(sFilePath, pageSystem.getAllText(mEditor
- .getText().toString()), currentEncoding, true, newFile.getAbsolutePath()).show(getFragmentManager(),
+ if (fileOpened && mEditor != null && mEditor.canSaveFile() && greatUri != null && pageSystem != null && currentEncoding != null) {
+ new SaveFileDialog(greatUri, pageSystem.getAllText(mEditor
+ .getText().toString()), currentEncoding, true, newUri).show(getFragmentManager(),
"dialog");
return;
}
new AsyncTask() {
- File file;
String message = "";
String fileText;
+ String filePath = "";
+ String fileName = "";
String encoding;
+ boolean isRootRequired = false;
ProgressDialog progressDialog;
@Override
@@ -827,52 +894,41 @@ public abstract class MainActivity extends ActionBarActivity implements IHomeAct
@Override
protected Void doInBackground(Void... params) {
- file = newFile;
- if (file == null) {
- file = new File("");
- }
try {
- if (!file.exists() || !file.isFile()) {
- fileText = newFileText;
- sFilePath = file.getAbsolutePath();
+ // if no new uri
+ if (newUri == null || newUri.getUri() == null || newUri.getUri() == Uri.EMPTY) {
fileExtension = "txt";
- return null;
- }
-
- file = file.getCanonicalFile();
- sFilePath = file.getAbsolutePath();
- fileExtension = FilenameUtils.getExtension(sFilePath).toLowerCase();
-
- boolean isRoot;
-
- if (!file.canRead()) {
- Shell shell;
- shell = Shell.startRootShell();
- Toolbox tb = new Toolbox(shell);
- isRoot = tb.isRootAccessGiven();
-
- if (isRoot) {
- File tempFile = new File(getFilesDir(), "temp.root.file");
- if (!tempFile.exists())
- tempFile.createNewFile();
- tb.copyFile(file.getAbsolutePath(),
- tempFile.getAbsolutePath(), false, false);
- file = new File(tempFile.getAbsolutePath());
- }
- }
-
- boolean autoencoding = PreferenceHelper.getAutoEncoding(MainActivity.this);
- if (autoencoding) {
-
- encoding = FileUtils.getDetectedEncoding(file);
- if (encoding.isEmpty()) {
- encoding = PreferenceHelper.getEncoding(MainActivity.this);
- }
+ fileText = newFileText;
} else {
- encoding = PreferenceHelper.getEncoding(MainActivity.this);
+ filePath = newUri.getFilePath();
+
+ // if the uri has no path
+ if (TextUtils.isEmpty(filePath)) {
+ fileName = newUri.getFileName();
+ fileExtension = FilenameUtils.getExtension(fileName).toLowerCase();
+
+ readUri(newUri.getUri(), filePath, false);
+ }
+ // if the uri has a path
+ else {
+ fileName = FilenameUtils.getName(filePath);
+ fileExtension = FilenameUtils.getExtension(fileName).toLowerCase();
+
+ isRootRequired = !(new File(filePath).canRead());
+ // if we cannot read the file, root permission required
+ if (isRootRequired) {
+ newUri.setRootRequired(true);
+ readUri(newUri.getUri(), filePath, true);
+ }
+ // if we can read the file associated with the uri
+ else {
+ readUri(newUri.getUri(), filePath, false);
+ }
+ }
+
}
- fileText = org.apache.commons.io.FileUtils.readFileToString(file, encoding);
+ greatUri = newUri;
} catch (Exception e) {
message = e.getMessage();
fileText = "";
@@ -888,31 +944,75 @@ public abstract class MainActivity extends ActionBarActivity implements IHomeAct
return null;
}
+ private void readUri(Uri uri, String path, boolean asRoot) throws IOException {
+
+
+ BufferedReader buffer = null;
+ StringBuilder stringBuilder = new StringBuilder();
+ String line;
+
+ if (asRoot) {
+
+ encoding = "UTF-8";
+
+ // Connect the shared connection
+ if (RootFW.connect()) {
+ FileReader reader = RootFW.getFileReader(path);
+ buffer = new BufferedReader(reader);
+ }
+ } else {
+
+ boolean autoencoding = PreferenceHelper.getAutoEncoding(MainActivity.this);
+ if (autoencoding) {
+ encoding = FileUtils.getDetectedEncoding(getContentResolver().openInputStream(uri));
+ if (encoding.isEmpty()) {
+ encoding = PreferenceHelper.getEncoding(MainActivity.this);
+ }
+ } else {
+ encoding = PreferenceHelper.getEncoding(MainActivity.this);
+ }
+
+ InputStream inputStream = getContentResolver().openInputStream(uri);
+ if(inputStream != null) {
+ buffer = new BufferedReader(new InputStreamReader(inputStream, encoding));
+ }
+ }
+
+ if (buffer != null) {
+ while((line = buffer.readLine()) != null) {
+ stringBuilder.append(line);
+ stringBuilder.append("\n");
+ }
+ buffer.close();
+ fileText = stringBuilder.toString();
+ }
+
+ }
+
@Override
protected void onPostExecute(Void result) {
super.onPostExecute(result);
progressDialog.hide();
- if (!message.isEmpty()) {
+ if (!TextUtils.isEmpty(message)) {
Toast.makeText(MainActivity.this, message, Toast.LENGTH_LONG).show();
cannotOpenFile();
} else {
- pageSystem = new PageSystem(MainActivity.this, MainActivity.this, fileText, new File(sFilePath));
+ pageSystem = new PageSystem(MainActivity.this, MainActivity.this, fileText);
currentEncoding = encoding;
- aFileWasSelected(sFilePath);
+ aFileWasSelected(greatUri);
showTextEditor();
- String name = FilenameUtils.getName(sFilePath);
- if (name.isEmpty())
+ if (fileName.isEmpty())
getSupportActionBar().setTitle(R.string.new_file);
else
- getSupportActionBar().setTitle(name);
+ getSupportActionBar().setTitle(fileName);
- if(!name.isEmpty()) {
- refreshList(sFilePath, true, false);
+ if(greatUri != null) {
+ refreshList(greatUri, true, false);
}
}
@@ -920,12 +1020,19 @@ public abstract class MainActivity extends ActionBarActivity implements IHomeAct
}.execute();
}
- public void savedAFile(String filePath) {
+ public void savedAFile(GreatUri uri, boolean updateList) {
- sFilePath = filePath;
- fileExtension = FilenameUtils.getExtension(sFilePath).toLowerCase();
+ if (uri != null) {
- toolbar.setTitle(FilenameUtils.getName(sFilePath));
+ String name = uri.getFileName();
+ fileExtension = FilenameUtils.getExtension(name).toLowerCase();
+ toolbar.setTitle(name);
+
+ if (updateList) {
+ refreshList(uri, true, false);
+ arrayAdapter.selectPosition(uri);
+ }
+ }
mEditor.clearHistory();
mEditor.fileSaved();
@@ -936,9 +1043,6 @@ public abstract class MainActivity extends ActionBarActivity implements IHomeAct
} catch (NullPointerException e) {
e.printStackTrace();
}
-
- refreshList(filePath, true, false);
- arrayAdapter.selectView(filePath);
}
/**
@@ -947,7 +1051,7 @@ public abstract class MainActivity extends ActionBarActivity implements IHomeAct
*
* @param event The event called
*/
- public void cannotOpenFile() {
+ void cannotOpenFile() {
//
mDrawerLayout.openDrawer(Gravity.LEFT);
//
@@ -964,7 +1068,7 @@ public abstract class MainActivity extends ActionBarActivity implements IHomeAct
}});
}
- public void aPreferenceValueWasChanged(List types) {
+ void aPreferenceValueWasChanged(List types) {
if (types.contains(PreferenceChangeType.THEME_CHANGE)) {
ThemeUtils.setWindowsBackground(this);
@@ -986,22 +1090,10 @@ public abstract class MainActivity extends ActionBarActivity implements IHomeAct
mEditor.disableTextChangedListener();
mEditor.replaceTextKeepCursor(null);
mEditor.enableTextChangedListener();
- if (PreferenceHelper.getLineNumbers(this)) {
- mEditor.setPadding(
- EditTextPadding.getPaddingWithLineNumbers(this, PreferenceHelper.getFontSize(this)),
- EditTextPadding.getPaddingTop(this),
- EditTextPadding.getPaddingTop(this),
- 0);
- } else {
- mEditor.setPadding(
- EditTextPadding.getPaddingWithoutLineNumbers(this),
- EditTextPadding.getPaddingTop(this),
- EditTextPadding.getPaddingTop(this),
- 0);
- }
+ mEditor.updatePadding();
} else if (types.contains(PreferenceChangeType.SYNTAX)) {
mEditor.disableTextChangedListener();
- mEditor.replaceTextKeepCursor(null);
+ mEditor.replaceTextKeepCursor(mEditor.getText().toString());
mEditor.enableTextChangedListener();
} else if (types.contains(PreferenceChangeType.MONOSPACE)) {
if (PreferenceHelper.getUseMonospace(this))
@@ -1035,20 +1127,12 @@ public abstract class MainActivity extends ActionBarActivity implements IHomeAct
else
mEditor.setTypeface(Typeface.DEFAULT);
} else if (types.contains(PreferenceChangeType.FONT_SIZE)) {
- if (PreferenceHelper.getLineNumbers(this)) {
- mEditor.setPadding(
- EditTextPadding.getPaddingWithLineNumbers(this, PreferenceHelper.getFontSize(this)),
- EditTextPadding.getPaddingTop(this),
- EditTextPadding.getPaddingTop(this),
- 0);
- } else {
- mEditor.setPadding(
- EditTextPadding.getPaddingWithoutLineNumbers(this),
- EditTextPadding.getPaddingTop(this),
- EditTextPadding.getPaddingTop(this),
- 0);
- }
+ mEditor.updatePadding();
mEditor.setTextSize(PreferenceHelper.getFontSize(this));
+ } else if (types.contains(PreferenceChangeType.ACCESSORY_VIEW)) {
+ HorizontalScrollView parentAccessoryView = (HorizontalScrollView) findViewById(R.id.parent_accessory_view);
+ ViewUtils.setVisible(parentAccessoryView, PreferenceHelper.getUseAccessoryView(this));
+ mEditor.updatePadding();
} else if (types.contains(PreferenceChangeType.ENCODING)) {
String oldEncoding, newEncoding;
oldEncoding = currentEncoding;
@@ -1071,43 +1155,50 @@ public abstract class MainActivity extends ActionBarActivity implements IHomeAct
}
}
- public void aFileWasSelected(String filePath) {
- arrayAdapter.selectView(filePath);
+ void aFileWasSelected(GreatUri uri) {
+ arrayAdapter.selectPosition(uri);
}
- public void closedTheFile() {
- arrayAdapter.selectView("");
+ void closedTheFile() {
+ arrayAdapter.selectPosition(new GreatUri(Uri.EMPTY, "", "", false));
}
//endregion
//region Calls from the layout
public void OpenFile(View view) {
-// if (Device.hasKitKatApi()) {
-// // ACTION_OPEN_DOCUMENT is the intent to choose a file via the system's file
-// // browser.
-// Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
-//
-// // Filter to only show results that can be "opened", such as a
-// // file (as opposed to a list of contacts or timezones)
-// //intent.addCategory(Intent.CATEGORY_OPENABLE);
-//
-// // Filter to show only images, using the image MIME data type.
-// // If one wanted to search for ogg vorbis files, the type would be "audio/ogg".
-// // To search for all documents available via installed storage providers,
-// // it would be "*/*".
-// intent.setType("*/*");
-//
-// startActivityForResult(intent, READ_REQUEST_CODE);
-// } else {
+ if (Device.hasKitKatApi() && PreferenceHelper.getUseStorageAccessFramework(this)) {
+ // ACTION_OPEN_DOCUMENT is the intent to choose a file via the system's file
+ // browser.
+ Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
+
+ // Filter to only show results that can be "opened", such as a
+ // file (as opposed to a list of contacts or timezones)
+ intent.addCategory(Intent.CATEGORY_OPENABLE);
+
+ // Filter to show only images, using the image MIME data type.
+ // If one wanted to search for ogg vorbis files, the type would be "audio/ogg".
+ // To search for all documents available via installed storage providers,
+ // it would be "*/*".
+ intent.setType("*/*");
+
+ startActivityForResult(intent, READ_REQUEST_CODE);
+ } else {
Intent subActivity = new Intent(MainActivity.this, SelectFileActivity.class);
subActivity.putExtra("action", SelectFileActivity.Actions.SelectFile);
AnimationUtils.startActivityWithScale(this, subActivity, true, SELECT_FILE_CODE, view);
-// }
+ }
}
public void CreateFile(View view) {
- newFileToOpen(null, ""); // do not send the event to others
+ if (Device.hasKitKatApi() && PreferenceHelper.getUseStorageAccessFramework(this)) {
+ Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
+ intent.setType("*/*");
+ //intent.putExtra(Intent.EXTRA_TITLE, ".txt");
+ startActivityForResult(intent, CREATE_REQUEST_CODE);
+ } else {
+ newFileToOpen(new GreatUri(Uri.EMPTY, "", "", false), "");
+ }
}
public void OpenInfo(View view) {
@@ -1264,17 +1355,17 @@ public abstract class MainActivity extends ActionBarActivity implements IHomeAct
}
@Override
- public void userDoesntWantToSave(boolean openNewFile, String pathOfNewFile) {
- Editor.canSaveFile = false;
+ public void userDoesntWantToSave(boolean openNewFile, GreatUri newUri) {
+ mEditor.fileSaved();
if(openNewFile)
- newFileToOpen(new File(pathOfNewFile), "");
+ newFileToOpen(newUri, "");
else
cannotOpenFile();
}
@Override
public void CancelItem(int position, boolean andCloseOpenedFile) {
- refreshList(files.get(position).getAbsolutePath(), false, true);
+ refreshList(greatUris.get(position), false, true);
if (andCloseOpenedFile)
cannotOpenFile();
}
@@ -1284,32 +1375,91 @@ public abstract class MainActivity extends ActionBarActivity implements IHomeAct
mEditor.getText().insert(mEditor.getSelectionStart(), text);
}
+ @Override
+ public void onEdittextDialogEnded(String result, String hint, EditTextDialog.Actions action) {
+
+ if (Device.hasKitKatApi() && TextUtils.isEmpty(greatUri.getFilePath())) {
+ Uri newUri = DocumentsContract.renameDocument(getContentResolver(), greatUri.getUri(), result);
+ // if everything is ok
+ if (newUri != null) {
+
+ // delete current
+ refreshList(greatUri, false, true);
+
+ greatUri.setUri(newUri);
+ greatUri.setFilePath(AccessStorageApi.getPath(this, newUri));
+ greatUri.setFileName(AccessStorageApi.getName(this, newUri));
+
+ new SaveFileTask(this, greatUri, pageSystem.getAllText(mEditor.getText().toString()), currentEncoding, new SaveFileTask.SaveFileInterface() {
+ @Override
+ public void fileSaved(Boolean success) {
+ savedAFile(greatUri, true);
+ }
+ }).execute();
+ }
+ else {
+ Toast.makeText(this, R.string.file_cannot_be_renamed, Toast.LENGTH_SHORT).show();
+ }
+ } else {
+ File newFile = new File(greatUri.getParentFolder(), result);
+ // if everything is ok
+ if (new File(greatUri.getFilePath()).renameTo(newFile)) {
+
+ // delete current
+ refreshList(greatUri, false, true);
+
+ greatUri.setUri(Uri.fromFile(newFile));
+ greatUri.setFilePath(newFile.getAbsolutePath());
+ greatUri.setFileName(newFile.getName());
+
+ new SaveFileTask(this, greatUri, pageSystem.getAllText(mEditor.getText().toString()), currentEncoding, new SaveFileTask.SaveFileInterface() {
+ @Override
+ public void fileSaved(Boolean success) {
+
+ savedAFile(greatUri, true);
+ }
+ }).execute();
+ }
+ else {
+ Toast.makeText(this, R.string.file_cannot_be_renamed, Toast.LENGTH_SHORT).show();
+ }
+ }
+
+
+ /*new SaveFileTask(this, greatUri, pageSystem.getAllText(mEditor.getText().toString()), currentEncoding, new SaveFileTask.SaveFileInterface() {
+ @Override
+ public void fileSaved(Boolean success) {
+ savedAFile(greatUri, true);
+ }
+ }).execute();*/
+ }
+
//endregion
public static class Editor extends EditText {
//region VARIABLES
- private static final TextPaint mPaintNumbers = new TextPaint();
+ private final TextPaint mPaintNumbers = new TextPaint();
/**
* The edit history.
*/
- private final EditHistory mEditHistory;
+ private EditHistory mEditHistory;
/**
* The change listener.
*/
- private final EditTextChangeListener
+ private EditTextChangeListener
mChangeListener;
/**
* Disconnect this undo/redo from the text
* view.
*/
- private static boolean enabledChangeListener;
- private static int paddingTop;
- private static int numbersWidth;
- private static int lineHeight;
+ private boolean enabledChangeListener;
+ private int paddingTop;
+ private int numbersWidth;
+ private int lineHeight;
- private static int lineCount, realLine, startingLine;
- private static LineUtils lineUtils;
+ private int lineCount, realLine, startingLine;
+ private LineUtils lineUtils;
/**
* Is undo/redo being performed? This member
* signals if an undo/redo operation is
@@ -1317,28 +1467,26 @@ public abstract class MainActivity extends ActionBarActivity implements IHomeAct
* text during undo/redo are not recorded
* because it would mess up the undo history.
*/
- private static boolean mIsUndoOrRedo;
- private static Matcher m;
- private static boolean mShowUndo, mShowRedo;
- private static boolean canSaveFile;
- private static KeyListener keyListener;
- private static int firstVisibleIndex, firstColoredIndex;
- private static int deviceHeight;
- private static int editorHeight;
- private static boolean[] isGoodLineArray;
- private static int[] realLines;
- private static boolean wrapContent;
- //private static int lastLine;
- //private static int firstLine;
- private static CharSequence textToHighlight;
- private static int lastVisibleIndex;
- private static int i;
+ private boolean mIsUndoOrRedo;
+ private Matcher m;
+ private boolean mShowUndo, mShowRedo;
+ private boolean canSaveFile;
+ private KeyListener keyListener;
+ private int firstVisibleIndex, firstColoredIndex, lastVisibleIndex;
+ private int deviceHeight;
+ private int editorHeight;
+ private boolean[] isGoodLineArray;
+ private int[] realLines;
+ private boolean wrapContent;
+ private CharSequence textToHighlight;
//endregion
//region CONSTRUCTOR
public Editor(final Context context, AttributeSet attrs) {
super(context, attrs);
+ }
+ public void setupEditor() {
//setLayerType(View.LAYER_TYPE_NONE, null);
mEditHistory = new EditHistory();
@@ -1359,19 +1507,8 @@ public abstract class MainActivity extends ActionBarActivity implements IHomeAct
} else {
setTextColor(getResources().getColor(R.color.textColor));
}
- if (PreferenceHelper.getLineNumbers(getContext())) {
- setPadding(
- EditTextPadding.getPaddingWithLineNumbers(getContext(), PreferenceHelper.getFontSize(getContext())),
- EditTextPadding.getPaddingTop(getContext()),
- EditTextPadding.getPaddingTop(getContext()),
- 0);
- } else {
- setPadding(
- EditTextPadding.getPaddingWithoutLineNumbers(getContext()),
- EditTextPadding.getPaddingTop(getContext()),
- EditTextPadding.getPaddingTop(getContext()),
- 0);
- }
+ // update the padding of the editor
+ updatePadding();
if (PreferenceHelper.getReadOnly(getContext())) {
setReadOnly(true);
@@ -1435,6 +1572,25 @@ public abstract class MainActivity extends ActionBarActivity implements IHomeAct
}
}
+ public void updatePadding() {
+ Context context = getContext();
+ if (PreferenceHelper.getLineNumbers(context)) {
+ setPadding(
+ EditTextPadding.getPaddingWithLineNumbers(context, PreferenceHelper.getFontSize(context)),
+ EditTextPadding.getPaddingTop(context),
+ EditTextPadding.getPaddingTop(context),
+ 0);
+ } else {
+ setPadding(
+ EditTextPadding.getPaddingWithoutLineNumbers(context),
+ EditTextPadding.getPaddingTop(context),
+ EditTextPadding.getPaddingTop(context),
+ 0);
+ }
+ // add a padding from bottom
+ verticalScroll.setPadding(0,0,0,EditTextPadding.getPaddingBottom(context));
+ }
+
//region OVERRIDES
@Override
public void setTextSize(float size) {
@@ -1461,12 +1617,10 @@ public abstract class MainActivity extends ActionBarActivity implements IHomeAct
}
- //editorHeight = getHeight();
-
if (PreferenceHelper.getLineNumbers(getContext())) {
wrapContent = PreferenceHelper.getWrapContent(getContext());
- for (i = 0; i < lineCount; i++) {
+ for (int i = 0; i < lineCount; i++) {
// if last line we count it anyway
if (!wrapContent
|| isGoodLineArray[i]) {
@@ -1706,16 +1860,21 @@ public abstract class MainActivity extends ActionBarActivity implements IHomeAct
enableTextChangedListener();
- boolean mantainCursorPos = cursorPos >= firstVisibleIndex && cursorPos <= lastVisibleIndex;
+ int newCursorPos;
- if (mantainCursorPos)
- firstVisibleIndex = cursorPos;
+ boolean cursorOnScreen = cursorPos >= firstVisibleIndex && cursorPos <= lastVisibleIndex;
- if (firstVisibleIndex > -1 && firstVisibleIndex < length()) {
+ if (cursorOnScreen) { // if the cursor is on screen
+ newCursorPos = cursorPos; // we dont change its position
+ } else {
+ newCursorPos = firstVisibleIndex; // else we set it to the first visible pos
+ }
+
+ if (newCursorPos > -1 && newCursorPos <= length()) {
if (cursorPosEnd != cursorPos)
setSelection(cursorPos, cursorPosEnd);
else
- setSelection(firstVisibleIndex);
+ setSelection(newCursorPos);
}
}
//endregion
@@ -1738,7 +1897,7 @@ public abstract class MainActivity extends ActionBarActivity implements IHomeAct
if (!newText && editorHeight > 0) {
firstVisibleIndex = getLayout().getLineStart(lineUtils.getFirstVisibleLine(verticalScroll, editorHeight, lineCount));
- lastVisibleIndex = getLayout().getLineStart(lineUtils.getLastVisibleLine(verticalScroll, editorHeight, lineCount, deviceHeight));
+ lastVisibleIndex = getLayout().getLineEnd(lineUtils.getLastVisibleLine(verticalScroll, editorHeight, lineCount, deviceHeight)-1);
} else {
firstVisibleIndex = 0;
lastVisibleIndex = CHARS_TO_COLOR;
@@ -1757,6 +1916,9 @@ public abstract class MainActivity extends ActionBarActivity implements IHomeAct
textToHighlight = editable.subSequence(firstColoredIndex, lastVisibleIndex);
+ if (TextUtils.isEmpty(fileExtension))
+ fileExtension = "";
+
if (fileExtension.contains("htm")
|| fileExtension.contains("xml")) {
color(Patterns.HTML_TAGS, editable, textToHighlight, firstColoredIndex);
diff --git a/libraries/sharedCode/src/main/java/sharedcode/turboeditor/activity/SelectFileActivity.java b/libraries/sharedCode/src/main/java/sharedcode/turboeditor/activity/SelectFileActivity.java
index b63f4e7..02188ab 100644
--- a/libraries/sharedCode/src/main/java/sharedcode/turboeditor/activity/SelectFileActivity.java
+++ b/libraries/sharedCode/src/main/java/sharedcode/turboeditor/activity/SelectFileActivity.java
@@ -20,6 +20,7 @@
package sharedcode.turboeditor.activity;
import android.content.Intent;
+import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.support.v4.view.MenuItemCompat;
@@ -38,26 +39,22 @@ import android.widget.TextView;
import android.widget.Toast;
import com.faizmalkani.floatingactionbutton.FloatingActionButton;
+import com.spazedog.lib.rootfw4.RootFW;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
-import org.sufficientlysecure.rootcommands.Shell;
-import org.sufficientlysecure.rootcommands.Toolbox;
import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
-import java.util.ArrayList;
-import java.util.Collections;
+import java.util.Arrays;
import java.util.Comparator;
import java.util.LinkedList;
-import java.util.concurrent.TimeoutException;
import sharedcode.turboeditor.R;
import sharedcode.turboeditor.adapter.AdapterDetailedList;
import sharedcode.turboeditor.dialogfragment.EditTextDialog;
import sharedcode.turboeditor.preferences.PreferenceHelper;
-import sharedcode.turboeditor.root.RootUtils;
import sharedcode.turboeditor.util.AlphanumComparator;
import sharedcode.turboeditor.util.Build;
import sharedcode.turboeditor.util.ThemeUtils;
@@ -84,8 +81,8 @@ public class SelectFileActivity extends ActionBarActivity implements SearchView.
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
- final Actions action = (Actions) getIntent().getExtras().getSerializable("action");
- wantAFile = action == Actions.SelectFile;
+ //final Actions action = (Actions) getIntent().getExtras().getSerializable("action");
+ wantAFile = true; //action == Actions.SelectFile;
listView = (ListView) findViewById(android.R.id.list);
listView.setOnItemClickListener(this);
@@ -163,15 +160,24 @@ public class SelectFileActivity extends ActionBarActivity implements SearchView.
return false;
}
- void returnData(String path) {
- final Intent returnIntent = new Intent();
- returnIntent.putExtra("path", path);
- setResult(RESULT_OK, returnIntent);
- // finish the activity
- finish();
+ /**
+ * Finish this Activity with a result code and URI of the selected file.
+ *
+ * @param file The file selected.
+ */
+ private void finishWithResult(File file) {
+ if (file != null) {
+ Uri uri = Uri.fromFile(file);
+ setResult(RESULT_OK, new Intent().setData(uri));
+ finish();
+ } else {
+ setResult(RESULT_CANCELED);
+ finish();
+ }
}
+
@Override
public void onItemClick(AdapterView> parent,
View view, int position, long id) {
@@ -198,7 +204,7 @@ public class SelectFileActivity extends ActionBarActivity implements SearchView.
final File selectedFile = new File(currentFolder, name);
if (selectedFile.isFile() && wantAFile) {
- returnData(selectedFile.getAbsolutePath());
+ finishWithResult(selectedFile);
} else if (selectedFile.isDirectory()) {
new UpdateList().execute(selectedFile.getAbsolutePath());
}
@@ -250,7 +256,7 @@ public class SelectFileActivity extends ActionBarActivity implements SearchView.
Toast.makeText(getBaseContext(), R.string.is_the_working_folder, Toast.LENGTH_SHORT).show();
return true;
} else if (i == R.id.im_select_folder) {
- returnData(currentFolder);
+ finishWithResult(new File(currentFolder));
return true;
}
return super.onOptionsItemSelected(item);
@@ -258,10 +264,15 @@ public class SelectFileActivity extends ActionBarActivity implements SearchView.
@Override
- public void onFinishEditDialog(final String inputText, final String hint, final EditTextDialog.Actions actions) {
+ public void onEdittextDialogEnded(final String inputText, final String hint, final EditTextDialog.Actions actions) {
if (actions == EditTextDialog.Actions.NewFile && !TextUtils.isEmpty(inputText)) {
File file = new File(currentFolder, inputText);
- returnData(file.getAbsolutePath());
+ try {
+ file.createNewFile();
+ finishWithResult(file);
+ } catch (IOException e) {
+ Toast.makeText(SelectFileActivity.this, e.getMessage(), Toast.LENGTH_SHORT).show();
+ }
} else if (actions == EditTextDialog.Actions.NewFolder && !TextUtils.isEmpty(inputText)) {
File file = new File(currentFolder, inputText);
file.mkdirs();
@@ -300,49 +311,59 @@ public class SelectFileActivity extends ActionBarActivity implements SearchView.
return null;
}
- File tempFile = new File(path);
- if (tempFile.isFile()) {
- tempFile = tempFile.getParentFile();
+ File tempFolder = new File(path);
+ if (tempFolder.isFile()) {
+ tempFolder = tempFolder.getParentFile();
}
String[] unopenableExtensions = {"apk", "mp3", "mp4", "png", "jpg", "jpeg"};
final LinkedList fileDetails = new LinkedList<>();
final LinkedList folderDetails = new LinkedList<>();
- final ArrayList files;
- currentFolder = tempFile.getAbsolutePath();
+ currentFolder = tempFolder.getAbsolutePath();
- boolean isRoot = false;
- if (!tempFile.canRead()) {
- try {
- Shell shell = null;
- shell = Shell.startRootShell();
- Toolbox tb = new Toolbox(shell);
- isRoot = tb.isRootAccessGiven();
- } catch (IOException | TimeoutException e) {
- e.printStackTrace();
- isRoot = false;
+ if (!tempFolder.canRead()) {
+ if (RootFW.connect()) {
+ com.spazedog.lib.rootfw4.utils.File folder = RootFW.getFile(currentFolder);
+ com.spazedog.lib.rootfw4.utils.File.FileStat[] stats = folder.getDetailedList();
+
+ if (stats != null) {
+ for (com.spazedog.lib.rootfw4.utils.File.FileStat stat : stats) {
+ if (stat.type().equals("d")) {
+ folderDetails.add(new AdapterDetailedList.FileDetail(stat.name(),
+ getString(R.string.folder),
+ ""));
+ } else if (!FilenameUtils.isExtension(stat.name().toLowerCase(), unopenableExtensions)
+ && stat.size() <= Build.MAX_FILE_SIZE * FileUtils.ONE_KB) {
+ final long fileSize = stat.size();
+ //SimpleDateFormat format = new SimpleDateFormat("MMM dd, yyyy hh:mm a");
+ //String date = format.format("");
+ fileDetails.add(new AdapterDetailedList.FileDetail(stat.name(),
+ FileUtils.byteCountToDisplaySize(fileSize), ""));
+ }
+ }
+ }
}
- }
+ } else {
+ File[] files = tempFolder.listFiles();
- files = RootUtils.getFileList(currentFolder, isRoot);
+ Arrays.sort(files, getFileNameComparator());
- Collections.sort(files, getFileNameComparator());
-
- if (files != null) {
- for (final File f : files) {
- if (f.isDirectory()) {
- folderDetails.add(new AdapterDetailedList.FileDetail(f.getName(),
- getString(R.string.folder),
- ""));
- } else if (f.isFile()
- && !FilenameUtils.isExtension(f.getName().toLowerCase(), unopenableExtensions)
- && FileUtils.sizeOf(f) <= Build.MAX_FILE_SIZE * FileUtils.ONE_KB) {
- final long fileSize = f.length();
- SimpleDateFormat format = new SimpleDateFormat("MMM dd, yyyy hh:mm a");
- String date = format.format(f.lastModified());
- fileDetails.add(new AdapterDetailedList.FileDetail(f.getName(),
- FileUtils.byteCountToDisplaySize(fileSize), date));
+ if (files != null) {
+ for (final File f : files) {
+ if (f.isDirectory()) {
+ folderDetails.add(new AdapterDetailedList.FileDetail(f.getName(),
+ getString(R.string.folder),
+ ""));
+ } else if (f.isFile()
+ && !FilenameUtils.isExtension(f.getName().toLowerCase(), unopenableExtensions)
+ && FileUtils.sizeOf(f) <= Build.MAX_FILE_SIZE * FileUtils.ONE_KB) {
+ final long fileSize = f.length();
+ SimpleDateFormat format = new SimpleDateFormat("MMM dd, yyyy hh:mm a");
+ String date = format.format(f.lastModified());
+ fileDetails.add(new AdapterDetailedList.FileDetail(f.getName(),
+ FileUtils.byteCountToDisplaySize(fileSize), date));
+ }
}
}
}
diff --git a/libraries/sharedCode/src/main/java/sharedcode/turboeditor/adapter/AdapterDrawer.java b/libraries/sharedCode/src/main/java/sharedcode/turboeditor/adapter/AdapterDrawer.java
index 7851294..4796595 100644
--- a/libraries/sharedCode/src/main/java/sharedcode/turboeditor/adapter/AdapterDrawer.java
+++ b/libraries/sharedCode/src/main/java/sharedcode/turboeditor/adapter/AdapterDrawer.java
@@ -21,7 +21,7 @@ package sharedcode.turboeditor.adapter;
import android.content.Context;
import android.graphics.Typeface;
-import android.text.TextUtils;
+import android.net.Uri;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@@ -29,26 +29,26 @@ import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.TextView;
-import java.io.File;
-import java.util.List;
+import java.util.LinkedList;
import sharedcode.turboeditor.R;
+import sharedcode.turboeditor.util.GreatUri;
public class AdapterDrawer extends
- ArrayAdapter {
+ ArrayAdapter {
private final Callbacks callbacks;
// Layout Inflater
private final LayoutInflater inflater;
// List of file details
- private final List files;
- private String selectedPath = "";
+ private final LinkedList greatUris;
+ private GreatUri selectedGreatUri = new GreatUri(Uri.EMPTY, "", "", false);
public AdapterDrawer(Context context,
- List files,
+ LinkedList greatUris,
Callbacks callbacks) {
- super(context, R.layout.item_file_list, files);
- this.files = files;
+ super(context, R.layout.item_file_list, greatUris);
+ this.greatUris = greatUris;
this.inflater = LayoutInflater.from(context);
this.callbacks = callbacks;
}
@@ -66,20 +66,21 @@ public class AdapterDrawer extends
hold.cancelButton = (ImageView) convertView.findViewById(R.id.button_remove_from_list);
convertView.setTag(hold);
- final String fileName = files.get(position).getName();
+ final GreatUri greatUri = greatUris.get(position);
+ final String fileName = greatUri.getFileName();
hold.nameLabel.setText(fileName);
hold.cancelButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
- boolean closeOpenedFile = TextUtils.equals(selectedPath, files.get(position).getAbsolutePath());
+ boolean closeOpenedFile = selectedGreatUri.getUri().equals(greatUri.getUri());
callbacks.CancelItem(position, closeOpenedFile);
if (closeOpenedFile)
- selectedPath = "";
+ selectPosition(new GreatUri(Uri.EMPTY, "", "", false));
}
});
- if (TextUtils.equals(selectedPath, files.get(position).getAbsolutePath())) {
+ if (selectedGreatUri.getUri().equals(greatUri.getUri())) {
hold.nameLabel.setTypeface(null, Typeface.BOLD);
} else {
hold.nameLabel.setTypeface(null, Typeface.NORMAL);
@@ -87,30 +88,33 @@ public class AdapterDrawer extends
} else {
final ViewHolder hold = ((ViewHolder) convertView.getTag());
- final String fileName = files.get(position).getName();
+ final GreatUri greatUri = greatUris.get(position);
+ final String fileName = greatUri.getFileName();
hold.nameLabel.setText(fileName);
hold.cancelButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
- boolean closeOpenedFile = TextUtils.equals(selectedPath, files.get(position).getAbsolutePath());
+ boolean closeOpenedFile = selectedGreatUri.getUri().equals(greatUri.getUri());
callbacks.CancelItem(position, closeOpenedFile);
if (closeOpenedFile)
- selectedPath = "";
+ selectPosition(new GreatUri(Uri.EMPTY, "", "", false));
+
}
});
- if (TextUtils.equals(selectedPath, files.get(position).getAbsolutePath())) {
+ if (selectedGreatUri.getUri().equals(greatUri.getUri())) {
hold.nameLabel.setTypeface(null, Typeface.BOLD);
} else {
hold.nameLabel.setTypeface(null, Typeface.NORMAL);
}
+
}
return convertView;
}
- public void selectView(String selectedPath) {
+ public void selectPosition(GreatUri greatUri) {
//callbacks.ItemSelected(selectedPath);
- this.selectedPath = selectedPath;
+ this.selectedGreatUri = greatUri;
notifyDataSetChanged();
}
diff --git a/libraries/sharedCode/src/main/java/sharedcode/turboeditor/dialogfragment/EditTextDialog.java b/libraries/sharedCode/src/main/java/sharedcode/turboeditor/dialogfragment/EditTextDialog.java
index 183a6ae..b4618a6 100644
--- a/libraries/sharedCode/src/main/java/sharedcode/turboeditor/dialogfragment/EditTextDialog.java
+++ b/libraries/sharedCode/src/main/java/sharedcode/turboeditor/dialogfragment/EditTextDialog.java
@@ -64,6 +64,9 @@ public class EditTextDialog extends DialogFragment implements TextView.OnEditorA
case NewFolder:
title = getString(R.string.folder);
break;
+ case Rename:
+ title = getString(R.string.rinomina);
+ break;
default:
title = null;
break;
@@ -108,7 +111,7 @@ public class EditTextDialog extends DialogFragment implements TextView.OnEditorA
if (target == null) {
target = (EditDialogListener) getActivity();
}
- target.onFinishEditDialog(this.mEditText.getText().toString(), getArguments().getString("hint"),
+ target.onEdittextDialogEnded(this.mEditText.getText().toString(), getArguments().getString("hint"),
(Actions) getArguments().getSerializable("action"));
this.dismiss();
}
@@ -123,10 +126,10 @@ public class EditTextDialog extends DialogFragment implements TextView.OnEditorA
}
public enum Actions {
- NewFile, NewFolder
+ NewFile, NewFolder, Rename
}
public interface EditDialogListener {
- void onFinishEditDialog(String result, String hint, Actions action);
+ void onEdittextDialogEnded(String result, String hint, Actions action);
}
}
\ No newline at end of file
diff --git a/libraries/sharedCode/src/main/java/sharedcode/turboeditor/dialogfragment/FileInfoDialog.java b/libraries/sharedCode/src/main/java/sharedcode/turboeditor/dialogfragment/FileInfoDialog.java
index dfc93df..852fed8 100644
--- a/libraries/sharedCode/src/main/java/sharedcode/turboeditor/dialogfragment/FileInfoDialog.java
+++ b/libraries/sharedCode/src/main/java/sharedcode/turboeditor/dialogfragment/FileInfoDialog.java
@@ -23,7 +23,9 @@ import android.app.AlertDialog;
import android.app.Dialog;
import android.app.DialogFragment;
import android.content.DialogInterface;
+import android.net.Uri;
import android.os.Bundle;
+import android.support.v4.provider.DocumentFile;
import android.view.View;
import android.widget.ListView;
@@ -34,15 +36,17 @@ import java.util.Date;
import sharedcode.turboeditor.R;
import sharedcode.turboeditor.adapter.AdapterTwoItem;
+import sharedcode.turboeditor.util.AccessStorageApi;
+import sharedcode.turboeditor.util.Device;
import sharedcode.turboeditor.views.DialogHelper;
// ...
public class FileInfoDialog extends DialogFragment {
- public static FileInfoDialog newInstance(String filePath) {
+ public static FileInfoDialog newInstance(Uri uri) {
final FileInfoDialog f = new FileInfoDialog();
final Bundle args = new Bundle();
- args.putString("filePath", filePath);
+ args.putParcelable("uri", uri);
f.setArguments(args);
return f;
}
@@ -58,7 +62,11 @@ public class FileInfoDialog extends DialogFragment {
ListView list = (ListView) view.findViewById(android.R.id.list);
- File file = new File(getArguments().getString("filePath"));
+ DocumentFile file = DocumentFile.fromFile(new File(AccessStorageApi.getPath(getActivity(), (Uri) getArguments().getParcelable("uri"))));
+
+ if (file == null && Device.hasKitKatApi()) {
+ file = DocumentFile.fromSingleUri(getActivity(), (Uri) getArguments().getParcelable("uri"));
+ }
// Get the last modification information.
Long lastModified = file.lastModified();
@@ -69,13 +77,13 @@ public class FileInfoDialog extends DialogFragment {
String[] lines1 = {
getString(R.string.name),
- getString(R.string.folder),
+ //getString(R.string.folder),
getString(R.string.size),
getString(R.string.modification_date)
};
String[] lines2 = {
file.getName(),
- file.getParent(),
+ //file.getParent(),
FileUtils.byteCountToDisplaySize(file.length()),
date.toString()
};
diff --git a/libraries/sharedCode/src/main/java/sharedcode/turboeditor/dialogfragment/NewFileDetailsDialog.java b/libraries/sharedCode/src/main/java/sharedcode/turboeditor/dialogfragment/NewFileDetailsDialog.java
index d34d9a3..e18e859 100644
--- a/libraries/sharedCode/src/main/java/sharedcode/turboeditor/dialogfragment/NewFileDetailsDialog.java
+++ b/libraries/sharedCode/src/main/java/sharedcode/turboeditor/dialogfragment/NewFileDetailsDialog.java
@@ -19,10 +19,12 @@
package sharedcode.turboeditor.dialogfragment;
+import android.annotation.SuppressLint;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.DialogFragment;
import android.content.DialogInterface;
+import android.net.Uri;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.View;
@@ -33,30 +35,32 @@ import android.widget.EditText;
import org.apache.commons.io.FileUtils;
import java.io.File;
+import java.io.IOException;
import sharedcode.turboeditor.R;
import sharedcode.turboeditor.activity.MainActivity;
import sharedcode.turboeditor.preferences.PreferenceHelper;
import sharedcode.turboeditor.task.SaveFileTask;
+import sharedcode.turboeditor.util.GreatUri;
import sharedcode.turboeditor.util.ViewUtils;
import sharedcode.turboeditor.views.DialogHelper;
// ...
+@SuppressLint("ValidFragment")
public class NewFileDetailsDialog extends DialogFragment {
private EditText mName;
private EditText mFolder;
private CheckBox mDeleteCurrentFile;
- public static NewFileDetailsDialog newInstance(String currentPath, String currentName, String fileText, String fileEncoding) {
- final NewFileDetailsDialog f = new NewFileDetailsDialog();
- final Bundle args = new Bundle();
- args.putString("path", currentPath);
- args.putString("name", currentName);
- args.putString("fileText", fileText);
- args.putString("fileEncoding", fileEncoding);
- f.setArguments(args);
- return f;
+ GreatUri currentUri;
+ String fileText;
+ String fileEncoding;
+
+ public NewFileDetailsDialog(GreatUri currentUri, String fileText, String fileEncoding) {
+ this.currentUri = currentUri;
+ this.fileText = fileText;
+ this.fileEncoding = fileEncoding;
}
@Override
@@ -70,18 +74,18 @@ public class NewFileDetailsDialog extends DialogFragment {
this.mName = (EditText) view.findViewById(android.R.id.text1);
this.mFolder = (EditText) view.findViewById(android.R.id.text2);
- boolean noName = TextUtils.isEmpty(getArguments().getString("name"));
- boolean noPath = TextUtils.isEmpty(getArguments().getString("path"));
+ boolean noName = TextUtils.isEmpty(currentUri.getFileName());
+ boolean noPath = TextUtils.isEmpty(currentUri.getFilePath());
if (noName) {
this.mName.setText(".txt");
} else {
- this.mName.setText(getArguments().getString("name"));
+ this.mName.setText(currentUri.getFileName());
}
if (noPath) {
this.mFolder.setText(PreferenceHelper.getWorkingFolder(getActivity()));
} else {
- this.mFolder.setText(getArguments().getString("path"));
+ this.mFolder.setText(currentUri.getParentFolder());
}
this.mDeleteCurrentFile = (CheckBox) view.findViewById(R.id.delete_current_file);
@@ -100,13 +104,29 @@ public class NewFileDetailsDialog extends DialogFragment {
public void onClick(DialogInterface dialog, int which) {
if (mDeleteCurrentFile.isChecked()) {
- FileUtils.deleteQuietly(new File(getArguments().getString("path"), getArguments().getString("name")));
+ FileUtils.deleteQuietly(new File(currentUri.getFilePath()));
}
if (!mName.getText().toString().isEmpty() && !mFolder.getText().toString().isEmpty()) {
- File file = new File(mFolder.getText().toString(), mName.getText().toString());
- new SaveFileTask((MainActivity) getActivity(), file.getPath(), getArguments().getString("fileText"), getArguments().getString("fileEncoding")).execute();
- PreferenceHelper.setWorkingFolder(getActivity(), file.getParent());
+
+ try {
+ File file = new File(mFolder.getText().toString(), mName.getText().toString());
+ file.getParentFile().mkdirs();
+ file.createNewFile();
+
+ final GreatUri newUri = new GreatUri(Uri.fromFile(file), file.getAbsolutePath(), file.getName(), false);
+
+ new SaveFileTask((MainActivity) getActivity(), newUri, fileText, fileEncoding, new SaveFileTask.SaveFileInterface() {
+ @Override
+ public void fileSaved(Boolean success) {
+ if (getActivity() != null) {
+ ((MainActivity) getActivity()).savedAFile(newUri, true);
+ }
+ }
+ }).execute();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
}
}
}
diff --git a/libraries/sharedCode/src/main/java/sharedcode/turboeditor/dialogfragment/SaveFileDialog.java b/libraries/sharedCode/src/main/java/sharedcode/turboeditor/dialogfragment/SaveFileDialog.java
index c60f236..323aa16 100644
--- a/libraries/sharedCode/src/main/java/sharedcode/turboeditor/dialogfragment/SaveFileDialog.java
+++ b/libraries/sharedCode/src/main/java/sharedcode/turboeditor/dialogfragment/SaveFileDialog.java
@@ -19,54 +19,55 @@
package sharedcode.turboeditor.dialogfragment;
+import android.annotation.SuppressLint;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.DialogFragment;
import android.content.DialogInterface;
+import android.net.Uri;
import android.os.Bundle;
import android.view.View;
-import org.apache.commons.io.FilenameUtils;
-
-import java.io.File;
-
import sharedcode.turboeditor.R;
import sharedcode.turboeditor.activity.MainActivity;
import sharedcode.turboeditor.task.SaveFileTask;
+import sharedcode.turboeditor.util.GreatUri;
import sharedcode.turboeditor.views.DialogHelper;
+@SuppressLint("ValidFragment")
public class SaveFileDialog extends DialogFragment {
- public static SaveFileDialog newInstance(String filePath, String text, String encoding) {
- return newInstance(filePath, text, encoding, false, "");
+ GreatUri uri;
+ String text;
+ String encoding;
+ boolean openNewFileAfter;
+ GreatUri newUri;
+
+ @SuppressLint("ValidFragment")
+ public SaveFileDialog(GreatUri uri, String text, String encoding) {
+ this.uri = uri;
+ this.text = text;
+ this.encoding = encoding;
+ this.openNewFileAfter = false;
+ this.newUri = new GreatUri(Uri.EMPTY, "", "", false);
}
- public static SaveFileDialog newInstance(String filePath, String text, String encoding, boolean openNewFileAfter, String pathOfNewFile) {
- SaveFileDialog frag = new SaveFileDialog();
- Bundle args = new Bundle();
- args.putString("filePath", filePath);
- args.putString("text", text);
- args.putString("encoding", encoding);
- args.putBoolean("openNewFileAfter", openNewFileAfter);
- args.putString("pathOfNewFile", pathOfNewFile);
- frag.setArguments(args);
- return frag;
+ @SuppressLint("ValidFragment")
+ public SaveFileDialog(GreatUri uri, String text, String encoding, boolean openNewFileAfter, GreatUri newUri) {
+ this.uri = uri;
+ this.text = text;
+ this.encoding = encoding;
+ this.openNewFileAfter = openNewFileAfter;
+ this.newUri = newUri;
}
-
-
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
- final String filePath = getArguments().getString("filePath");
- final String text = getArguments().getString("text");
- final String encoding = getArguments().getString("encoding");
- final String fileName = FilenameUtils.getName(filePath);
- final File file = new File(filePath);
View view = new DialogHelper.Builder(getActivity())
.setIcon(getResources().getDrawable(R.drawable.ic_action_save))
.setTitle(R.string.salva)
- .setMessage(String.format(getString(R.string.save_changes), fileName))
+ .setMessage(String.format(getString(R.string.save_changes), uri.getFileName()))
.createCommonView();
return new AlertDialog.Builder(getActivity())
@@ -75,15 +76,22 @@ public class SaveFileDialog extends DialogFragment {
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
- if (!fileName.isEmpty())
- new SaveFileTask((MainActivity) getActivity(), filePath, text,
- encoding).execute();
- else {
+ if (uri.getFileName().isEmpty()) {
NewFileDetailsDialog dialogFrag =
- NewFileDetailsDialog.newInstance("","",text,
+ new NewFileDetailsDialog(uri,text,
encoding);
dialogFrag.show(getFragmentManager().beginTransaction(),
"dialog");
+ } else {
+ new SaveFileTask((MainActivity) getActivity(), uri, text,
+ encoding, new SaveFileTask.SaveFileInterface() {
+ @Override
+ public void fileSaved(Boolean success) {
+ if (getActivity() != null) {
+ ((MainActivity) getActivity()).savedAFile(uri, true);
+ }
+ }
+ }).execute();
}
}
}
@@ -98,8 +106,7 @@ public class SaveFileDialog extends DialogFragment {
target = (ISaveDialog) getActivity();
}
target.userDoesntWantToSave(
- getArguments().getBoolean("openNewFileAfter"),
- getArguments().getString("pathOfNewFile")
+ openNewFileAfter, newUri
);
}
}
@@ -108,6 +115,6 @@ public class SaveFileDialog extends DialogFragment {
}
public interface ISaveDialog {
- void userDoesntWantToSave(boolean openNewFile, String pathOfNewFile);
+ void userDoesntWantToSave(boolean openNewFile, GreatUri newUri);
}
}
\ No newline at end of file
diff --git a/libraries/sharedCode/src/main/java/sharedcode/turboeditor/preferences/PreferenceChangeType.java b/libraries/sharedCode/src/main/java/sharedcode/turboeditor/preferences/PreferenceChangeType.java
index a6432c7..509ad4a 100644
--- a/libraries/sharedCode/src/main/java/sharedcode/turboeditor/preferences/PreferenceChangeType.java
+++ b/libraries/sharedCode/src/main/java/sharedcode/turboeditor/preferences/PreferenceChangeType.java
@@ -20,5 +20,5 @@
package sharedcode.turboeditor.preferences;
public enum PreferenceChangeType {
- FONT_SIZE, ENCODING, SYNTAX, WRAP_CONTENT, MONOSPACE, LINE_NUMERS, THEME_CHANGE, TEXT_SUGGESTIONS, READ_ONLY,
+ FONT_SIZE, ENCODING, SYNTAX, WRAP_CONTENT, MONOSPACE, LINE_NUMERS, THEME_CHANGE, TEXT_SUGGESTIONS, READ_ONLY, ACCESSORY_VIEW
}
\ No newline at end of file
diff --git a/libraries/sharedCode/src/main/java/sharedcode/turboeditor/preferences/PreferenceHelper.java b/libraries/sharedCode/src/main/java/sharedcode/turboeditor/preferences/PreferenceHelper.java
index 78b7b30..ed59459 100644
--- a/libraries/sharedCode/src/main/java/sharedcode/turboeditor/preferences/PreferenceHelper.java
+++ b/libraries/sharedCode/src/main/java/sharedcode/turboeditor/preferences/PreferenceHelper.java
@@ -26,6 +26,8 @@ import android.preference.PreferenceManager;
import java.io.File;
+import sharedcode.turboeditor.util.Device;
+
public final class PreferenceHelper {
//public static final String SD_CARD_ROOT = Environment.getExternalStorageDirectory().getAbsolutePath();
@@ -47,6 +49,14 @@ public final class PreferenceHelper {
return getPrefs(context).getBoolean("use_monospace", false);
}
+ public static boolean getUseAccessoryView(Context context) {
+ return getPrefs(context).getBoolean("accessory_view", true);
+ }
+
+ public static boolean getUseStorageAccessFramework(Context context) {
+ return getPrefs(context).getBoolean("storage_access_framework", false);
+ }
+
public static boolean getLineNumbers(Context context) {
return getPrefs(context).getBoolean("editor_line_numbers", true);
}
@@ -97,23 +107,24 @@ public final class PreferenceHelper {
public static String defaultFolder(Context context) {
String folder;
- /*File externalFolder = context.getFilesDir();
+ File externalFolder = context.getExternalFilesDir(null);
- if (externalFolder != null) {
+ if (externalFolder != null && Device.isKitKatApi()) {
folder = externalFolder.getAbsolutePath();
} else {
folder = Environment.getExternalStorageDirectory().getAbsolutePath();
- }*/
- folder = Environment.getExternalStorageDirectory().getAbsolutePath();
+ }
+ //folder = context.getExternalFilesDir(null).getAbsolutePath();
+ //folder = Environment.getExternalStorageDirectory().getAbsolutePath();
return folder;
}
public static String getWorkingFolder(Context context) {
- return getPrefs(context).getString("working_folder", defaultFolder(context));
+ return getPrefs(context).getString("working_folder2", defaultFolder(context));
}
public static String[] getSavedPaths(Context context) {
- return getPrefs(context).getString("savedPaths", "").split(",");
+ return getPrefs(context).getString("savedPaths2", "").split(",");
}
public static boolean getPageSystemButtonsPopupShown(Context context) {
@@ -145,6 +156,14 @@ public final class PreferenceHelper {
getEditor(context).putBoolean("use_monospace", value).commit();
}
+ public static void setUseAccessoryView(Context context, boolean value) {
+ getEditor(context).putBoolean("accessory_view", value).commit();
+ }
+
+ public static void setUseStorageAccessFramework(Context context, boolean value) {
+ getEditor(context).putBoolean("storage_access_framework", value).commit();
+ }
+
public static void setLineNumbers(Context context, boolean value) {
getEditor(context).putBoolean("editor_line_numbers", value).commit();
}
@@ -166,11 +185,11 @@ public final class PreferenceHelper {
}
public static void setWorkingFolder(Context context, String value) {
- getEditor(context).putString("working_folder", value).commit();
+ getEditor(context).putString("working_folder2", value).commit();
}
public static void setSavedPaths(Context context, StringBuilder stringBuilder) {
- getEditor(context).putString("savedPaths", stringBuilder.toString()).commit();
+ getEditor(context).putString("savedPaths2", stringBuilder.toString()).commit();
}
public static void setPageSystemButtonsPopupShown(Context context, boolean value) {
diff --git a/libraries/sharedCode/src/main/java/sharedcode/turboeditor/preferences/SettingsFragment.java b/libraries/sharedCode/src/main/java/sharedcode/turboeditor/preferences/SettingsFragment.java
index 15bfd09..8477eeb 100644
--- a/libraries/sharedCode/src/main/java/sharedcode/turboeditor/preferences/SettingsFragment.java
+++ b/libraries/sharedCode/src/main/java/sharedcode/turboeditor/preferences/SettingsFragment.java
@@ -20,6 +20,7 @@
package sharedcode.turboeditor.preferences;
import android.app.Fragment;
+import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
@@ -46,7 +47,8 @@ public class SettingsFragment extends Fragment implements NumberPickerDialog.INu
private boolean sWrapContent;
private boolean sUseMonospace;
private boolean sReadOnly;
-
+ private boolean sAccessoryView;
+ private boolean sStorageAccessFramework;
private boolean sSuggestions;
private boolean sAutoSave;
private boolean sIgnoreBackButton;
@@ -56,17 +58,19 @@ public class SettingsFragment extends Fragment implements NumberPickerDialog.INu
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
- sUseMonospace = PreferenceHelper.getUseMonospace(getActivity());
- sColorSyntax = PreferenceHelper.getSyntaxHighlight(getActivity());
- sWrapContent = PreferenceHelper.getWrapContent(getActivity());
- sLineNumbers = PreferenceHelper.getLineNumbers(getActivity());
- sReadOnly = PreferenceHelper.getReadOnly(getActivity());
-
- sSuggestions = PreferenceHelper.getSuggestionActive(getActivity());
- sAutoSave = PreferenceHelper.getAutoSave(getActivity());
- sIgnoreBackButton = PreferenceHelper.getIgnoreBackButton(getActivity());
- sSplitText = PreferenceHelper.getSplitText(getActivity());
- sErrorReports = PreferenceHelper.getSendErrorReports(getActivity());
+ Context context = getActivity();
+ sUseMonospace = PreferenceHelper.getUseMonospace(context);
+ sColorSyntax = PreferenceHelper.getSyntaxHighlight(context);
+ sWrapContent = PreferenceHelper.getWrapContent(context);
+ sLineNumbers = PreferenceHelper.getLineNumbers(context);
+ sReadOnly = PreferenceHelper.getReadOnly(context);
+ sAccessoryView = PreferenceHelper.getUseAccessoryView(context);
+ sStorageAccessFramework = PreferenceHelper.getUseStorageAccessFramework(context);
+ sSuggestions = PreferenceHelper.getSuggestionActive(context);
+ sAutoSave = PreferenceHelper.getAutoSave(context);
+ sIgnoreBackButton = PreferenceHelper.getIgnoreBackButton(context);
+ sSplitText = PreferenceHelper.getSplitText(context);
+ sErrorReports = PreferenceHelper.getSendErrorReports(context);
}
@Override
@@ -75,7 +79,7 @@ public class SettingsFragment extends Fragment implements NumberPickerDialog.INu
// Our custom layout
final View rootView = inflater.inflate(R.layout.fragment_settings, container, false);
final SwitchCompat swLineNumbers, swSyntax, swWrapContent, swMonospace, swReadOnly;
- final SwitchCompat swSuggestions, swAutoSave, swIgnoreBackButton, swSplitText, swErrorReports;
+ final SwitchCompat swSuggestions, swAccessoryView, swStorageAccessFramework, swAutoSave, swIgnoreBackButton, swSplitText, swErrorReports;
swLineNumbers = (SwitchCompat) rootView.findViewById(R.id.switch_line_numbers);
swSyntax = (SwitchCompat) rootView.findViewById(R.id.switch_syntax);
@@ -84,6 +88,8 @@ public class SettingsFragment extends Fragment implements NumberPickerDialog.INu
swReadOnly = (SwitchCompat) rootView.findViewById(R.id.switch_read_only);
swSuggestions = (SwitchCompat) rootView.findViewById(R.id.switch_suggestions_active);
+ swAccessoryView = (SwitchCompat) rootView.findViewById(R.id.switch_accessory_view);
+ swStorageAccessFramework = (SwitchCompat) rootView.findViewById(R.id.switch_storage_access_framework);
swAutoSave = (SwitchCompat) rootView.findViewById(R.id.switch_auto_save);
swIgnoreBackButton = (SwitchCompat) rootView.findViewById(R.id.switch_ignore_backbutton);
swSplitText = (SwitchCompat) rootView.findViewById(R.id.switch_page_system);
@@ -96,6 +102,8 @@ public class SettingsFragment extends Fragment implements NumberPickerDialog.INu
swReadOnly.setChecked(sReadOnly);
swSuggestions.setChecked(sSuggestions);
+ swAccessoryView.setChecked(sAccessoryView);
+ swStorageAccessFramework.setChecked(sStorageAccessFramework);
swAutoSave.setChecked(sAutoSave);
swIgnoreBackButton.setChecked(sIgnoreBackButton);
swSplitText.setChecked(sSplitText);
@@ -215,6 +223,21 @@ public class SettingsFragment extends Fragment implements NumberPickerDialog.INu
}
});
+ swAccessoryView.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
+ @Override
+ public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
+ PreferenceHelper.setUseAccessoryView(getActivity(), isChecked);
+ ((MainActivity) getActivity()).aPreferenceValueWasChanged(PreferenceChangeType.ACCESSORY_VIEW);
+ }
+ });
+
+ swStorageAccessFramework.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
+ @Override
+ public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
+ PreferenceHelper.setUseStorageAccessFramework(getActivity(), isChecked);
+ }
+ });
+
swAutoSave.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
diff --git a/libraries/sharedCode/src/main/java/sharedcode/turboeditor/root/LinuxShell.java b/libraries/sharedCode/src/main/java/sharedcode/turboeditor/root/LinuxShell.java
deleted file mode 100644
index 8162dd2..0000000
--- a/libraries/sharedCode/src/main/java/sharedcode/turboeditor/root/LinuxShell.java
+++ /dev/null
@@ -1,77 +0,0 @@
-/*
- * Copyright (C) 2014 Vlad Mihalachi
- *
- * This file is part of Turbo Editor.
- *
- * Turbo Editor is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * Turbo Editor is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-
-package sharedcode.turboeditor.root;
-
-import android.util.Log;
-
-import java.io.BufferedReader;
-import java.io.DataOutputStream;
-import java.io.IOException;
-import java.io.InputStreamReader;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-public class LinuxShell {
-
- private static final String UNIX_ESCAPE_EXPRESSION = "(\\(|\\)|\\[|\\]|\\s|\'|\"|`|\\{|\\}|&|\\\\|\\?)";
-
- public static String getCommandLineString(String input) {
- return input.replaceAll(UNIX_ESCAPE_EXPRESSION, "\\\\$1");
- }
-
- public static BufferedReader execute(String cmd) {
- BufferedReader reader = null;
- try {
- Process process = Runtime.getRuntime().exec("su");
- DataOutputStream os = new DataOutputStream(
- process.getOutputStream());
- os.writeBytes(cmd + "\n");
- os.writeBytes("exit\n");
- reader = new BufferedReader(new InputStreamReader(
- process.getInputStream()));
- String err = (new BufferedReader(new InputStreamReader(
- process.getErrorStream()))).readLine();
- os.flush();
-
- if (process.waitFor() != 0 || (!"".equals(err) && null != err)
- && containsIllegals(err) != true) {
- Log.e("Root Error, cmd: " + cmd, err);
- return null;
- }
- return reader;
-
- } catch (IOException e) {
- e.printStackTrace();
- } catch (InterruptedException e) {
- e.printStackTrace();
- } catch (Exception e) {
- e.printStackTrace();
- }
- return null;
- }
-
- public static boolean containsIllegals(String toExamine) {
- // checks for "+" sign so the program doesn't throw an error when its
- // not erroring.
- Pattern pattern = Pattern.compile("[+]");
- Matcher matcher = pattern.matcher(toExamine);
- return matcher.find();
- }
-}
diff --git a/libraries/sharedCode/src/main/java/sharedcode/turboeditor/root/RootUtils.java b/libraries/sharedCode/src/main/java/sharedcode/turboeditor/root/RootUtils.java
deleted file mode 100644
index d0c30bf..0000000
--- a/libraries/sharedCode/src/main/java/sharedcode/turboeditor/root/RootUtils.java
+++ /dev/null
@@ -1,101 +0,0 @@
-/*
- * Copyright (C) 2014 Vlad Mihalachi
- *
- * This file is part of Turbo Editor.
- *
- * Turbo Editor is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * Turbo Editor is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-
-/**
- * 920 Text Editor is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * 920 Text Editor is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with 920 Text Editor. If not, see .
- */
-
-package sharedcode.turboeditor.root;
-
-import android.content.Context;
-
-import org.apache.commons.io.FileUtils;
-import org.sufficientlysecure.rootcommands.Shell;
-import org.sufficientlysecure.rootcommands.Toolbox;
-
-import java.io.BufferedReader;
-import java.io.File;
-import java.util.ArrayList;
-import java.util.Collections;
-
-
-public class RootUtils {
-
- public static void writeFile(Context context, String path, String text, String encoding, boolean isRoot) throws Exception {
- File file = new File(path);
- if (!file.canWrite() && isRoot) {
- File appFolder = context.getFilesDir();
- File tempFile = new File(appFolder, "temp.root.file");
- if (!tempFile.exists())
- tempFile.createNewFile();
- FileUtils.write(tempFile, text, encoding);
- Shell shell = Shell.startRootShell();
- Toolbox tb = new Toolbox(shell);
- String mount = tb.getFilePermissions(path);
- tb.copyFile(tempFile.getAbsolutePath(), path, true, false);
- tb.setFilePermissions(path, mount);
- tempFile.delete();
- } else {
- FileUtils.write(file,
- text,
- encoding);
- }
- }
-
- public static ArrayList getFileList(String path, boolean runAtRoot) {
- ArrayList filesList = new ArrayList();
- if (runAtRoot == false) {
- File base = new File(path);
- File[] files = base.listFiles();
- if (files == null)
- return null;
- Collections.addAll(filesList, files);
- } else {
- BufferedReader reader = null; //errReader = null;
- try {
- LinuxShell.execute("ls -a " + LinuxShell.getCommandLineString(path));
- if (reader == null)
- return null;
-
- File f;
- String line;
- while ((line = reader.readLine()) != null) {
- f = new File(line.substring(2));
- filesList.add(f);
- }
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
-
- return filesList;
- }
-
-}
\ No newline at end of file
diff --git a/libraries/sharedCode/src/main/java/sharedcode/turboeditor/task/SaveFileTask.java b/libraries/sharedCode/src/main/java/sharedcode/turboeditor/task/SaveFileTask.java
index 0819b6a..203de63 100644
--- a/libraries/sharedCode/src/main/java/sharedcode/turboeditor/task/SaveFileTask.java
+++ b/libraries/sharedCode/src/main/java/sharedcode/turboeditor/task/SaveFileTask.java
@@ -19,42 +19,48 @@
package sharedcode.turboeditor.task;
+import android.net.Uri;
import android.os.AsyncTask;
+import android.os.ParcelFileDescriptor;
+import android.text.TextUtils;
import android.widget.Toast;
-import org.sufficientlysecure.rootcommands.Shell;
-import org.sufficientlysecure.rootcommands.Toolbox;
+import com.spazedog.lib.rootfw4.RootFW;
+import com.spazedog.lib.rootfw4.utils.File;
-import java.io.File;
+import org.apache.commons.io.FileUtils;
+
+import java.io.FileOutputStream;
import java.io.IOException;
-import java.util.concurrent.TimeoutException;
+import java.nio.charset.Charset;
import sharedcode.turboeditor.R;
import sharedcode.turboeditor.activity.MainActivity;
-import sharedcode.turboeditor.root.RootUtils;
+import sharedcode.turboeditor.util.Device;
+import sharedcode.turboeditor.util.GreatUri;
public class SaveFileTask extends AsyncTask {
private final MainActivity activity;
- private final String filePath;
- private final String text;
+ private final GreatUri uri;
+ private final String newContent;
private final String encoding;
- private File file;
private String message;
private String positiveMessage;
+ private SaveFileInterface mCompletionHandler;
- public SaveFileTask(MainActivity activity, String filePath, String text, String encoding) {
+ public SaveFileTask(MainActivity activity, GreatUri uri, String newContent, String encoding, SaveFileInterface mCompletionHandler) {
this.activity = activity;
- this.filePath = filePath;
- this.text = text;
+ this.uri = uri;
+ this.newContent = newContent;
this.encoding = encoding;
+ this.mCompletionHandler = mCompletionHandler;
}
@Override
protected void onPreExecute() {
super.onPreExecute();
- file = new File(filePath);
- positiveMessage = String.format(activity.getString(R.string.file_saved_with_success), file.getName());
+ positiveMessage = String.format(activity.getString(R.string.file_saved_with_success), uri.getFileName());
}
/**
@@ -64,27 +70,29 @@ public class SaveFileTask extends AsyncTask {
protected Void doInBackground(final Void... voids) {
try {
-
- if (!file.exists()) {
- file.getParentFile().mkdirs();
- file.createNewFile();
- }
-
- boolean isRoot = false;
- if (!file.canWrite()) {
- try {
- Shell shell = null;
- shell = Shell.startRootShell();
- Toolbox tb = new Toolbox(shell);
- isRoot = tb.isRootAccessGiven();
- } catch (IOException | TimeoutException e) {
- e.printStackTrace();
- isRoot = false;
+ String filePath = uri.getFilePath();
+ // if the uri has no path
+ if (TextUtils.isEmpty(filePath)) {
+ writeUri(uri.getUri(), newContent, encoding);
+ } else {
+ if (uri.isRootRequired()) {
+ if (RootFW.connect()) {
+ File file = RootFW.getFile(filePath);
+ file.write(newContent);
+ }
+ }
+ // if we can read the file associated with the uri
+ else {
+ if (Device.hasKitKatApi())
+ writeUri(uri.getUri(), newContent, encoding);
+ else {
+ FileUtils.write(new java.io.File(filePath),
+ newContent,
+ encoding);
+ }
}
}
- RootUtils.writeFile(activity, file.getAbsolutePath(), text, encoding, isRoot);
-
message = positiveMessage;
} catch (Exception e) {
message = e.getMessage();
@@ -92,6 +100,14 @@ public class SaveFileTask extends AsyncTask {
return null;
}
+ private void writeUri(Uri uri, String newContent, String encoding) throws IOException {
+ ParcelFileDescriptor pfd = activity.getContentResolver().openFileDescriptor(uri, "w");
+ FileOutputStream fileOutputStream = new FileOutputStream(pfd.getFileDescriptor());
+ fileOutputStream.write(newContent.getBytes(Charset.forName(encoding)));
+ fileOutputStream.close();
+ pfd.close();
+ }
+
/**
* {@inheritDoc}
*/
@@ -99,7 +115,12 @@ public class SaveFileTask extends AsyncTask {
protected void onPostExecute(final Void aVoid) {
super.onPostExecute(aVoid);
Toast.makeText(activity, message, Toast.LENGTH_LONG).show();
- if (message.equals(positiveMessage))
- activity.savedAFile(filePath);
+
+ if (mCompletionHandler != null)
+ mCompletionHandler.fileSaved(message.equals(positiveMessage));
+ }
+
+ public interface SaveFileInterface {
+ public void fileSaved(Boolean success);
}
}
\ No newline at end of file
diff --git a/libraries/sharedCode/src/main/java/sharedcode/turboeditor/texteditor/EditTextPadding.java b/libraries/sharedCode/src/main/java/sharedcode/turboeditor/texteditor/EditTextPadding.java
index b5de286..1e33b2f 100644
--- a/libraries/sharedCode/src/main/java/sharedcode/turboeditor/texteditor/EditTextPadding.java
+++ b/libraries/sharedCode/src/main/java/sharedcode/turboeditor/texteditor/EditTextPadding.java
@@ -21,6 +21,7 @@ package sharedcode.turboeditor.texteditor;
import android.content.Context;
+import sharedcode.turboeditor.preferences.PreferenceHelper;
import sharedcode.turboeditor.util.PixelDipConverter;
public class EditTextPadding {
@@ -29,6 +30,11 @@ public class EditTextPadding {
return (int) PixelDipConverter.convertDpToPixel(5, context);
}
+ public static int getPaddingBottom(Context context) {
+ boolean useAccessoryView = PreferenceHelper.getUseAccessoryView(context);
+ return (int) PixelDipConverter.convertDpToPixel(useAccessoryView ? 50 : 0, context);
+ }
+
public static int getPaddingWithLineNumbers(Context context, float fontSize) {
return (int) PixelDipConverter.convertDpToPixel(fontSize * 2f, context);
}
diff --git a/libraries/sharedCode/src/main/java/sharedcode/turboeditor/texteditor/FileUtils.java b/libraries/sharedCode/src/main/java/sharedcode/turboeditor/texteditor/FileUtils.java
index 54b3ccd..1e9430e 100644
--- a/libraries/sharedCode/src/main/java/sharedcode/turboeditor/texteditor/FileUtils.java
+++ b/libraries/sharedCode/src/main/java/sharedcode/turboeditor/texteditor/FileUtils.java
@@ -23,16 +23,15 @@ import org.mozilla.universalchardet.UniversalDetector;
import java.io.File;
import java.io.FileInputStream;
+import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
public class FileUtils {
- public static String getDetectedEncoding(File file) {
- InputStream is = null;
+ public static String getDetectedEncoding(InputStream is) {
String encoding = null;
try {
- is = new FileInputStream(file);
UniversalDetector detector = new UniversalDetector(null);
byte[] buf = new byte[4096];
int nread;
@@ -55,4 +54,8 @@ public class FileUtils {
}
return encoding;
}
+
+ public static String getDetectedEncoding(File file) throws FileNotFoundException {
+ return getDetectedEncoding(new FileInputStream(file));
+ }
}
diff --git a/libraries/sharedCode/src/main/java/sharedcode/turboeditor/texteditor/PageSystem.java b/libraries/sharedCode/src/main/java/sharedcode/turboeditor/texteditor/PageSystem.java
index 6dc47cd..8b11de4 100644
--- a/libraries/sharedCode/src/main/java/sharedcode/turboeditor/texteditor/PageSystem.java
+++ b/libraries/sharedCode/src/main/java/sharedcode/turboeditor/texteditor/PageSystem.java
@@ -20,11 +20,7 @@
package sharedcode.turboeditor.texteditor;
import android.content.Context;
-import android.support.annotation.Nullable;
-import org.apache.commons.io.FileUtils;
-
-import java.io.File;
import java.util.LinkedList;
import java.util.List;
@@ -37,29 +33,24 @@ public class PageSystem {
private int currentPage = 0;
private PageSystemInterface pageSystemInterface;
- public PageSystem(Context context, PageSystemInterface pageSystemInterface, String text, @Nullable File file) {
+ public PageSystem(Context context, PageSystemInterface pageSystemInterface, String text) {
- final int charForPage = 15000;
- final int MAX_KBs_WITHOUT_PAGE_SYSTEM = 50;
+ final int charForPage = 20000;
+ final int firstPageChars = 50000;
this.pageSystemInterface = pageSystemInterface;
pages = new LinkedList<>();
- final boolean dimensionOverLimit;
- if(file != null && file.exists() && file.isFile())
- dimensionOverLimit = FileUtils.sizeOf(file) >= MAX_KBs_WITHOUT_PAGE_SYSTEM * FileUtils.ONE_KB;
- else
- dimensionOverLimit = false;
-
int i = 0;
int to;
int nextIndexOfReturn;
final int textLength = text.length();
boolean pageSystemEnabled = PreferenceHelper.getSplitText(context);
- if (pageSystemEnabled && dimensionOverLimit) {
+ if (pageSystemEnabled) {
while (i < textLength) {
- to = i + charForPage;
+ // first page is longer
+ to = i + (i == 0 ? firstPageChars : charForPage);
nextIndexOfReturn = text.indexOf("\n", to);
if (nextIndexOfReturn > to) to = nextIndexOfReturn;
if (to > text.length()) to = text.length();
diff --git a/libraries/sharedCode/src/main/java/sharedcode/turboeditor/util/AccessStorageApi.java b/libraries/sharedCode/src/main/java/sharedcode/turboeditor/util/AccessStorageApi.java
index a9d3bed..7a24e76 100644
--- a/libraries/sharedCode/src/main/java/sharedcode/turboeditor/util/AccessStorageApi.java
+++ b/libraries/sharedCode/src/main/java/sharedcode/turboeditor/util/AccessStorageApi.java
@@ -31,6 +31,8 @@ import android.os.Environment;
import android.provider.DocumentsContract;
import android.provider.MediaStore;
+import org.apache.commons.io.FilenameUtils;
+
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
@@ -87,64 +89,113 @@ public class AccessStorageApi {
@SuppressLint("NewApi")
public static String getPath(final Context context, final Uri uri) {
- final boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;
+ String path = "";
- // DocumentProvider
- if (isKitKat && DocumentsContract.isDocumentUri(context, uri)) {
- // ExternalStorageProvider
- if (isExternalStorageDocument(uri)) {
- final String docId = DocumentsContract.getDocumentId(uri);
- final String[] split = docId.split(":");
- final String type = split[0];
+ if (uri == null || uri.equals(Uri.EMPTY))
+ return "";
- if ("primary".equalsIgnoreCase(type)) {
- return Environment.getExternalStorageDirectory() + "/" + split[1];
+ try {
+ final boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;
+ // DocumentProvider
+ if (isKitKat && DocumentsContract.isDocumentUri(context, uri)) {
+ if (isTurboDocument(uri)) {
+ final String docId = DocumentsContract.getDocumentId(uri);
+ final String[] split = docId.split(":");
+ path = "/" + split[1];
}
+ // ExternalStorageProvider
+ else if (isExternalStorageDocument(uri)) {
- // TODO handle non-primary volumes
- }
- // DownloadsProvider
- else if (isDownloadsDocument(uri)) {
+ final String docId = DocumentsContract.getDocumentId(uri);
+ final String[] split = docId.split(":");
+ final String type = split[0];
- final String id = DocumentsContract.getDocumentId(uri);
- final Uri contentUri = ContentUris.withAppendedId(
- Uri.parse("content://downloads/public_downloads"), Long.valueOf(id));
+ if ("primary".equalsIgnoreCase(type)) {
+ path = Environment.getExternalStorageDirectory() + "/" + split[1];
+ }
- return getDataColumn(context, contentUri, null, null);
- }
- // MediaProvider
- else if (isMediaDocument(uri)) {
- final String docId = DocumentsContract.getDocumentId(uri);
- final String[] split = docId.split(":");
- final String type = split[0];
-
- Uri contentUri = null;
- if ("image".equals(type)) {
- contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
- } else if ("video".equals(type)) {
- contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
- } else if ("audio".equals(type)) {
- contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
+ // TODO handle non-primary volumes
}
+ // DownloadsProvider
+ else if (isDownloadsDocument(uri)) {
- final String selection = "_id=?";
- final String[] selectionArgs = new String[]{
- split[1]
- };
+ final String id = DocumentsContract.getDocumentId(uri);
+ final Uri contentUri = ContentUris.withAppendedId(
+ Uri.parse("content://downloads/public_downloads"), Long.valueOf(id));
- return getDataColumn(context, contentUri, selection, selectionArgs);
+ path = getDataColumn(context, contentUri, null, null);
+ }
+ // MediaProvider
+ else if (isMediaDocument(uri)) {
+ final String docId = DocumentsContract.getDocumentId(uri);
+ final String[] split = docId.split(":");
+ final String type = split[0];
+
+ Uri contentUri = null;
+ if ("image".equals(type)) {
+ contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
+ } else if ("video".equals(type)) {
+ contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
+ } else if ("audio".equals(type)) {
+ contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
+ }
+
+ final String selection = "_id=?";
+ final String[] selectionArgs = new String[]{
+ split[1]
+ };
+
+ path = getDataColumn(context, contentUri, selection, selectionArgs);
+ }
}
- }
- // MediaStore (and general)
- else if ("content".equalsIgnoreCase(uri.getScheme())) {
- return getDataColumn(context, uri, null, null);
- }
- // File
- else if ("file".equalsIgnoreCase(uri.getScheme())) {
- return uri.getPath();
+ // MediaStore (and general)
+ else if ("content".equalsIgnoreCase(uri.getScheme())) {
+ path = getDataColumn(context, uri, null, null);
+ }
+ // File
+ else if ("file".equalsIgnoreCase(uri.getScheme())) {
+ path = uri.getPath();
+ }
+ } catch (Exception ex) {
+ return "";
}
- return null;
+
+ return path;
+ }
+
+ public static String getName(Context context, Uri uri)
+ {
+
+ if (uri == null || uri.equals(Uri.EMPTY))
+ return "";
+
+ String fileName = "";
+ try {
+ String scheme = uri.getScheme();
+ if (scheme.equals("file")) {
+ fileName = uri.getLastPathSegment();
+ }
+ else if (scheme.equals("content")) {
+ String[] proj = { MediaStore.Images.Media.DISPLAY_NAME };
+ Cursor cursor = context.getContentResolver().query(uri, proj, null, null, null);
+ if (cursor != null && cursor.getCount() != 0) {
+ int columnIndex = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DISPLAY_NAME);
+ cursor.moveToFirst();
+ fileName = cursor.getString(columnIndex);
+ }
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+ } catch (Exception ex) {
+ return "";
+ }
+ return fileName;
+ }
+
+ public static String getExtension(Context context, Uri uri) {
+ return FilenameUtils.getExtension(getName(context, uri));
}
/**
@@ -205,4 +256,11 @@ public class AccessStorageApi {
return "com.android.providers.media.documents".equals(uri.getAuthority());
}
+ /**
+ * @param uri The Uri to check.
+ * @return Whether the Uri authority is Turbo Storage.
+ */
+ public static boolean isTurboDocument(Uri uri) {
+ return "sharedcode.turboeditor.util.documents".equals(uri.getAuthority());
+ }
}
diff --git a/libraries/sharedCode/src/main/java/sharedcode/turboeditor/util/Device.java b/libraries/sharedCode/src/main/java/sharedcode/turboeditor/util/Device.java
index 3be1c54..8d6fdce 100644
--- a/libraries/sharedCode/src/main/java/sharedcode/turboeditor/util/Device.java
+++ b/libraries/sharedCode/src/main/java/sharedcode/turboeditor/util/Device.java
@@ -53,6 +53,14 @@ public class Device {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;
}
+ /**
+ * @return {@code true} if device is running
+ * {@link android.os.Build.VERSION_CODES#KITKAT KitKat} {@code false} otherwise.
+ */
+ public static boolean isKitKatApi() {
+ return Build.VERSION.SDK_INT == Build.VERSION_CODES.KITKAT;
+ }
+
/**
* @return {@code true} if device is running
* {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2 Jelly Bean 4.3} or higher, {@code false} otherwise.
diff --git a/libraries/sharedCode/src/main/java/sharedcode/turboeditor/util/GreatUri.java b/libraries/sharedCode/src/main/java/sharedcode/turboeditor/util/GreatUri.java
new file mode 100644
index 0000000..f58e9ca
--- /dev/null
+++ b/libraries/sharedCode/src/main/java/sharedcode/turboeditor/util/GreatUri.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2014 Vlad Mihalachi
+ *
+ * This file is part of Turbo Editor.
+ *
+ * Turbo Editor is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Turbo Editor is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package sharedcode.turboeditor.util;
+
+import android.net.Uri;
+
+import org.apache.commons.lang3.builder.EqualsBuilder;
+import org.apache.commons.lang3.builder.HashCodeBuilder;
+
+import java.io.File;
+
+/**
+ * Created by mac on 19/03/15.
+ */
+public class GreatUri {
+ private Uri uri;
+ private String filePath;
+ private String fileName;
+ private boolean isRootRequired;
+
+ public GreatUri(Uri uri, String filePath, String fileName, boolean isRootRequired) {
+ this.uri = uri;
+ this.filePath = filePath;
+ this.fileName = fileName;
+ this.isRootRequired = isRootRequired;
+ }
+
+ @Override
+ public int hashCode() {
+ return new HashCodeBuilder(17, 31). // two randomly chosen prime numbers
+ // if deriving: appendSuper(super.hashCode()).
+ append(uri).
+ toHashCode();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof GreatUri))
+ return false;
+ if (obj == this)
+ return true;
+
+ GreatUri rhs = (GreatUri) obj;
+ return new EqualsBuilder().
+ // if deriving: appendSuper(super.equals(obj)).
+ append(uri, rhs.uri).
+ isEquals();
+ }
+
+ public Uri getUri() {
+ return uri;
+ }
+
+ public void setUri(Uri uri) {
+ this.uri = uri;
+ }
+
+ public String getFilePath() {
+ return filePath;
+ }
+
+ public void setFilePath(String filePath) {
+ this.filePath = filePath;
+ }
+
+ public String getParentFolder() {
+ return new File(filePath).getParent();
+ }
+
+ public String getFileName() {
+ return fileName;
+ }
+
+ public void setFileName(String fileName) {
+ this.fileName = fileName;
+ }
+
+ public boolean isRootRequired() {
+ return isRootRequired;
+ }
+
+ public void setRootRequired(boolean isRootRequired) {
+ this.isRootRequired = isRootRequired;
+ }
+}
diff --git a/libraries/sharedCode/src/main/res/layout/activity_home.xml b/libraries/sharedCode/src/main/res/layout/activity_home.xml
index 6d71c6b..ba6363a 100644
--- a/libraries/sharedCode/src/main/res/layout/activity_home.xml
+++ b/libraries/sharedCode/src/main/res/layout/activity_home.xml
@@ -58,8 +58,7 @@
android:scrollbars="vertical"
android:fillViewport="true"
android:id="@id/vertical_scroll"
- android:background="@null"
- android:layout_marginBottom="50dp">
+ android:background="@null">
+ android:fillViewport="false"
+ android:id="@+id/parent_accessory_view">
@@ -32,6 +32,5 @@
android:layout_height="match_parent"
android:inputType="text"
android:imeOptions="actionDone"
- android:padding="10dp"
/>
diff --git a/libraries/sharedCode/src/main/res/layout/fragment_settings.xml b/libraries/sharedCode/src/main/res/layout/fragment_settings.xml
index 103c76b..3018caa 100644
--- a/libraries/sharedCode/src/main/res/layout/fragment_settings.xml
+++ b/libraries/sharedCode/src/main/res/layout/fragment_settings.xml
@@ -176,6 +176,34 @@
android:background="?selectableItemBackground"
android:textColor="@color/navigation_drawer_button_text_color_inverted"/>
+
+
+
+
+
+
diff --git a/libraries/sharedCode/src/main/res/menu/fragment_editor_search.xml b/libraries/sharedCode/src/main/res/menu/fragment_editor_search.xml
index 70adc42..a0bc024 100644
--- a/libraries/sharedCode/src/main/res/menu/fragment_editor_search.xml
+++ b/libraries/sharedCode/src/main/res/menu/fragment_editor_search.xml
@@ -36,7 +36,7 @@
-
-
-
- 隐藏上下文|打开上下文
-关闭
+ 关闭
关于
开源 的应用。
Copyright 2013-2014 Vlad Mihalachi. All Rights Reserved.
diff --git a/libraries/sharedCode/src/main/res/values-zh-rCN/strings_donation.xml b/libraries/sharedCode/src/main/res/values-zh-rCN/strings_donation.xml
index 16cbc5e..e117db6 100644
--- a/libraries/sharedCode/src/main/res/values-zh-rCN/strings_donation.xml
+++ b/libraries/sharedCode/src/main/res/values-zh-rCN/strings_donation.xml
@@ -20,18 +20,15 @@
-->
捐赠
- Donate to developer
- open source app.
- You can show your appreciation and support development by donating:
- ]]>
- You\'ve donated for this item already.
- An ice cream
- Cup of coffee
- Electricity bills
- The right pillow
- Solid-state drive
- Sound system
+ 捐赠开发者
+ 开放源码 的应用程序。你想表达你的感谢或支持开发,可以捐赠我们:]]>
+ 你已经为这个项目捐赠过了。
+ 一支冰淇淋
+ 一杯咖啡
+ 电费账单
+ 舒适的枕头
+ 固态硬盘
+ 音响系统
Failed to setup in-app-billing service!
Notice that Google is not responsible for that payments method.
diff --git a/libraries/sharedCode/src/main/res/values-zh-rTW/strings.xml b/libraries/sharedCode/src/main/res/values-zh-rTW/strings.xml
index adabaf0..47d8789 100644
--- a/libraries/sharedCode/src/main/res/values-zh-rTW/strings.xml
+++ b/libraries/sharedCode/src/main/res/values-zh-rTW/strings.xml
@@ -79,4 +79,7 @@
Dark theme
Black theme
View markdown result
+ Accessory view
+ The file cannot be renamed
+ Use kitkat file selection
diff --git a/libraries/sharedCode/src/main/res/values/ids.xml b/libraries/sharedCode/src/main/res/values/ids.xml
index e32e613..7232f4f 100644
--- a/libraries/sharedCode/src/main/res/values/ids.xml
+++ b/libraries/sharedCode/src/main/res/values/ids.xml
@@ -22,6 +22,7 @@
+
@@ -84,6 +85,8 @@
+
+
diff --git a/libraries/sharedCode/src/main/res/values/strings.xml b/libraries/sharedCode/src/main/res/values/strings.xml
index 2a9f3c3..57afff9 100644
--- a/libraries/sharedCode/src/main/res/values/strings.xml
+++ b/libraries/sharedCode/src/main/res/values/strings.xml
@@ -79,4 +79,7 @@
Dark theme
Black theme
View markdown result
+ Accessory view
+ The file cannot be renamed
+ Use the "Storage Access Framework"
diff --git a/settings.gradle b/settings.gradle
index 5537738..0cdef46 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -17,4 +17,4 @@
* along with this program. If not, see .
*/
-include ':app', ':libraries:RootCommands', ':libraries:FloatingActionButton', ':libraries:sharedCode', ':app-pro'
+include ':app', ':libraries:FloatingActionButton', ':libraries:sharedCode', ':app-pro'