Compare commits

...

3 Commits

Author SHA1 Message Date
Adrian Malacoda
d9f35737de Rename Turbo-Editor in German language. 2018-06-25 01:31:21 -05:00
Adrian Malacoda
f7a95be11b Need to flush/close output stream, else our data won't actually be written. 2018-06-24 20:17:32 -05:00
Adrian Malacoda
f08edb736d Swap out rootfw for libsu.
THis compiles but I haven't yet tested it. It won't get merged back in until I test it fully.
2018-06-24 19:43:30 -05:00
25 changed files with 40 additions and 6141 deletions

View File

@ -58,8 +58,7 @@ android {
}
dependencies {
compile project(':libraries:sharedCode')
//compile 'com.spazedog.lib:rootfw_gen4:+@aar'
compile 'com.github.topjohnwu:libsu:1.2.0'
compile project(':libraries:FloatingActionButton')
compile 'org.apache.commons:commons-lang3:+'
compile 'com.googlecode.juniversalchardet:juniversalchardet:1.0.3'

View File

@ -68,8 +68,7 @@ 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 com.topjohnwu.superuser.io.SuFileInputStream;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang3.ArrayUtils;
@ -978,10 +977,8 @@ public abstract class MainActivity extends ActionBarActivity implements IHomeAct
encoding = "UTF-8";
// Connect the shared connection
if (RootFW.connect()) {
FileReader reader = RootFW.getFileReader(path);
buffer = new BufferedReader(reader);
}
InputStreamReader reader = new InputStreamReader(new SuFileInputStream(path));
buffer = new BufferedReader(reader);
} else {
boolean autoencoding = PreferenceHelper.getAutoEncoding(MainActivity.this);
@ -1008,9 +1005,6 @@ public abstract class MainActivity extends ActionBarActivity implements IHomeAct
buffer.close();
fileText = stringBuilder.toString();
}
if (isRootRequired)
RootFW.disconnect();
}
@Override

View File

@ -39,7 +39,7 @@ import android.widget.TextView;
import android.widget.Toast;
import com.faizmalkani.floatingactionbutton.FloatingActionButton;
import com.spazedog.lib.rootfw4.RootFW;
import com.topjohnwu.superuser.io.SuFile;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
@ -383,24 +383,22 @@ public class SelectFileActivity extends ActionBarActivity implements SearchView.
currentFolder = tempFolder.getAbsolutePath();
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();
SuFile folder = new SuFile(currentFolder);
SuFile[] files = folder.listFiles();
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), ""));
}
if (files != null) {
for (SuFile file : files) {
if (file.isDirectory()) {
folderDetails.add(new AdapterDetailedList.FileDetail(file.getName(),
getString(R.string.folder),
""));
} else if (!FilenameUtils.isExtension(file.getName().toLowerCase(), unopenableExtensions)
&& file.length() <= Build.MAX_FILE_SIZE * FileUtils.ONE_KB) {
final long fileSize = file.length();
//SimpleDateFormat format = new SimpleDateFormat("MMM dd, yyyy hh:mm a");
//String date = format.format("");
fileDetails.add(new AdapterDetailedList.FileDetail(file.getName(),
FileUtils.byteCountToDisplaySize(fileSize), ""));
}
}
}

View File

@ -24,11 +24,18 @@ import android.view.ViewConfiguration;
import java.lang.reflect.Field;
public class MyApp extends Application {
import com.topjohnwu.superuser.Shell;
public class MyApp extends Shell.ContainerApp {
@Override
public void onCreate() {
super.onCreate();
// Shell initialization
Shell.setFlags(Shell.FLAG_REDIRECT_STDERR);
Shell.verboseLogging(true);
// force to sow the overflow menu icon
try {
ViewConfiguration config = ViewConfiguration.get(this);

View File

@ -25,12 +25,9 @@ import android.os.ParcelFileDescriptor;
import android.text.TextUtils;
import android.widget.Toast;
import com.spazedog.lib.rootfw4.RootFW;
import com.spazedog.lib.rootfw4.Shell;
import com.spazedog.lib.rootfw4.utils.File;
import com.spazedog.lib.rootfw4.utils.Filesystem;
import com.topjohnwu.superuser.io.SuFileOutputStream;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import java.io.FileOutputStream;
import java.io.IOException;
@ -41,6 +38,8 @@ import com.maskyn.fileeditorpro.activity.MainActivity;
import com.maskyn.fileeditorpro.util.Device;
import com.maskyn.fileeditorpro.util.GreatUri;
import android.util.Log;
public class SaveFileTask extends AsyncTask<Void, Void, Void> {
private final MainActivity activity;
@ -73,7 +72,6 @@ public class SaveFileTask extends AsyncTask<Void, Void, Void> {
protected Void doInBackground(final Void... voids) {
boolean isRootNeeded = false;
Shell.Result resultRoot = null;
try {
String filePath = uri.getFilePath();
@ -93,34 +91,13 @@ public class SaveFileTask extends AsyncTask<Void, Void, Void> {
}
// if we can read the file associated with the uri
else {
if (RootFW.connect()) {
Filesystem.Disk systemPart = RootFW.getDisk(uri.getParentFolder());
systemPart.mount(new String[]{"rw"});
File file = RootFW.getFile(uri.getFilePath());
resultRoot = file.writeResult(newContent);
RootFW.disconnect();
}
SuFileOutputStream out = new SuFileOutputStream(uri.getFilePath());
IOUtils.write(newContent, out, encoding);
out.close();
}
}
if (isRootNeeded) {
if (resultRoot != null && resultRoot.wasSuccessful()) {
message = positiveMessage;
}
else if (resultRoot != null) {
message = negativeMessage + " command number: " + resultRoot.getCommandNumber() + " result code: " + resultRoot.getResultCode() + " error lines: " + resultRoot.getString();
}
else
message = negativeMessage;
}
else
message = positiveMessage;
message = positiveMessage;
} catch (Exception e) {
e.printStackTrace();
message = e.getMessage();
@ -155,4 +132,4 @@ public class SaveFileTask extends AsyncTask<Void, Void, Void> {
public interface SaveFileInterface {
void fileSaved(Boolean success);
}
}
}

View File

@ -61,7 +61,7 @@
<string name="codifica">Verschlüsseln</string>
<string name="condividi">Teilen</string>
<string name="info">Informationen</string>
<string name="nome_app_turbo_editor">Turbo-Editor</string>
<string name="nome_app_turbo_editor">Text Editor 8000</string>
<string name="preferenze">Einstellungen</string>
<string name="salva">Speichern</string>
<string name="save_as">Speichern unter</string>

View File

@ -36,5 +36,6 @@ allprojects {
repositories {
jcenter()
google()
maven { url 'https://jitpack.io' }
}
}

View File

@ -1 +0,0 @@
/build

View File

@ -1,55 +0,0 @@
/*
* Copyright (C) 2016 Adrian Malacoda
* Copyright (C) 2014 Vlad Mihalachi
*
* This file is part of Text Editor 8000.
*
* Text Editor 8000 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.
*
* Text Editor 8000 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/>.
*/
apply plugin: 'com.android.library'
android {
compileSdkVersion 22
buildToolsVersion '22.0.1'
defaultConfig {
minSdkVersion 11
targetSdkVersion 22
versionCode 1
versionName "1.0"
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_7
targetCompatibility JavaVersion.VERSION_1_7
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
lintOptions {
abortOnError false
}
packagingOptions {
exclude 'META-INF/LICENSE.txt'
exclude 'META-INF/NOTICE.txt'
}
}

View File

@ -1,17 +0,0 @@
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in C:/Users/Vlad/AppData/Local/Android/android-sdk/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the proguardFiles
# directive in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# Add any project specific keep options here:
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}

View File

@ -1,32 +0,0 @@
/*
* Copyright (C) 2014 Vlad Mihalachi
*
* This file is part of Text Editor 8000.
*
* Text Editor 8000 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.
*
* Text Editor 8000 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 sharedcode.turboeditor;
import android.app.Application;
import android.test.ApplicationTestCase;
/**
* <a href="http://d.android.com/tools/testing/testing_android.html">Testing Fundamentals</a>
*/
public class ApplicationTest extends ApplicationTestCase<Application> {
public ApplicationTest() {
super(Application.class);
}
}

View File

@ -1,23 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2014 Vlad Mihalachi
~
~ This file is part of Text Editor 8000.
~
~ Text Editor 8000 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.
~
~ Text Editor 8000 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/>.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="sharedcode.turboeditor" >
</manifest>

View File

@ -1,119 +0,0 @@
/*
* 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 <http://www.gnu.org/licenses/>
*/
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<String, Integer> UIDS = new HashMap<String, Integer>();
public static Map<Integer, String> UNAMES = new HashMap<Integer, String>();
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<String, Integer> 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;
}
}

View File

@ -1,348 +0,0 @@
/*
* 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 <http://www.gnu.org/licenses/>
*/
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 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<OnConnectionListener> mListeners = new HashSet<OnConnectionListener>();
/**
* An interface that can be used to monitor the current state of the global connection.
*
* @see #addConnectionListener(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.<br /><br />
*
* 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.<br /><br />
*
* 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 OnConnectionListener} to the global shell
*
* @see #removeConnectionListener(OnConnectionListener)
*/
public static void addConnectionListener(OnConnectionListener listener) {
synchronized(mLock) {
mListeners.add(listener);
}
}
/**
* Remove a {@link OnConnectionListener} from the global shell
*
* @see #addConnectionListener(OnConnectionListener)
*/
public static void removeConnectionListener(OnConnectionListener listener) {
synchronized(mLock) {
mListeners.remove(listener);
}
}
/**
* @see Shell#execute(String)
*/
public static Result execute(String command) {
return mShell.execute(command);
}
/**
* @see Shell#execute(String[])
*/
public static Result execute(String[] commands) {
return mShell.execute(commands);
}
/**
* @see Shell#execute(String[], Integer[], OnShellValidateListener)
*/
public static Result execute(String[] commands, Integer[] resultCodes, OnShellValidateListener validater) {
return mShell.execute(commands, resultCodes, validater);
}
/**
* @see Shell#executeAsync(String, OnShellResultListener)
*/
public static void executeAsync(String command, OnShellResultListener listener) {
mShell.executeAsync(command, listener);
}
/**
* @see Shell#executeAsync(String[], OnShellResultListener)
*/
public static void executeAsync(String[] commands, OnShellResultListener listener) {
mShell.executeAsync(commands, listener);
}
/**
* @see Shell#executeAsync(String[], Integer[], OnShellValidateListener, OnShellResultListener)
*/
public static void executeAsync(String[] commands, Integer[] resultCodes, OnShellValidateListener validater, OnShellResultListener listener) {
mShell.executeAsync(commands, resultCodes, validater, listener);
}
/**
* @see Shell#isRoot()
*/
public static Boolean isRoot() {
return mShell.isRoot();
}
/**
* @see Shell#isConnected()
*/
public static Boolean isConnected() {
return mShell != null && mShell.isConnected();
}
/**
* @see Shell#getTimeout()
*/
public static Integer getTimeout() {
return mShell.getTimeout();
}
/**
* @see Shell#setTimeout(Integer)
*/
public static void setTimeout(Integer timeout) {
mShell.setTimeout(timeout);
}
/**
* @see Shell#getBinary(String)
*/
public static String findCommand(String bin) {
return mShell.findCommand(bin);
}
/**
* @see Shell#createAttempts(String)
*/
public static Attempts createAttempts(String command) {
return mShell.createAttempts(command);
}
/**
* @see Shell#getFileReader(String)
*/
public static FileReader getFileReader(String file) {
return mShell.getFileReader(file);
}
/**
* @see Shell#getFileWriter(String, Boolean)
*/
public static FileWriter getFileWriter(String file, Boolean append) {
return mShell.getFileWriter(file, append);
}
/**
* @see Shell#getFile(String)
*/
public static File getFile(String file) {
return mShell.getFile(file);
}
/**
* @see Shell#getFilesystem()
*/
public static Filesystem getFilesystem() {
return mShell.getFilesystem();
}
/**
* @see Shell#getDisk(String)
*/
public static Disk getDisk(String disk) {
return mShell.getDisk(disk);
}
/**
* @see Shell#getDevice()
*/
public static Device getDevice() {
return mShell.getDevice();
}
/**
* @see Shell#getProcess(String)
*/
public static Process getProcess(String process) {
return mShell.getProcess(process);
}
/**
* @see Shell#getProcess(Integer)
*/
public static Process getProcess(Integer pid) {
return mShell.getProcess(pid);
}
/**
* @see Shell#getMemory()
*/
public static Memory getMemory() {
return mShell.getMemory();
}
/**
* @see Shell#getCompCache()
*/
public static CompCache getCompCache() {
return mShell.getCompCache();
}
/**
* @see Shell#getSwap(String device)
*/
public static Swap getSwap(String device) {
return mShell.getSwap(device);
}
}

View File

@ -1,789 +0,0 @@
/*
* Copyright (C) 2014 Vlad Mihalachi
*
* This file is part of Text Editor 8000.
*
* Text Editor 8000 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.
*
* Text Editor 8000 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.spazedog.lib.rootfw4;
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;
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;
/**
* 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<Shell> mInstances = Collections.newSetFromMap(new WeakHashMap<Shell, Boolean>());
protected static Map<String, String> mBinaries = new HashMap<String, String>();
protected Set<OnShellBroadcastListener> mBroadcastRecievers = Collections.newSetFromMap(new WeakHashMap<OnShellBroadcastListener, Boolean>());
protected Set<OnShellConnectionListener> mConnectionRecievers = new HashSet<OnShellConnectionListener>();
protected Object mLock = new Object();
protected ShellStream mStream;
protected Boolean mIsConnected = false;
protected Boolean mIsRoot = false;
protected List<String> mOutput = null;
protected Integer mResultCode = 0;
protected Integer mShellTimeout = 15000;
protected Set<Integer> mResultCodes = new HashSet<Integer>();
/**
* 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 Shell#executeAsync(String[], 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<String> output, Set<Integer> 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 Data} class.
*/
public static class Result extends Data<Result> {
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 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 Shell#executeAsync(String[], Integer[], OnShellResultListener)} and {@link Shell#execute(String[], Integer[])} <br /><br />
*
* All attempts are created based on {@link Common#BINARIES}. <br /><br />
*
* Example: String("ls") would become String["ls", "busybox ls", "toolbox ls"] if {@link Common#BINARIES} equals String[null, "busybox", "toolbox"].<br /><br />
*
* You can also apply the keyword %binary if you need to apply the binaries to more than the beginning of a command. <br /><br />
*
* 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 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<String>();
}
@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 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 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.<br /><br />
*
* 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. <br /><br />
*
* <code>Shell.execute( new String(){"cat file", "toolbox cat file", "busybox cat file"} );</code><br /><br />
*
* Whether or not a command was successful, depends on {@link Shell#addResultCode(Integer)} which by default only contains '0'.
* The command number that was successful can be checked using {@link Result#getCommandNumber()}.
*
* @param commands
* The commands to try
*
* @param resultCodes
* Result Codes representing successful execution. These will be temp. merged with {@link Shell#addResultCode(Integer)}.
*
* @param validater
* A {@link OnShellValidateListener} instance or NULL
*/
public Result execute(String[] commands, Integer[] resultCodes, OnShellValidateListener validater) {
synchronized(mLock) {
if (mStream.waitFor(mShellTimeout)) {
Integer cmdCount = 0;
Set<Integer> codes = new HashSet<Integer>(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;
}
if (mOutput != null) {
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 Shell#executeAsync(String[], Integer[], OnShellResultListener)
*
* @param command
* The command to execute
*
* @param listener
* A {@link 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 Shell#executeAsync(String[], Integer[], OnShellResultListener)
*
* @param commands
* The commands to try
*
* @param listener
* A {@link 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 Shell#execute(String[], Integer[])
*
* @param commands
* The commands to try
*
* @param resultCodes
* Result Codes representing successful execution. These will be temp. merged with {@link Shell#addResultCode(Integer)}.
*
* @param validater
* A {@link OnShellValidateListener} instance or NULL
*
* @param listener
* A {@link 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 OnShellConnectionListener} callback instance
*/
public void addShellConnectionListener(OnShellConnectionListener listener) {
mConnectionRecievers.add(listener);
}
/**
* Remove a shell connection listener from the stack.
*
* @param listener
* A {@link 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 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 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.<br /><br />
*
* 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 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 FileReader}. This is the same as {@link FileReader#FileReader(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 FileWriter}. This is the same as {@link FileWriter#FileWriter(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 File} instance.
*
* @param file
* Path to the file or directory
*/
public File getFile(String file) {
return new File(this, file);
}
/**
* Get a new {@link Filesystem} instance.
*/
public Filesystem getFilesystem() {
return new Filesystem(this);
}
/**
* Get a new {@link 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 Device} instance.
*/
public Device getDevice() {
return new Device(this);
}
/**
* Get a new {@link Process} instance.
*
* @param process
* The name of the process
*/
public Process getProcess(String process) {
return new Process(this, process);
}
/**
* Get a new {@link Process} instance.
*
* @param pid
* The process id
*/
public Process getProcess(Integer pid) {
return new Process(this, pid);
}
/**
* Get a new {@link Memory} instance.
*/
public Memory getMemory() {
return new Memory(this);
}
/**
* Get a new {@link CompCache} instance.
*/
public CompCache getCompCache() {
return new CompCache(this);
}
/**
* Get a new {@link Swap} instance.
*
* @param device
* The /dev/ swap device
*/
public Swap getSwap(String device) {
return new Swap(this, device);
}
}

View File

@ -1,363 +0,0 @@
/*
* Copyright (C) 2014 Vlad Mihalachi
*
* This file is part of Text Editor 8000.
*
* Text Editor 8000 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.
*
* Text Editor 8000 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.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 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 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.contains(mCommandEnd)) {
Integer result = 0;
try {
if (output.startsWith(mCommandEnd)) {
result = Integer.parseInt(output.substring(mCommandEnd.length()+1));
} else {
result = 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.<br /><br />
*
* This method is executed asynchronous. If you need to wait until the command finishes,
* then use {@link 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 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.<br /><br />
*
* 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. <br /><br />
*
* This will force close the connection. Use this only when running a consistent command (if {@link ShellStream#isRunning()} returns true).
* When possible, sending the 'exit' command to the shell is a better choice. <br /><br />
*
* 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;
}
}
}

View File

@ -1,35 +0,0 @@
/*
* 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 <http://www.gnu.org/licenses/>
*/
package com.spazedog.lib.rootfw4.containers;
import java.util.HashMap;
import java.util.Map;
public abstract class BasicContainer {
private Map<String, Object> mObjects = new HashMap<String, Object>();
public void putObject(String name, Object object) {
mObjects.put(name, object);
}
public Object getObject(String name) {
return mObjects.get(name);
}
}

View File

@ -1,414 +0,0 @@
/*
* 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 <http://www.gnu.org/licenses/>
*/
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<DATATYPE extends Data<DATATYPE>> extends BasicContainer {
protected String[] mLines;
/**
* This interface is used as the argument for the <code>sort()</code> and <code>assort()</code> 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.
* <br />
* Note that the <code>sort()</code> method will remove the line upon false, whereas <code>assort()</code> 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 <code>replace()</code> 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 <code>DataSorting</code> class which should handle the line replacement
*
* @return
* This instance
*/
public DATATYPE replace(DataReplace dataReplace) {
if (size() > 0) {
List<String> list = new ArrayList<String>();
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 <code>DataSorting</code> instance and then removed upon a true return.
*
* @param DataSorting
* An instance of the <code>DataSorting</code> class which should determine whether or not to remove the line
*
* @return
* This instance
*/
public DATATYPE assort(DataSorting test) {
if (size() > 0) {
List<String> list = new ArrayList<String>();
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 <code>DataSorting</code> instance and then removed upon a false return.
*
* @param DataSorting
* An instance of the <code>DataSorting</code> interface which should determine whether or not to keep the line
*
* @return
* This instance
*/
public DATATYPE sort(DataSorting test) {
if (size() > 0) {
List<String> list = new ArrayList<String>();
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 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 <code>start</code> and <code>stop</code> indexes parsed via the arguments.
* <br />
* Note that the method will also except negative values.
*
* <dl>
* <dt><span class="strong">Example 1:</span></dt>
* <dd>Remove the first and last index in the array</dd>
* <dd><code>sort(1, -1)</code></dd>
* </dl>
*
* <dl>
* <dt><span class="strong">Example 2:</span></dt>
* <dd>Only keep the first and last index in the array</dd>
* <dd><code>sort(-1, 1)</code></dd>
* </dl>
*
* @param start
* Where to start
*
* @param stop
* Where to stop
*
* @return
* This instance
*/
public DATATYPE sort(Integer start, Integer stop) {
if (size() > 0) {
List<String> list = new ArrayList<String>();
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 <code>start</code> and <code>stop</code> indexes parsed via the arguments.
* <br />
* Note that the method will also except negative values.
*
* <dl>
* <dt><span class="strong">Example 1:</span></dt>
* <dd>Remove the first and last index in the array</dd>
* <dd><code>sort(-1, 1)</code></dd>
* </dl>
*
* <dl>
* <dt><span class="strong">Example 2:</span></dt>
* <dd>Only keep the first and last index in the array</dd>
* <dd><code>sort(1, -1)</code></dd>
* </dl>
*
* @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 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<String> list = new ArrayList<String>();
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 Data#getLine(Integer, Boolean)
*/
public String getLine(Integer aLineNumber) {
return getLine(aLineNumber, false);
}
/**
* This will return one specified line of the data array.
* <br />
* 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;
}
}

View File

@ -1,444 +0,0 @@
/*
* 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 <http://www.gnu.org/licenses/>
*/
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<ProcessInfo> processes = new ArrayList<ProcessInfo>();
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.<br /><br />
*
* This method first tries using the {@link PowerManager}, if that fails it fallbacks on using the reboot command from toolbox.<br /><br />
*
* Note that using the {@link PowerManager} requires your app to optain the 'REBOOT' permission. If you don't want this, just parse NULL as {@link 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 Context} or NULL to skip using the {@link 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.<br /><br />
*
* 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.<br /><br />
*
* 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 Process} instance
*
* @param process
* The name of the process
*/
public Process getProcess(String process) {
return new Process(mShell, process);
}
/**
* Get a new {@link 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<Integer> list = new ArrayList<Integer>();
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();
}
}
}

View File

@ -1,693 +0,0 @@
/*
* 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 <http://www.gnu.org/licenses/>
*/
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.
* <br /><br />
* 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 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<String> cache = new HashSet<String>();
List<MountStat> list = new ArrayList<MountStat>();
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 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 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
* <code>True</code> on success, <code>False</code> 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
* <code>True</code> on success, <code>False</code> 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
* <code>True</code> on success, <code>False</code> 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
* <code>True</code> on success, <code>False</code> 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.
* <br />
* Note that if the device parsed to the constructor {@link #Disk(Shell, String)}
* is a folder, this method will use the <code>--bind</code> option to attach it to the location. Also note that when attaching folders to a location,
* the <code>type</code> and <code>options</code> arguments will not be used and should just be parsed as <code>NULL</code>.
*
* @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
* <code>True</code> on success, <code>False</code> 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
* <code>True</code> on success, <code>False</code> 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
* <code>True</code> on success, <code>False</code> 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
* <code>True</code> if mounted, <code>False</code> 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 <code>mode=xxxx</code> can also be checked by just parsing <code>mode</code> as the argument.
*
* @param option
* The name of the option to find
*
* @return
* <code>True</code> if the options was used to attach the device, <code>False</code> 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 <code>noexec</code>, <code>nosuid</code> and <code>nodev</code> does not have any values and will return <code>NULL</code>.
* This method is used to get values from options like <code>gid=xxxx</code>, <code>mode=xxxx</code> and <code>size=xxxx</code> where <code>xxxx</code> is the value.
*
* @param option
* The name of the option to find
*
* @return
* <code>True</code> if the options was used to attach the device, <code>False</code> 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 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 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 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;
}
}
}

View File

@ -1,686 +0,0 @@
/*
* 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 <http://www.gnu.org/licenses/>
*/
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<SwapStat> statList = new ArrayList<SwapStat>();
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 Swap}
*
* @param device
* The Swap block device
*/
public Swap getSwap(String device) {
return new Swap(mShell, device);
}
/**
* Get a new instance of {@link CompCache}
*/
public CompCache getCompCache() {
return new CompCache(mShell);
}
/**
* Change the swappiness level.<br /><br />
*
* 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 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 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. <br /><br />
*
* 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. <br /><br />
*
* 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.<br /><br />
*
* This overwrites {@link 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. <br /><br />
*
* 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 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.<br /><br />
*
* 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.<br /><br />
*
* This overwrites {@link 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;
}
}
}

View File

@ -1,174 +0,0 @@
/*
* 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 <http://www.gnu.org/licenses/>
*/
package com.spazedog.lib.rootfw4.utils.io;
import java.io.BufferedReader;
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;
import com.spazedog.lib.rootfw4.Common;
import com.spazedog.lib.rootfw4.Shell;
import com.spazedog.lib.rootfw4.ShellStream;
/**
* 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 BufferedReader} and such. <br /><br />
*
* Note that this should not be used for unending streams. This is only meant for regular files. If you need unending streams, like <code>/dev/input/event*</code>,
* 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 InputStreamReader}. However {@link FileReader#FileReader(Shell, String)} is a better option.
*/
public FileReader(String file) throws FileNotFoundException {
this(null, file);
}
/**
* Create a new {@link InputStreamReader}. If <code>shell</code> is not <code>NULL</code>, then
* the best match for <code>cat</code> 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();
}
}

View File

@ -1,150 +0,0 @@
/*
* 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 <http://www.gnu.org/licenses/>
*/
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);
}
}
}

View File

@ -17,4 +17,4 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
include ':libraries:FloatingActionButton', ':libraries:sharedCode', ':app'
include ':libraries:FloatingActionButton', ':app'