/*
* Copyright (C) 2014 Arpit Khurana <arpitkh96@gmail.com>
*
* This file is part of Amaze File Manager.
*
* Amaze File Manager 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.
*
* This program 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 <http://www.gnu.org/licenses/>.
*/
package com.amaze.filemanager.filesystem;
import android.content.Context;
import android.content.SharedPreferences;
import android.net.Uri;
import android.preference.PreferenceManager;
import android.support.v4.provider.DocumentFile;
import android.util.Log;
import com.amaze.filemanager.activities.MainActivity;
import com.amaze.filemanager.exceptions.RootNotPermittedException;
import com.amaze.filemanager.utils.Futils;
import com.amaze.filemanager.utils.OpenMode;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import eu.chainfire.libsuperuser.Shell;
public class RootHelper {
/**
* Runs the command and stores output in a list. The listener is set on the handler
* thread {@link MainActivity#handlerThread} thus any code run in callback must be thread safe.
* Command is run from the root context (u:r:SuperSU0)
*
* @param cmd the command
* @return a list of results. Null only if the command passed is a blocking call or no output is
* there for the command passed
* @throws RootNotPermittedException
*/
public static ArrayList<String> runShellCommand(String cmd) throws RootNotPermittedException {
if (MainActivity.shellInteractive == null || !MainActivity.shellInteractive.isRunning())
throw new RootNotPermittedException();
final ArrayList<String> result = new ArrayList<>();
// callback being called on a background handler thread
MainActivity.shellInteractive.addCommand(cmd, 0, new Shell.OnCommandResultListener() {
@Override
public void onCommandResult(int commandCode, int exitCode, List<String> output) {
for (String line : output) {
result.add(line);
}
}
});
MainActivity.shellInteractive.waitForIdle();
return result;
}
/**
* Runs the command on an interactive shell. Provides a listener for the caller to interact.
* The caller is executed on a worker background thread, hence any calls from the callback
* should be thread safe.
* Command is run from superuser context (u:r:SuperSU0)
*
* @param cmd the command
* @param callback
* @return a list of results. Null only if the command passed is a blocking call or no output is
* there for the command passed
* @throws RootNotPermittedException
*/
public static void runShellCommand(String cmd, Shell.OnCommandResultListener callback)
throws RootNotPermittedException {
if (MainActivity.shellInteractive == null || !MainActivity.shellInteractive.isRunning())
throw new RootNotPermittedException();
MainActivity.shellInteractive.addCommand(cmd, 0, callback);
MainActivity.shellInteractive.waitForIdle();
}
/**
* @param cmd the command
* @return a list of results. Null only if the command passed is a blocking call or no output is
* there for the command passed
* @throws RootNotPermittedException
* @deprecated Use {@link #runShellCommand(String)} instead which runs command on an interactive shell
* <p>
* Runs the command and stores output in a list. The listener is set on the caller thread,
* thus any code run in callback must be thread safe.
* Command is run from a third-party level context (u:r:init_shell0)
* Not callback supported as the shell is not interactive
*/
public static List<String> runNonRootShellCommand(String cmd) {
return Shell.SH.run(cmd);
}
public static String getCommandLineString(String input) {
return input.replaceAll(UNIX_ESCAPE_EXPRESSION, "\\\\$1");
}
private static final String UNIX_ESCAPE_EXPRESSION = "(\\(|\\)|\\[|\\]|\\s|\'|\"|`|\\{|\\}|&|\\\\|\\?)";
/**
* Loads files in a path using basic filesystem callbacks
*
* @param path the path
* @param showHidden
* @return
*/
public static ArrayList<BaseFile> getFilesList(String path, boolean showHidden) {
File f = new File(path);
ArrayList<BaseFile> files = new ArrayList<>();
try {
if (f.exists() && f.isDirectory()) {
for (File x : f.listFiles()) {
long size = 0;
if (!x.isDirectory()) size = x.length();
BaseFile baseFile = new BaseFile(x.getPath(), parseFilePermission(x),
x.lastModified(), size, x.isDirectory());
baseFile.setName(x.getName());
baseFile.setMode(OpenMode.FILE);
if (showHidden) {
files.add(baseFile);
} else {
if (!x.isHidden()) {
files.add(baseFile);
}
}
}
}
} catch (Exception e) {
}
return files;
}
/**
* Traverse to a specified path in OTG
*
* @param path
* @param context
* @param createRecursive flag used to determine whether to create new file while traversing to path,
* in case path is not present. Notably useful in opening an output stream.
* @return
*/
public static DocumentFile getDocumentFile(String path, Context context, boolean createRecursive) {
SharedPreferences manager = PreferenceManager.getDefaultSharedPreferences(context);
String rootUriString = manager.getString(MainActivity.KEY_PREF_OTG, null);
// start with root of SD card and then parse through document tree.
DocumentFile rootUri = DocumentFile.fromTreeUri(context, Uri.parse(rootUriString));
String[] parts = path.split("/");
for (int i = 0; i < parts.length; i++) {
if (path.equals("otg:/")) break;
if (parts[i].equals("otg:") || parts[i].equals("")) continue;
Log.d(context.getClass().getSimpleName(), "Currently at: " + parts[i]);
// iterating through the required path to find the end point
DocumentFile nextDocument = rootUri.findFile(parts[i]);
if (createRecursive) {
if (nextDocument == null || !nextDocument.exists()) {
nextDocument = rootUri.createFile(parts[i].substring(parts[i].lastIndexOf(".")), parts[i]);
Log.d(context.getClass().getSimpleName(), "NOT FOUND! File created: " + parts[i]);
}
}
rootUri = nextDocument;
}
return rootUri;
}
public static BaseFile generateBaseFile(File x, boolean showHidden) {
long size = 0;
if (!x.isDirectory())
size = x.length();
BaseFile baseFile = new BaseFile(x.getPath(), parseFilePermission(x), x.lastModified(), size, x.isDirectory());
baseFile.setName(x.getName());
baseFile.setMode(OpenMode.FILE);
if (showHidden) {
return (baseFile);
} else if (!x.isHidden()) {
return (baseFile);
}
return null;
}
public static BaseFile generateBaseFile(DocumentFile file, boolean showHidden) {
long size = 0;
if (!file.isDirectory())
size = file.length();
BaseFile baseFile = new BaseFile(file.getName(), parseDocumentFilePermission(file),
file.lastModified(), size, file.isDirectory());
baseFile.setName(file.getName());
baseFile.setMode(OpenMode.OTG);
return baseFile;
}
public static String parseFilePermission(File f) {
String per = "";
if (f.canRead()) {
per = per + "r";
}
if (f.canWrite()) {
per = per + "w";
}
if (f.canExecute()) {
per = per + "x";
}
return per;
}
public static String parseDocumentFilePermission(DocumentFile file) {
String per = "";
if (file.canRead()) {
per = per + "r";
}
if (file.canWrite()) {
per = per + "w";
}
if (file.canWrite()) {
per = per + "x";
}
return per;
}
/**
* Whether a file exist at a specified path. We try to reload a list and conform from that list
* of parent's children that the file we're looking for is there or not.
*
* @param path
* @return
* @throws RootNotPermittedException
*/
public static boolean fileExists(String path) throws RootNotPermittedException {
File f = new File(path);
String p = f.getParent();
if (p != null && p.length() > 0) {
ArrayList<BaseFile> ls = getFilesList(p, true, true, new GetModeCallBack() {
@Override
public void getMode(OpenMode mode) {
}
});
for (BaseFile strings : ls) {
if (strings.getPath() != null && strings.getPath().equals(path)) {
return true;
}
}
}
return false;
}
static boolean contains(String[] a, String name) {
for (String s : a) {
//Log.e("checking",s);
if (s.equals(name)) return true;
}
return false;
}
/**
* Whether toTest file is directory or not
*
* @param toTest
* @param root
* @param count
* @return TODO: Avoid parsing ls
*/
public static boolean isDirectory(String toTest, boolean root, int count)
throws RootNotPermittedException {
File f = new File(toTest);
String name = f.getName();
String p = f.getParent();
if (p != null && p.length() > 0) {
ArrayList<String> ls = runShellCommand("ls -l " + p);
for (String s : ls) {
if (contains(s.split(" "), name)) {
try {
BaseFile path = Futils.parseName(s);
if (path.getPermission().trim().startsWith("d")) return true;
else if (path.getPermission().trim().startsWith("l")) {
if (count > 5)
return f.isDirectory();
else
return isDirectory(path.getLink().trim(), root, ++count);
} else return f.isDirectory();
} catch (Exception e) {
e.printStackTrace();
}
break;
}
}
}
return f.isDirectory();
}
private static boolean isDirectory(BaseFile path) {
return path.getPermission().startsWith("d") || new File(path.getPath()).isDirectory();
}
/**
* Callback to setting type of file to handle, while loading list of files
*/
public interface GetModeCallBack {
void getMode(OpenMode mode);
}
/**
* Get a list of files using shell, supposing the path is not a SMB/OTG/Custom (*.apk/images)
*
* @param path
* @param root whether root is available or not
* @param showHidden to show hidden files
* @param getModeCallBack callback to set the type of file
* @return TODO: Avoid parsing ls
*/
public static ArrayList<BaseFile> getFilesList(String path, boolean root, boolean showHidden,
GetModeCallBack getModeCallBack)
throws RootNotPermittedException {
//String p = " ";
OpenMode mode = OpenMode.FILE;
//if (showHidden) p = "a ";
ArrayList<BaseFile> files = new ArrayList<>();
ArrayList<String> ls;
if (root) {
// we're rooted and we're trying to load file with superuser
if (!path.startsWith("/storage") && !path.startsWith("/sdcard")) {
// we're at the root directories, superuser is required!
String cpath = getCommandLineString(path);
//ls = Shell.SU.run("ls -l " + cpath);
ls = runShellCommand("ls -l " + (showHidden ? "-a " : "") + "\"" + cpath + "\"");
if (ls != null) {
for (int i = 0; i < ls.size(); i++) {
String file = ls.get(i);
if (!file.contains("Permission denied"))
try {
BaseFile array = Futils.parseName(file);
array.setMode(OpenMode.ROOT);
if (array != null) {
array.setName(array.getPath());
array.setPath(path + "/" + array.getPath());
if (array.getLink().trim().length() > 0) {
boolean isdirectory = isDirectory(array.getLink(), root, 0);
array.setDirectory(isdirectory);
} else array.setDirectory(isDirectory(array));
files.add(array);
}
} catch (Exception e) {
e.printStackTrace();
}
}
mode = OpenMode.ROOT;
}
} else if (Futils.canListFiles(new File(path))) {
// we might as well not require root to load files
files = getFilesList(path, showHidden);
mode = OpenMode.FILE;
} else {
// couldn't load files using native java filesystem callbacks
// maybe the access is not allowed due to android system restrictions, we'll see later
mode = OpenMode.FILE;
files = new ArrayList<>();
}
} else if (Futils.canListFiles(new File(path))) {
// we don't have root, so we're taking a chance to load files using basic java filesystem
files = getFilesList(path, showHidden);
mode = OpenMode.FILE;
} else {
// couldn't load files using native java filesystem callbacks
// maybe the access is not allowed due to android system restrictions, we'll see later
mode = OpenMode.FILE;
files = new ArrayList<>();
}
if (getModeCallBack != null) getModeCallBack.getMode(mode);
return files;
}
}