diff --git a/.idea/gradle.xml b/.idea/gradle.xml index 1fd1ef0f7..6d530c3d5 100644 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -10,12 +10,16 @@ diff --git a/.idea/misc.xml b/.idea/misc.xml index 47fae2600..0fa1f5670 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -27,47 +27,6 @@ - - - - - - Android Lint - - - Groovy - - - HTML - - - J2ME issues - - - JUnit issues - - - Java language level issues - - - JavaBeans issues - - - Pattern Validation - - - Threading issues - - - Threading issuesGroovy - - - XML - - - - - diff --git a/.idea/modules.xml b/.idea/modules.xml index fa3d8c775..106f29ccb 100644 --- a/.idea/modules.xml +++ b/.idea/modules.xml @@ -3,7 +3,9 @@ + + \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 1e00d1ff1..1682655a2 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -25,4 +25,5 @@ android { dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) + compile project(':lib:RootCommands') } diff --git a/app/src/androidTest/java/com/topjohnwu/magisk/ApplicationTest.java b/app/src/androidTest/java/com/topjohnwu/magisk/ApplicationTest.java deleted file mode 100644 index 3f3732cc9..000000000 --- a/app/src/androidTest/java/com/topjohnwu/magisk/ApplicationTest.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.topjohnwu.magisk; - -import android.app.Application; -import android.test.ApplicationTestCase; - -/** - * Testing Fundamentals - */ -public class ApplicationTest extends ApplicationTestCase { - public ApplicationTest() { - super(Application.class); - } -} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e95f185f7..f3c51b779 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -8,14 +8,14 @@ android:label="@string/app_name" android:supportsRtl="true" android:theme="@style/AppTheme"> - + - + diff --git a/app/src/main/java/com/topjohnwu/magisk/Module.java b/app/src/main/java/com/topjohnwu/magisk/model/Module.java similarity index 98% rename from app/src/main/java/com/topjohnwu/magisk/Module.java rename to app/src/main/java/com/topjohnwu/magisk/model/Module.java index 26c3e4346..657354e8a 100644 --- a/app/src/main/java/com/topjohnwu/magisk/Module.java +++ b/app/src/main/java/com/topjohnwu/magisk/model/Module.java @@ -1,4 +1,4 @@ -package com.topjohnwu.magisk; +package com.topjohnwu.magisk.model; import java.io.BufferedReader; import java.io.File; diff --git a/app/src/main/java/com/topjohnwu/magisk/rv/ModulesAdapter.java b/app/src/main/java/com/topjohnwu/magisk/rv/ModulesAdapter.java new file mode 100644 index 000000000..1afd95794 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/rv/ModulesAdapter.java @@ -0,0 +1,60 @@ +package com.topjohnwu.magisk.rv; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.TextView; + +import com.topjohnwu.magisk.R; +import com.topjohnwu.magisk.model.Module; + +import java.util.List; + +public class ModulesAdapter extends ArrayAdapter { + + public ModulesAdapter(Context context, int resource, List modules) { + super(context, resource, modules); + } + + @SuppressLint("SetTextI18n") + @Override + public View getView(int position, View convertView, ViewGroup parent) { + ViewHolder vh; + + if (convertView == null) { + LayoutInflater inflater = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE); + convertView = inflater.inflate(R.layout.row, null); + + vh = new ViewHolder(); + vh.name = (TextView) convertView.findViewById(R.id.name); + vh.version = (TextView) convertView.findViewById(R.id.version); + vh.versionCode = (TextView) convertView.findViewById(R.id.versionCode); + vh.description = (TextView) convertView.findViewById(R.id.description); + vh.cache = (TextView) convertView.findViewById(R.id.cache); + + convertView.setTag(vh); + } else { + vh = (ViewHolder) convertView.getTag(); + } + + Module module = getItem(position); + vh.name.setText("name= " + module.getName()); + vh.version.setText("version= " + module.getVersion()); + vh.versionCode.setText("versioncode= " + module.getVersionCode()); + vh.description.setText("description= " + module.getDescription()); + vh.cache.setText("is from cache= " + module.isCache()); + + return convertView; + } + + static class ViewHolder { + public TextView name; + public TextView version; + public TextView versionCode; + public TextView description; + public TextView cache; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/topjohnwu/magisk/MainActivity.java b/app/src/main/java/com/topjohnwu/magisk/ui/MainActivity.java similarity index 56% rename from app/src/main/java/com/topjohnwu/magisk/MainActivity.java rename to app/src/main/java/com/topjohnwu/magisk/ui/MainActivity.java index ab32d65ab..0a6b834a9 100644 --- a/app/src/main/java/com/topjohnwu/magisk/MainActivity.java +++ b/app/src/main/java/com/topjohnwu/magisk/ui/MainActivity.java @@ -1,19 +1,17 @@ -package com.topjohnwu.magisk; +package com.topjohnwu.magisk.ui; import android.app.Activity; import android.content.Intent; import android.graphics.Color; -import android.os.AsyncTask; import android.os.Bundle; -import android.os.Handler; import android.widget.Switch; import android.widget.TextView; -import java.io.BufferedReader; -import java.io.DataOutputStream; +import com.topjohnwu.magisk.R; + import java.io.File; -import java.io.IOException; -import java.io.InputStreamReader; + +import static com.topjohnwu.magisk.ui.utils.Utils.executeCommand; public class MainActivity extends Activity { @@ -34,59 +32,29 @@ public class MainActivity extends Activity { safetyNet = (TextView) findViewById(R.id.safety_net); permissive = (TextView) findViewById(R.id.permissive); - suPath = execute("getprop magisk.supath"); + suPath = executeCommand("getprop magisk.supath"); updateStatus(); - rootToggle.setOnClickListener(view -> { - Switch s = (Switch) view; - if (s.isChecked()) { - (new SU()).execute("setprop magisk.root 1"); - } else { - (new SU()).execute("setprop magisk.root 0"); - } + rootToggle.setOnCheckedChangeListener((view, checked) -> { + executeCommand(checked ? "setprop magisk.root 1" : "setprop magisk.root 0"); + updateStatus(); }); - selinuxToggle.setOnClickListener(view -> { - Switch s = (Switch) view; - if (s.isChecked()) { - new SU().execute("setenforce 1"); - } else { - new SU().execute("setenforce 0"); - } + selinuxToggle.setOnCheckedChangeListener((view, checked) -> { + executeCommand(checked ? "setenforce 1" : "setenforce 0"); + updateStatus(); }); findViewById(R.id.modules).setOnClickListener(view -> startActivity(new Intent(this, ModulesActivity.class))); } - private String execute(String command) { - - StringBuilder output = new StringBuilder(); - - Process p; - try { - p = Runtime.getRuntime().exec(command); - p.waitFor(); - BufferedReader reader = new BufferedReader(new InputStreamReader(p.getInputStream())); - - String line; - while ((line = reader.readLine()) != null) { - if (output.length() != 0) output.append("\n"); - output.append(line); - } - - } catch (Exception e) { - e.printStackTrace(); - } - return output.toString(); - - } - private void updateStatus() { + String selinux = executeCommand("getenforce"); - String selinux = execute("getenforce"); - magiskVersion.setText(getString(R.string.magisk_version, execute("getprop magisk.version"))); + magiskVersion.setText(getString(R.string.magisk_version, executeCommand("getprop magisk.version"))); selinuxStatus.setText(selinux); + assert selinux != null; if (selinux.equals("Enforcing")) { selinuxStatus.setTextColor(Color.GREEN); selinuxToggle.setChecked(true); @@ -109,7 +77,7 @@ public class MainActivity extends Activity { safetyNet.setTextColor(Color.RED); rootToggle.setChecked(true); - if (!(new File(suPath + "/su")).exists()) { + if (!new File(suPath + "/su").exists()) { rootStatus.setText(R.string.root_system); safetyNet.setText(R.string.root_system_info); rootToggle.setEnabled(false); @@ -135,29 +103,4 @@ public class MainActivity extends Activity { } } - protected class SU extends AsyncTask { - - @Override - protected Void doInBackground(String... params) { - try { - Process su = Runtime.getRuntime().exec(suPath + "/su"); - DataOutputStream out = new DataOutputStream(su.getOutputStream()); - for (String command : params) { - out.writeBytes(command + "\n"); - out.flush(); - } - out.writeBytes("exit\n"); - out.flush(); - } catch (IOException e) { - e.printStackTrace(); - } - return null; - } - - @Override - protected void onPostExecute(Void aVoid) { - final Handler handler = new Handler(); - handler.postDelayed(MainActivity.this::updateStatus, 1500); - } - } } diff --git a/app/src/main/java/com/topjohnwu/magisk/ModulesActivity.java b/app/src/main/java/com/topjohnwu/magisk/ui/ModulesActivity.java similarity index 53% rename from app/src/main/java/com/topjohnwu/magisk/ModulesActivity.java rename to app/src/main/java/com/topjohnwu/magisk/ui/ModulesActivity.java index 3d81f0d4b..b6dca6580 100644 --- a/app/src/main/java/com/topjohnwu/magisk/ModulesActivity.java +++ b/app/src/main/java/com/topjohnwu/magisk/ui/ModulesActivity.java @@ -1,17 +1,14 @@ -package com.topjohnwu.magisk; +package com.topjohnwu.magisk.ui; -import android.annotation.SuppressLint; import android.app.Activity; import android.app.ProgressDialog; -import android.content.Context; import android.os.AsyncTask; import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ArrayAdapter; import android.widget.ListView; -import android.widget.TextView; + +import com.topjohnwu.magisk.R; +import com.topjohnwu.magisk.model.Module; +import com.topjohnwu.magisk.rv.ModulesAdapter; import java.io.File; import java.util.ArrayList; @@ -90,49 +87,4 @@ public class ModulesActivity extends Activity { } } - private class ModulesAdapter extends ArrayAdapter { - - public ModulesAdapter(Context context, int resource, List modules) { - super(context, resource, modules); - } - - @SuppressLint("SetTextI18n") - @Override - public View getView(int position, View convertView, ViewGroup parent) { - ViewHolder vh; - - if (convertView == null) { - LayoutInflater inflater = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE); - convertView = inflater.inflate(R.layout.row, null); - - vh = new ViewHolder(); - vh.name = (TextView) convertView.findViewById(R.id.name); - vh.version = (TextView) convertView.findViewById(R.id.version); - vh.versionCode = (TextView) convertView.findViewById(R.id.versionCode); - vh.description = (TextView) convertView.findViewById(R.id.description); - vh.cache = (TextView) convertView.findViewById(R.id.cache); - - convertView.setTag(vh); - } else { - vh = (ViewHolder) convertView.getTag(); - } - - Module module = getItem(position); - vh.name.setText("name= " + module.getName()); - vh.version.setText("version= " + module.getVersion()); - vh.versionCode.setText("versioncode= " + module.getVersionCode()); - vh.description.setText("description= " + module.getDescription()); - vh.cache.setText("is from cache= " + module.isCache()); - - return convertView; - } - - private class ViewHolder { - public TextView name; - public TextView version; - public TextView versionCode; - public TextView description; - public TextView cache; - } - } } diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/utils/Utils.java b/app/src/main/java/com/topjohnwu/magisk/ui/utils/Utils.java new file mode 100644 index 000000000..c36b4eaaf --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/ui/utils/Utils.java @@ -0,0 +1,28 @@ +package com.topjohnwu.magisk.ui.utils; + +import org.sufficientlysecure.rootcommands.Shell; +import org.sufficientlysecure.rootcommands.command.SimpleCommand; + +import java.io.IOException; +import java.util.concurrent.TimeoutException; + +public class Utils { + + public static String executeCommand(String... commands) { + try { + Shell shell = Shell.startRootShell(); + SimpleCommand command = new SimpleCommand(commands); + shell.add(command).waitForFinish(); + + String output = command.getOutput(); + output = output.replaceAll("\n", ""); + + shell.close(); + + return output; + } catch (IOException | TimeoutException e) { + return ""; + } + } + +} diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index d36a1d85a..5ef991c60 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -4,7 +4,7 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:focusableInTouchMode="true" - tools:context="com.topjohnwu.magisk.MainActivity"> + tools:context=".ui.MainActivity"> + tools:context=".ui.ModulesActivity"> + + + + + \ No newline at end of file diff --git a/lib/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/Mount.java b/lib/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/Mount.java new file mode 100644 index 000000000..30c887bf4 --- /dev/null +++ b/lib/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/Mount.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2012 Dominik Schürmann + * Copyright (c) 2012 Stephen Erickson, Chris Ravenscroft, Adam Shanks (RootTools) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.sufficientlysecure.rootcommands; + +import java.io.File; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +public class Mount { + protected final File mDevice; + protected final File mMountPoint; + protected final String mType; + protected final Set mFlags; + + Mount(File device, File path, String type, String flagsStr) { + mDevice = device; + mMountPoint = path; + mType = type; + mFlags = new HashSet(Arrays.asList(flagsStr.split(","))); + } + + public File getDevice() { + return mDevice; + } + + public File getMountPoint() { + return mMountPoint; + } + + public String getType() { + return mType; + } + + public Set getFlags() { + return mFlags; + } + + @Override + public String toString() { + return String.format("%s on %s type %s %s", mDevice, mMountPoint, mType, mFlags); + } +} diff --git a/lib/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/Remounter.java b/lib/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/Remounter.java new file mode 100644 index 000000000..f575e0dd1 --- /dev/null +++ b/lib/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/Remounter.java @@ -0,0 +1,188 @@ +/* + * Copyright (C) 2012 Dominik Schürmann + * Copyright (c) 2012 Stephen Erickson, Chris Ravenscroft, Adam Shanks (RootTools) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.sufficientlysecure.rootcommands; + +import org.sufficientlysecure.rootcommands.command.SimpleCommand; +import org.sufficientlysecure.rootcommands.util.Log; + +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.io.LineNumberReader; +import java.util.ArrayList; +import java.util.Locale; + +//no modifier, this means it is package-private. Only our internal classes can use this. +class Remounter { + + private Shell shell; + + public Remounter(Shell shell) { + super(); + this.shell = shell; + } + + /** + * This will return an ArrayList of the class Mount. The class mount contains the following + * property's: device mountPoint type flags + *

+ * These will provide you with any information you need to work with the mount points. + * + * @return ArrayList an ArrayList of the class Mount. + * @throws Exception if we cannot return the mount points. + */ + protected static ArrayList getMounts() throws Exception { + + final String tempFile = "/data/local/RootToolsMounts"; + + // copy /proc/mounts to tempfile. Directly reading it does not work on 4.3 + Shell shell = Shell.startRootShell(); + Toolbox tb = new Toolbox(shell); + tb.copyFile("/proc/mounts", tempFile, false, false); + tb.setFilePermissions(tempFile, "777"); + shell.close(); + + LineNumberReader lnr = null; + lnr = new LineNumberReader(new FileReader(tempFile)); + String line; + ArrayList mounts = new ArrayList(); + while ((line = lnr.readLine()) != null) { + + Log.d(RootCommands.TAG, line); + + String[] fields = line.split(" "); + mounts.add(new Mount(new File(fields[0]), // device + new File(fields[1]), // mountPoint + fields[2], // fstype + fields[3] // flags + )); + } + lnr.close(); + + return mounts; + } + + /** + * This will take a path, which can contain the file name as well, and attempt to remount the + * underlying partition. + *

+ * For example, passing in the following string: + * "/system/bin/some/directory/that/really/would/never/exist" will result in /system ultimately + * being remounted. However, keep in mind that the longer the path you supply, the more work + * this has to do, and the slower it will run. + * + * @param file file path + * @param mountType mount type: pass in RO (Read only) or RW (Read Write) + * @return a boolean which indicates whether or not the partition has been + * remounted as specified. + */ + protected boolean remount(String file, String mountType) { + + // if the path has a trailing slash get rid of it. + if (file.endsWith("/") && !file.equals("/")) { + file = file.substring(0, file.lastIndexOf("/")); + } + // Make sure that what we are trying to remount is in the mount list. + boolean foundMount = false; + while (!foundMount) { + try { + for (Mount mount : getMounts()) { + Log.d(RootCommands.TAG, mount.getMountPoint().toString()); + + if (file.equals(mount.getMountPoint().toString())) { + foundMount = true; + break; + } + } + } catch (Exception e) { + Log.e(RootCommands.TAG, "Exception", e); + return false; + } + if (!foundMount) { + try { + file = (new File(file).getParent()).toString(); + } catch (Exception e) { + Log.e(RootCommands.TAG, "Exception", e); + return false; + } + } + } + Mount mountPoint = findMountPointRecursive(file); + + Log.d(RootCommands.TAG, "Remounting " + mountPoint.getMountPoint().getAbsolutePath() + + " as " + mountType.toLowerCase(Locale.US)); + final boolean isMountMode = mountPoint.getFlags().contains(mountType.toLowerCase(Locale.US)); + + if (!isMountMode) { + // grab an instance of the internal class + try { + SimpleCommand command = new SimpleCommand("busybox mount -o remount," + + mountType.toLowerCase(Locale.US) + " " + mountPoint.getDevice().getAbsolutePath() + + " " + mountPoint.getMountPoint().getAbsolutePath(), + "toolbox mount -o remount," + mountType.toLowerCase(Locale.US) + " " + + mountPoint.getDevice().getAbsolutePath() + " " + + mountPoint.getMountPoint().getAbsolutePath(), "mount -o remount," + + mountType.toLowerCase(Locale.US) + " " + + mountPoint.getDevice().getAbsolutePath() + " " + + mountPoint.getMountPoint().getAbsolutePath(), + "/system/bin/toolbox mount -o remount," + mountType.toLowerCase(Locale.US) + " " + + mountPoint.getDevice().getAbsolutePath() + " " + + mountPoint.getMountPoint().getAbsolutePath()); + + // execute on shell + shell.add(command).waitForFinish(); + + } catch (Exception e) { + } + + mountPoint = findMountPointRecursive(file); + } + + if (mountPoint != null) { + Log.d(RootCommands.TAG, mountPoint.getFlags() + " AND " + mountType.toLowerCase(Locale.US)); + if (mountPoint.getFlags().contains(mountType.toLowerCase(Locale.US))) { + Log.d(RootCommands.TAG, mountPoint.getFlags().toString()); + return true; + } else { + Log.d(RootCommands.TAG, mountPoint.getFlags().toString()); + } + } else { + Log.d(RootCommands.TAG, "mountPoint is null"); + } + return false; + } + + private Mount findMountPointRecursive(String file) { + try { + ArrayList mounts = getMounts(); + for (File path = new File(file); path != null; ) { + for (Mount mount : mounts) { + if (mount.getMountPoint().equals(path)) { + return mount; + } + } + } + return null; + } catch (IOException e) { + throw new RuntimeException(e); + } catch (Exception e) { + Log.e(RootCommands.TAG, "Exception", e); + } + return null; + } +} diff --git a/lib/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/RootCommands.java b/lib/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/RootCommands.java new file mode 100644 index 000000000..67e583757 --- /dev/null +++ b/lib/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/RootCommands.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2012 Dominik Schürmann + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.sufficientlysecure.rootcommands; + +import org.sufficientlysecure.rootcommands.util.Log; + +public class RootCommands { + public static final String TAG = "RootCommands"; + public static boolean DEBUG = false; + public static int DEFAULT_TIMEOUT = 10000; + + /** + * General method to check if user has su binary and accepts root access for this program! + * + * @return true if everything worked + */ + public static boolean rootAccessGiven() { + boolean rootAccess = false; + + try { + Shell rootShell = Shell.startRootShell(); + + Toolbox tb = new Toolbox(rootShell); + if (tb.isRootAccessGiven()) { + rootAccess = true; + } + + rootShell.close(); + } catch (Exception e) { + Log.e(TAG, "Problem while checking for root access!", e); + } + + return rootAccess; + } +} diff --git a/lib/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/Shell.java b/lib/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/Shell.java new file mode 100644 index 000000000..74aa0bfe3 --- /dev/null +++ b/lib/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/Shell.java @@ -0,0 +1,347 @@ +/* + * Copyright (C) 2012 Dominik Schürmann + * Copyright (c) 2012 Stephen Erickson, Chris Ravenscroft, Adam Shanks, Jeremy Lakeman (RootTools) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.sufficientlysecure.rootcommands; + +import org.sufficientlysecure.rootcommands.command.Command; +import org.sufficientlysecure.rootcommands.util.Log; +import org.sufficientlysecure.rootcommands.util.RootAccessDeniedException; +import org.sufficientlysecure.rootcommands.util.Utils; + +import java.io.BufferedReader; +import java.io.Closeable; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.List; + +public class Shell implements Closeable { + private static final String LD_LIBRARY_PATH = System.getenv("LD_LIBRARY_PATH"); + private static final String token = "F*D^W@#FGF"; + private final Process shellProcess; + private final BufferedReader stdOutErr; + private final DataOutputStream outputStream; + private final List commands = new ArrayList(); + private boolean close = false; + private Runnable inputRunnable = new Runnable() { + public void run() { + try { + writeCommands(); + } catch (IOException e) { + Log.e(RootCommands.TAG, "IO Exception", e); + } + } + }; + private Runnable outputRunnable = new Runnable() { + public void run() { + try { + readOutput(); + } catch (IOException e) { + Log.e(RootCommands.TAG, "IOException", e); + } catch (InterruptedException e) { + Log.e(RootCommands.TAG, "InterruptedException", e); + } + } + }; + + private Shell(String shell, ArrayList customEnv, String baseDirectory) + throws IOException { + Log.d(RootCommands.TAG, "Starting shell: " + shell); + + // start shell process! + shellProcess = Utils.runWithEnv(shell, customEnv, baseDirectory); + + // StdErr is redirected to StdOut, defined in Command.getCommand() + stdOutErr = new BufferedReader(new InputStreamReader(shellProcess.getInputStream())); + outputStream = new DataOutputStream(shellProcess.getOutputStream()); + + outputStream.write("echo Started\n".getBytes()); + outputStream.flush(); + + while (true) { + String line = stdOutErr.readLine(); + if (line == null) + throw new RootAccessDeniedException( + "stdout line is null! Access was denied or this executeable is not a shell!"); + if ("".equals(line)) + continue; + if ("Started".equals(line)) + break; + + destroyShellProcess(); + throw new IOException("Unable to start shell, unexpected output \"" + line + "\""); + } + + new Thread(inputRunnable, "Shell Input").start(); + new Thread(outputRunnable, "Shell Output").start(); + } + + /** + * Start root shell + * + * @param customEnv + * @param baseDirectory + * @return + * @throws IOException + */ + public static Shell startRootShell(ArrayList customEnv, String baseDirectory) + throws IOException { + Log.d(RootCommands.TAG, "Starting Root Shell!"); + + // On some versions of Android (ICS) LD_LIBRARY_PATH is unset when using su + // We need to pass LD_LIBRARY_PATH over su for some commands to work correctly. + if (customEnv == null) { + customEnv = new ArrayList(); + } + customEnv.add("LD_LIBRARY_PATH=" + LD_LIBRARY_PATH); + + Shell shell = new Shell(Utils.getSuPath(), customEnv, baseDirectory); + + return shell; + } + + /** + * Start root shell without custom environment and base directory + * + * @return + * @throws IOException + */ + public static Shell startRootShell() throws IOException { + return startRootShell(null, null); + } + + /** + * Start default sh shell + * + * @param customEnv + * @param baseDirectory + * @return + * @throws IOException + */ + public static Shell startShell(ArrayList customEnv, String baseDirectory) + throws IOException { + Log.d(RootCommands.TAG, "Starting Shell!"); + Shell shell = new Shell("sh", customEnv, baseDirectory); + return shell; + } + + /** + * Start default sh shell without custom environment and base directory + * + * @return + * @throws IOException + */ + public static Shell startShell() throws IOException { + return startShell(null, null); + } + + /** + * Start custom shell defined by shellPath + * + * @param shellPath + * @param customEnv + * @param baseDirectory + * @return + * @throws IOException + */ + public static Shell startCustomShell(String shellPath, ArrayList customEnv, + String baseDirectory) throws IOException { + Log.d(RootCommands.TAG, "Starting Custom Shell!"); + Shell shell = new Shell(shellPath, customEnv, baseDirectory); + + return shell; + } + + /** + * Start custom shell without custom environment and base directory + * + * @param shellPath + * @return + * @throws IOException + */ + public static Shell startCustomShell(String shellPath) throws IOException { + return startCustomShell(shellPath, null, null); + } + + /** + * Destroy shell process considering that the process could already be terminated + */ + private void destroyShellProcess() { + try { + // Yes, this really is the way to check if the process is + // still running. + shellProcess.exitValue(); + } catch (IllegalThreadStateException e) { + // Only call destroy() if the process is still running; + // Calling it for a terminated process will not crash, but + // (starting with at least ICS/4.0) spam the log with INFO + // messages ala "Failed to destroy process" and "kill + // failed: ESRCH (No such process)". + shellProcess.destroy(); + } + + Log.d(RootCommands.TAG, "Shell destroyed"); + } + + /** + * Writes queued commands one after another into the opened shell. After an execution a token is + * written to seperate command output on read + * + * @throws IOException + */ + private void writeCommands() throws IOException { + try { + int commandIndex = 0; + while (true) { + DataOutputStream out; + synchronized (commands) { + while (!close && commandIndex >= commands.size()) { + commands.wait(); + } + out = this.outputStream; + } + if (commandIndex < commands.size()) { + Command next = commands.get(commandIndex); + next.writeCommand(out); + String line = "\necho " + token + " " + commandIndex + " $?\n"; + out.write(line.getBytes()); + out.flush(); + commandIndex++; + } else if (close) { + out.write("\nexit 0\n".getBytes()); + out.flush(); + Log.d(RootCommands.TAG, "Closing shell"); + shellProcess.waitFor(); + out.close(); + return; + } else { + Thread.sleep(50); + } + } + } catch (InterruptedException e) { + Log.e(RootCommands.TAG, "interrupted while writing command", e); + } + } + + /** + * Reads output line by line, seperated by token written after every command + * + * @throws IOException + * @throws InterruptedException + */ + private void readOutput() throws IOException, InterruptedException { + Command command = null; + + // index of current command + int commandIndex = 0; + while (true) { + String lineStdOut = stdOutErr.readLine(); + + // terminate on EOF + if (lineStdOut == null) + break; + + if (command == null) { + + // break on close after last command + if (commandIndex >= commands.size()) { + if (close) + break; + continue; + } + + // get current command + command = commands.get(commandIndex); + } + + int pos = lineStdOut.indexOf(token); + if (pos > 0) { + command.processOutput(lineStdOut.substring(0, pos)); + } + if (pos >= 0) { + lineStdOut = lineStdOut.substring(pos); + String fields[] = lineStdOut.split(" "); + int id = Integer.parseInt(fields[1]); + if (id == commandIndex) { + command.setExitCode(Integer.parseInt(fields[2])); + + // go to next command + commandIndex++; + command = null; + continue; + } + } + command.processOutput(lineStdOut); + } + Log.d(RootCommands.TAG, "Read all output"); + shellProcess.waitFor(); + stdOutErr.close(); + destroyShellProcess(); + + while (commandIndex < commands.size()) { + if (command == null) { + command = commands.get(commandIndex); + } + command.terminated("Unexpected Termination!"); + commandIndex++; + command = null; + } + } + + /** + * Add command to shell queue + * + * @param command + * @return + * @throws IOException + */ + public Command add(Command command) throws IOException { + if (close) + throw new IOException("Unable to add commands to a closed shell"); + synchronized (commands) { + commands.add(command); + // set shell on the command object, to know where the command is running on + command.addedToShell(this, (commands.size() - 1)); + commands.notifyAll(); + } + + return command; + } + + /** + * Close shell + * + * @throws IOException + */ + public void close() throws IOException { + synchronized (commands) { + this.close = true; + commands.notifyAll(); + } + } + + /** + * Returns number of queued commands + * + * @return + */ + public int getCommandsSize() { + return commands.size(); + } + +} \ No newline at end of file diff --git a/lib/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/SystemCommands.java b/lib/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/SystemCommands.java new file mode 100644 index 000000000..1fa6b169d --- /dev/null +++ b/lib/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/SystemCommands.java @@ -0,0 +1,120 @@ +/* + * Copyright (C) 2012 Dominik Schürmann + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.sufficientlysecure.rootcommands; + +import android.annotation.TargetApi; +import android.content.ContentResolver; +import android.content.Context; +import android.location.LocationManager; +import android.os.PowerManager; +import android.provider.Settings; + +/** + * This methods work when the apk is installed as a system app (under /system/app) + */ +public class SystemCommands { + Context context; + + public SystemCommands(Context context) { + super(); + this.context = context; + } + + /** + * Get GPS status + */ + public boolean getGPS() { + return ((LocationManager) context.getSystemService(Context.LOCATION_SERVICE)) + .isProviderEnabled(LocationManager.GPS_PROVIDER); + } + + /** + * Enable/Disable GPS + * + * @param value + */ + @TargetApi(8) + public void setGPS(boolean value) { + ContentResolver localContentResolver = context.getContentResolver(); + Settings.Secure.setLocationProviderEnabled(localContentResolver, + LocationManager.GPS_PROVIDER, value); + } + + /** + * TODO: Not ready yet + */ + @TargetApi(8) + public void reboot() { + PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE); + pm.reboot("recovery"); + pm.reboot(null); + + // not working: + // reboot(null); + } + + /** + * Reboot the device immediately, passing 'reason' (may be null) to the underlying __reboot + * system call. Should not return. + * + * Taken from com.android.server.PowerManagerService.reboot + */ + // public void reboot(String reason) { + // + // // final String finalReason = reason; + // Runnable runnable = new Runnable() { + // public void run() { + // synchronized (this) { + // // ShutdownThread.reboot(mContext, finalReason, false); + // try { + // Class clazz = Class.forName("com.android.internal.app.ShutdownThread"); + // + // // if (mReboot) { + // Method method = clazz.getMethod("reboot", Context.class, String.class, + // Boolean.TYPE); + // method.invoke(null, context, null, false); + // + // // if (mReboot) { + // // Method method = clazz.getMethod("reboot", Context.class, String.class, + // // Boolean.TYPE); + // // method.invoke(null, mContext, mReason, mConfirm); + // // } else { + // // Method method = clazz.getMethod("shutdown", Context.class, Boolean.TYPE); + // // method.invoke(null, mContext, mConfirm); + // // } + // } catch (Exception e) { + // e.printStackTrace(); + // } + // } + // + // } + // }; + // // ShutdownThread must run on a looper capable of displaying the UI. + // mHandler.post(runnable); + // + // // PowerManager.reboot() is documented not to return so just wait for the inevitable. + // // synchronized (runnable) { + // // while (true) { + // // try { + // // runnable.wait(); + // // } catch (InterruptedException e) { + // // } + // // } + // // } + // } + +} diff --git a/lib/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/Toolbox.java b/lib/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/Toolbox.java new file mode 100644 index 000000000..9f2a7fbd0 --- /dev/null +++ b/lib/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/Toolbox.java @@ -0,0 +1,772 @@ +/* + * Copyright (C) 2012 Dominik Schürmann + * Copyright (c) 2012 Stephen Erickson, Chris Ravenscroft, Adam Shanks (RootTools) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.sufficientlysecure.rootcommands; + +import android.os.StatFs; +import android.os.SystemClock; + +import org.sufficientlysecure.rootcommands.command.Command; +import org.sufficientlysecure.rootcommands.command.ExecutableCommand; +import org.sufficientlysecure.rootcommands.command.SimpleCommand; +import org.sufficientlysecure.rootcommands.util.BrokenBusyboxException; +import org.sufficientlysecure.rootcommands.util.Log; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.ArrayList; +import java.util.concurrent.TimeoutException; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * All methods in this class are working with Androids toolbox. Toolbox is similar to busybox, but + * normally shipped on every Android OS. You can find toolbox commands on + * https://github.com/CyanogenMod/android_system_core/tree/ics/toolbox + *

+ * This means that these commands are designed to work on every Android OS, with a _working_ toolbox + * binary on it. They don't require busybox! + */ +public class Toolbox { + public static final int REBOOT_HOTREBOOT = 1; + public static final int REBOOT_REBOOT = 2; + public static final int REBOOT_SHUTDOWN = 3; + public static final int REBOOT_RECOVERY = 4; + private Shell shell; + + /** + * All methods in this class are working with Androids toolbox. Toolbox is similar to busybox, + * but normally shipped on every Android OS. + * + * @param shell where to execute commands on + */ + public Toolbox(Shell shell) { + super(); + this.shell = shell; + } + + /** + * Checks if user accepted root access + *

+ * (commands: id) + * + * @return true if user has given root access + * @throws IOException + * @throws TimeoutException + * @throws BrokenBusyboxException + */ + public boolean isRootAccessGiven() throws TimeoutException, IOException { + SimpleCommand idCommand = new SimpleCommand("id"); + shell.add(idCommand).waitForFinish(); + + return idCommand.getOutput().contains("uid=0"); + } + + /** + * This method can be used to kill a running process + *

+ * (commands: ps, kill) + * + * @param processName name of process to kill + * @return true if process was found and killed successfully + * @throws IOException + * @throws TimeoutException + * @throws BrokenBusyboxException + */ + public boolean killAll(String processName) throws TimeoutException, + IOException { + Log.d(RootCommands.TAG, "Killing process " + processName); + + PsCommand psCommand = new PsCommand(processName); + shell.add(psCommand).waitForFinish(); + + // kill processes + if (!psCommand.getPids().isEmpty()) { + // example: kill -9 1234 1222 5343 + SimpleCommand killCommand = new SimpleCommand("kill -9 " + + psCommand.getPidsString()); + shell.add(killCommand).waitForFinish(); + + return killCommand.getExitCode() == 0; + } else { + Log.d(RootCommands.TAG, "No pid found! Nothing was killed!"); + return false; + } + } + + /** + * Kill a running executable + *

+ * See README for more information how to use your own executables! + * + * @param executableName + * @return + * @throws BrokenBusyboxException + * @throws TimeoutException + * @throws IOException + */ + public boolean killAllExecutable(String executableName) throws + TimeoutException, IOException { + return killAll(ExecutableCommand.EXECUTABLE_PREFIX + executableName + ExecutableCommand.EXECUTABLE_SUFFIX); + } + + /** + * This method can be used to to check if a process is running + * + * @param processName name of process to check + * @return true if process was found + * @throws IOException + * @throws BrokenBusyboxException + * @throws TimeoutException (Could not determine if the process is running) + */ + public boolean isProcessRunning(String processName) throws + TimeoutException, IOException { + PsCommand psCommand = new PsCommand(processName); + shell.add(psCommand).waitForFinish(); + + // if pids are available process is running! + return !psCommand.getPids().isEmpty(); + } + + /** + * Checks if binary is running + * + * @param binaryName + * @return + * @throws BrokenBusyboxException + * @throws TimeoutException + * @throws IOException + */ + public boolean isBinaryRunning(String binaryName) throws + TimeoutException, IOException { + return isProcessRunning(ExecutableCommand.EXECUTABLE_PREFIX + binaryName + + ExecutableCommand.EXECUTABLE_SUFFIX); + } + + /** + * @param file String that represent the file, including the full path to the file and its name. + * @return File permissions as String, for example: 777, returns null on error + * @throws IOException + * @throws TimeoutException + * @throws BrokenBusyboxException + */ + public String getFilePermissions(String file) throws TimeoutException, + IOException { + Log.d(RootCommands.TAG, "Checking permissions for " + file); + + String permissions = null; + + if (fileExists(file)) { + Log.d(RootCommands.TAG, file + " was found."); + + LsCommand lsCommand = new LsCommand(file); + shell.add(lsCommand).waitForFinish(); + + permissions = lsCommand.getPermissions(); + } + + return permissions; + } + + /** + * Sets permission of file + * + * @param file absolute path to file + * @param permissions String like 777 + * @return true if command worked + * @throws BrokenBusyboxException + * @throws TimeoutException + * @throws IOException + */ + public boolean setFilePermissions(String file, String permissions) + throws TimeoutException, IOException { + Log.d(RootCommands.TAG, "Set permissions of " + file + " to " + permissions); + + SimpleCommand chmodCommand = new SimpleCommand("chmod " + permissions + " " + file); + shell.add(chmodCommand).waitForFinish(); + + return chmodCommand.getExitCode() == 0; + } + + /** + * This will return a String that represent the symlink for a specified file. + * + * @param file The path to the file to get the Symlink for. (must have absolute path) + * @return A String that represent the symlink for a specified file or null if no symlink + * exists. + * @throws IOException + * @throws TimeoutException + * @throws BrokenBusyboxException + */ + public String getSymlink(String file) throws TimeoutException, + IOException { + Log.d(RootCommands.TAG, "Find symlink for " + file); + + String symlink = null; + + LsCommand lsCommand = new LsCommand(file); + shell.add(lsCommand).waitForFinish(); + + symlink = lsCommand.getSymlink(); + + return symlink; + } + + /** + * Copys a file to a destination. Because cp is not available on all android devices, we use dd + * or cat. + * + * @param source example: /data/data/org.adaway/files/hosts + * @param destination example: /system/etc/hosts + * @param remountAsRw remounts the destination as read/write before writing to it + * @return true if it was successfully copied + * @throws BrokenBusyboxException + * @throws IOException + * @throws TimeoutException + */ + public boolean copyFile(String source, String destination, boolean remountAsRw, + boolean preservePermissions) throws IOException, + TimeoutException { + + /* + * dd can only copy files, but we can not check if the source is a file without invoking + * shell commands, because from Java we probably have no read access, thus we only check if + * they are ending with trailing slashes + */ + if (source.endsWith("/") || destination.endsWith("/")) { + throw new FileNotFoundException("dd can only copy files!"); + } + + // remount destination as read/write before copying to it + if (remountAsRw) { + if (!remount(destination, "RW")) { + Log.d(RootCommands.TAG, + "Remounting failed! There is probably no need to remount this partition!"); + } + } + + // get permissions of source before overwriting + String permissions = null; + if (preservePermissions) { + permissions = getFilePermissions(source); + } + + boolean commandSuccess = false; + + SimpleCommand ddCommand = new SimpleCommand("dd if=" + source + " of=" + + destination); + shell.add(ddCommand).waitForFinish(); + + if (ddCommand.getExitCode() == 0) { + commandSuccess = true; + } else { + // try cat if dd fails + SimpleCommand catCommand = new SimpleCommand("cat " + source + " > " + + destination); + shell.add(catCommand).waitForFinish(); + + if (catCommand.getExitCode() == 0) { + commandSuccess = true; + } + } + + // set back permissions from source to destination + if (preservePermissions) { + setFilePermissions(destination, permissions); + } + + // remount destination back to read only + if (remountAsRw) { + if (!remount(destination, "RO")) { + Log.d(RootCommands.TAG, + "Remounting failed! There is probably no need to remount this partition!"); + } + } + + return commandSuccess; + } + + /** + * Shutdown or reboot device. Possible actions are REBOOT_HOTREBOOT, REBOOT_REBOOT, + * REBOOT_SHUTDOWN, REBOOT_RECOVERY + * + * @throws IOException + * @throws TimeoutException + * @throws BrokenBusyboxException + */ + public void reboot(int action) throws TimeoutException, IOException { + if (action == REBOOT_HOTREBOOT) { + killAll("system_server"); + // or: killAll("zygote"); + } else { + String command; + switch (action) { + case REBOOT_REBOOT: + command = "reboot"; + break; + case REBOOT_SHUTDOWN: + command = "reboot -p"; + break; + case REBOOT_RECOVERY: + command = "reboot recovery"; + break; + default: + command = "reboot"; + break; + } + + SimpleCommand rebootCommand = new SimpleCommand(command); + shell.add(rebootCommand).waitForFinish(); + + if (rebootCommand.getExitCode() == -1) { + Log.e(RootCommands.TAG, "Reboot failed!"); + } + } + } + + /** + * Use this to check whether or not a file exists on the filesystem. + * + * @param file String that represent the file, including the full path to the file and its name. + * @return a boolean that will indicate whether or not the file exists. + * @throws IOException + * @throws TimeoutException + * @throws BrokenBusyboxException + */ + public boolean fileExists(String file) throws TimeoutException, + IOException { + FileExistsCommand fileExistsCommand = new FileExistsCommand(file); + shell.add(fileExistsCommand).waitForFinish(); + + return fileExistsCommand.isFileExists(); + } + + /** + * Execute user defined Java code while having temporary permissions on a file + * + * @param file + * @param withPermissions + * @throws BrokenBusyboxException + * @throws TimeoutException + * @throws IOException + */ + public void withPermission(String file, String permission, WithPermissions withPermissions) + throws TimeoutException, IOException { + String oldPermissions = getFilePermissions(file); + + // set permissions (If set to 666, then Dalvik VM can also write to that file!) + setFilePermissions(file, permission); + + // execute user defined code + withPermissions.whileHavingPermissions(); + + // set back to old permissions + setFilePermissions(file, oldPermissions); + } + + /** + * Execute user defined Java code while having temporary write permissions on a file using chmod + * 666 + * + * @param file + * @param withWritePermissions + * @throws BrokenBusyboxException + * @throws TimeoutException + * @throws IOException + */ + public void withWritePermissions(String file, WithPermissions withWritePermissions) + throws TimeoutException, IOException { + withPermission(file, "666", withWritePermissions); + } + + /** + * Sets system clock using /dev/alarm + * + * @param millis + * @throws BrokenBusyboxException + * @throws TimeoutException + * @throws IOException + */ + public void setSystemClock(final long millis) throws TimeoutException, + IOException { + withWritePermissions("/dev/alarm", new WithPermissions() { + + @Override + void whileHavingPermissions() { + SystemClock.setCurrentTimeMillis(millis); + } + }); + } + + /** + * Adjust system clock by offset using /dev/alarm + * + * @param offset + * @throws BrokenBusyboxException + * @throws TimeoutException + * @throws IOException + */ + public void adjustSystemClock(final long offset) throws + TimeoutException, IOException { + withWritePermissions("/dev/alarm", new WithPermissions() { + + @Override + void whileHavingPermissions() { + SystemClock.setCurrentTimeMillis(System.currentTimeMillis() + offset); + } + }); + } + + /** + * This will take a path, which can contain the file name as well, and attempt to remount the + * underlying partition. + *

+ * For example, passing in the following string: + * "/system/bin/some/directory/that/really/would/never/exist" will result in /system ultimately + * being remounted. However, keep in mind that the longer the path you supply, the more work + * this has to do, and the slower it will run. + * + * @param file file path + * @param mountType mount type: pass in RO (Read only) or RW (Read Write) + * @return a boolean which indicates whether or not the partition has been + * remounted as specified. + */ + public boolean remount(String file, String mountType) { + // Recieved a request, get an instance of Remounter + Remounter remounter = new Remounter(shell); + // send the request + return (remounter.remount(file, mountType)); + } + + /** + * This will tell you how the specified mount is mounted. rw, ro, etc... + * + * @return String What the mount is mounted as. + * @throws Exception if we cannot determine how the mount is mounted. + */ + public String getMountedAs(String path) throws Exception { + ArrayList mounts = Remounter.getMounts(); + if (mounts != null) { + for (Mount mount : mounts) { + if (path.contains(mount.getMountPoint().getAbsolutePath())) { + Log.d(RootCommands.TAG, (String) mount.getFlags().toArray()[0]); + return (String) mount.getFlags().toArray()[0]; + } + } + + throw new Exception(); + } else { + throw new Exception(); + } + } + + /** + * Check if there is enough space on partition where target is located + * + * @param size size of file to put on partition + * @param target path where to put the file + * @return true if it will fit on partition of target, false if it will not fit. + */ + public boolean hasEnoughSpaceOnPartition(String target, long size) { + try { + // new File(target).getFreeSpace() (API 9) is not working on data partition + + // get directory without file + String directory = new File(target).getParent().toString(); + + StatFs stat = new StatFs(directory); + long blockSize = stat.getBlockSize(); + long availableBlocks = stat.getAvailableBlocks(); + long availableSpace = availableBlocks * blockSize; + + Log.i(RootCommands.TAG, "Checking for enough space: Target: " + target + + ", directory: " + directory + " size: " + size + ", availableSpace: " + + availableSpace); + + if (size < availableSpace) { + return true; + } else { + Log.e(RootCommands.TAG, "Not enough space on partition!"); + return false; + } + } catch (Exception e) { + // if new StatFs(directory) fails catch IllegalArgumentException and just return true as + // workaround + Log.e(RootCommands.TAG, "Problem while getting available space on partition!", e); + return true; + } + } + + /** + * TODO: Not tested! + * + * @param toggle + * @throws IOException + * @throws TimeoutException + * @throws BrokenBusyboxException + */ + public void toggleAdbDaemon(boolean toggle) throws TimeoutException, + IOException { + SimpleCommand disableAdb = new SimpleCommand("setprop persist.service.adb.enable 0", + "stop adbd"); + SimpleCommand enableAdb = new SimpleCommand("setprop persist.service.adb.enable 1", + "stop adbd", "sleep 1", "start adbd"); + + if (toggle) { + shell.add(enableAdb).waitForFinish(); + } else { + shell.add(disableAdb).waitForFinish(); + } + } + + /** + * This command class gets all pids to a given process name + */ + private class PsCommand extends Command { + private String processName; + private ArrayList pids; + private String psRegex; + private Pattern psPattern; + + public PsCommand(String processName) { + super("ps"); + this.processName = processName; + pids = new ArrayList(); + + /** + * regex to get pid out of ps line, example: + * + *

+             *  root    24736    1   12140  584   ffffffff 40010d14 S /data/data/org.adaway/files/blank_webserver
+             * ^\\S \\s ([0-9]+)                          .*                                      processName    $
+             * 
+ */ + psRegex = "^\\S+\\s+([0-9]+).*" + Pattern.quote(processName) + "$"; + psPattern = Pattern.compile(psRegex); + } + + public ArrayList getPids() { + return pids; + } + + public String getPidsString() { + StringBuilder sb = new StringBuilder(); + for (String s : pids) { + sb.append(s); + sb.append(" "); + } + + return sb.toString(); + } + + @Override + public void output(int id, String line) { + // general check if line contains processName + if (line.contains(processName)) { + Matcher psMatcher = psPattern.matcher(line); + + // try to match line exactly + try { + if (psMatcher.find()) { + String pid = psMatcher.group(1); + // add to pids list + pids.add(pid); + Log.d(RootCommands.TAG, "Found pid: " + pid); + } else { + Log.d(RootCommands.TAG, "Matching in ps command failed!"); + } + } catch (Exception e) { + Log.e(RootCommands.TAG, "Error with regex!", e); + } + } + } + + @Override + public void afterExecution(int id, int exitCode) { + } + + } + + /** + * Ls command to get permissions or symlinks + */ + private class LsCommand extends Command { + private String fileName; + private String permissionRegex; + private Pattern permissionPattern; + private String symlinkRegex; + private Pattern symlinkPattern; + + private String symlink; + private String permissions; + + public LsCommand(String file) { + super("ls -l " + file); + + // get only filename: + this.fileName = (new File(file)).getName(); + Log.d(RootCommands.TAG, "fileName: " + fileName); + + /** + * regex to get pid out of ps line, example: + * + *
+             * with busybox:
+             *     lrwxrwxrwx     1 root root            15 Aug 13 12:14 dev/stdin -> /proc/self/fd/0
+             *
+             * with toolbox:
+             *     lrwxrwxrwx root root            15 Aug 13 12:14 stdin -> /proc/self/fd/0
+             *
+             * Regex:
+             * ^.*?(\\S{10})                     .*                                                  $
+             * 
+ */ + permissionRegex = "^.*?(\\S{10}).*$"; + permissionPattern = Pattern.compile(permissionRegex); + + /** + * regex to get symlink + * + *
+             *     ->           /proc/self/fd/0
+             * ^.*?\\-\\> \\s+  (.*)           $
+             * 
+ */ + symlinkRegex = "^.*?\\-\\>\\s+(.*)$"; + symlinkPattern = Pattern.compile(symlinkRegex); + } + + public String getSymlink() { + return symlink; + } + + public String getPermissions() { + return permissions; + } + + /** + * Converts permission string from ls command to numerical value. Example: -rwxrwxrwx gets + * to 777 + * + * @param permissions + * @return + */ + private String convertPermissions(String permissions) { + int owner = getGroupPermission(permissions.substring(1, 4)); + int group = getGroupPermission(permissions.substring(4, 7)); + int world = getGroupPermission(permissions.substring(7, 10)); + + return "" + owner + group + world; + } + + /** + * Calculates permission for one group + * + * @param permission + * @return value of permission string + */ + private int getGroupPermission(String permission) { + int value = 0; + + if (permission.charAt(0) == 'r') { + value += 4; + } + if (permission.charAt(1) == 'w') { + value += 2; + } + if (permission.charAt(2) == 'x') { + value += 1; + } + + return value; + } + + @Override + public void output(int id, String line) { + // general check if line contains file + if (line.contains(fileName)) { + + // try to match line exactly + try { + Matcher permissionMatcher = permissionPattern.matcher(line); + if (permissionMatcher.find()) { + permissions = convertPermissions(permissionMatcher.group(1)); + + Log.d(RootCommands.TAG, "Found permissions: " + permissions); + } else { + Log.d(RootCommands.TAG, "Permissions were not found in ls command!"); + } + + // try to parse for symlink + Matcher symlinkMatcher = symlinkPattern.matcher(line); + if (symlinkMatcher.find()) { + /* + * TODO: If symlink points to a file in the same directory the path is not + * absolute!!! + */ + symlink = symlinkMatcher.group(1); + Log.d(RootCommands.TAG, "Symlink found: " + symlink); + } else { + Log.d(RootCommands.TAG, "No symlink found!"); + } + } catch (Exception e) { + Log.e(RootCommands.TAG, "Error with regex!", e); + } + } + } + + @Override + public void afterExecution(int id, int exitCode) { + } + + } + + /** + * This command checks if a file exists + */ + private class FileExistsCommand extends Command { + private String file; + private boolean fileExists = false; + + public FileExistsCommand(String file) { + super("ls " + file); + this.file = file; + } + + public boolean isFileExists() { + return fileExists; + } + + @Override + public void output(int id, String line) { + if (line.trim().equals(file)) { + fileExists = true; + } + } + + @Override + public void afterExecution(int id, int exitCode) { + } + + } + + public abstract class WithPermissions { + abstract void whileHavingPermissions(); + } + +} diff --git a/lib/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/command/Command.java b/lib/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/command/Command.java new file mode 100644 index 000000000..e7e54781c --- /dev/null +++ b/lib/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/command/Command.java @@ -0,0 +1,169 @@ +/* + * Copyright (C) 2012 Dominik Schürmann + * Copyright (c) 2012 Stephen Erickson, Chris Ravenscroft, Adam Shanks, Jeremy Lakeman (RootTools) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.sufficientlysecure.rootcommands.command; + +import org.sufficientlysecure.rootcommands.RootCommands; +import org.sufficientlysecure.rootcommands.Shell; +import org.sufficientlysecure.rootcommands.util.BrokenBusyboxException; +import org.sufficientlysecure.rootcommands.util.Log; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.concurrent.TimeoutException; + +public abstract class Command { + final String command[]; + boolean finished = false; + boolean brokenBusyboxDetected = false; + int exitCode; + int id; + int timeout = RootCommands.DEFAULT_TIMEOUT; + Shell shell = null; + + public Command(String... command) { + this.command = command; + } + + public Command(int timeout, String... command) { + this.command = command; + this.timeout = timeout; + } + + /** + * This is called from Shell after adding it + * + * @param shell + * @param id + */ + public void addedToShell(Shell shell, int id) { + this.shell = shell; + this.id = id; + } + + /** + * Gets command string executed on the shell + * + * @return + */ + public String getCommand() { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < command.length; i++) { + // redirect stderr to stdout + sb.append(command[i] + " 2>&1"); + sb.append('\n'); + } + Log.d(RootCommands.TAG, "Sending command(s): " + sb.toString()); + return sb.toString(); + } + + public void writeCommand(OutputStream out) throws IOException { + out.write(getCommand().getBytes()); + } + + public void processOutput(String line) { + Log.d(RootCommands.TAG, "ID: " + id + ", Output: " + line); + + /* + * Try to detect broken toolbox/busybox binaries (see + * https://code.google.com/p/busybox-android/issues/detail?id=1) + * + * It is giving "Value too large for defined data type" on certain file operations (e.g. ls + * and chown) in certain directories (e.g. /data/data) + */ + if (line.contains("Value too large for defined data type")) { + Log.e(RootCommands.TAG, "Busybox is broken with high probability due to line: " + line); + brokenBusyboxDetected = true; + } + + // now execute specific output parsing + output(id, line); + } + + public abstract void output(int id, String line); + + public void processAfterExecution(int exitCode) { + Log.d(RootCommands.TAG, "ID: " + id + ", ExitCode: " + exitCode); + + afterExecution(id, exitCode); + } + + public abstract void afterExecution(int id, int exitCode); + + public void commandFinished(int id) { + Log.d(RootCommands.TAG, "Command " + id + " finished."); + } + + public void setExitCode(int code) { + synchronized (this) { + exitCode = code; + finished = true; + commandFinished(id); + this.notifyAll(); + } + } + + /** + * Close the shell + * + * @param reason + */ + public void terminate(String reason) { + try { + shell.close(); + Log.d(RootCommands.TAG, "Terminating the shell."); + terminated(reason); + } catch (IOException e) { + } + } + + public void terminated(String reason) { + setExitCode(-1); + Log.d(RootCommands.TAG, "Command " + id + " did not finish, because of " + reason); + } + + /** + * Waits for this command to finish and forwards exitCode into afterExecution method + * + * @throws TimeoutException + * @throws BrokenBusyboxException + */ + public void waitForFinish() throws TimeoutException, BrokenBusyboxException { + synchronized (this) { + while (!finished) { + try { + this.wait(timeout); + } catch (InterruptedException e) { + Log.e(RootCommands.TAG, "InterruptedException in waitForFinish()", e); + } + + if (!finished) { + finished = true; + terminate("Timeout"); + throw new TimeoutException("Timeout has occurred."); + } + } + + if (brokenBusyboxDetected) { + throw new BrokenBusyboxException(); + } + + processAfterExecution(exitCode); + } + } + +} \ No newline at end of file diff --git a/lib/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/command/ExecutableCommand.java b/lib/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/command/ExecutableCommand.java new file mode 100644 index 000000000..6c008a212 --- /dev/null +++ b/lib/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/command/ExecutableCommand.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2012 Dominik Schürmann + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.sufficientlysecure.rootcommands.command; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.os.Build; + +import java.io.File; + +public abstract class ExecutableCommand extends Command { + public static final String EXECUTABLE_PREFIX = "lib"; + public static final String EXECUTABLE_SUFFIX = "_exec.so"; + + /** + * This class provides a way to use your own binaries! + *

+ * Include your own executables, renamed from * to lib*_exec.so, in your libs folder under the + * architecture directories. Now they will be deployed by Android the same way libraries are + * deployed! + *

+ * See README for more information how to use your own executables! + * + * @param context + * @param executableName + * @param parameters + */ + public ExecutableCommand(Context context, String executableName, String parameters) { + super(getLibDirectory(context) + File.separator + EXECUTABLE_PREFIX + executableName + + EXECUTABLE_SUFFIX + " " + parameters); + } + + /** + * Get full path to lib directory of app + * + * @return dir as String + */ + @SuppressLint("NewApi") + private static String getLibDirectory(Context context) { + if (Build.VERSION.SDK_INT >= 9) { + return context.getApplicationInfo().nativeLibraryDir; + } else { + return context.getApplicationInfo().dataDir + File.separator + "lib"; + } + } + + public abstract void output(int id, String line); + + public abstract void afterExecution(int id, int exitCode); + +} \ No newline at end of file diff --git a/lib/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/command/SimpleCommand.java b/lib/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/command/SimpleCommand.java new file mode 100644 index 000000000..9049040f0 --- /dev/null +++ b/lib/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/command/SimpleCommand.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2012 Dominik Schürmann + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.sufficientlysecure.rootcommands.command; + +public class SimpleCommand extends Command { + private StringBuilder sb = new StringBuilder(); + + public SimpleCommand(String... command) { + super(command); + } + + @Override + public void output(int id, String line) { + sb.append(line).append('\n'); + } + + @Override + public void afterExecution(int id, int exitCode) { + } + + public String getOutput() { + return sb.toString(); + } + + public int getExitCode() { + return exitCode; + } + +} \ No newline at end of file diff --git a/lib/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/command/SimpleExecutableCommand.java b/lib/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/command/SimpleExecutableCommand.java new file mode 100644 index 000000000..95d2faefb --- /dev/null +++ b/lib/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/command/SimpleExecutableCommand.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2012 Dominik Schürmann + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.sufficientlysecure.rootcommands.command; + +import android.content.Context; + +public class SimpleExecutableCommand extends ExecutableCommand { + private StringBuilder sb = new StringBuilder(); + + public SimpleExecutableCommand(Context context, String executableName, String parameters) { + super(context, executableName, parameters); + } + + @Override + public void output(int id, String line) { + sb.append(line).append('\n'); + } + + @Override + public void afterExecution(int id, int exitCode) { + } + + public String getOutput() { + return sb.toString(); + } + + public int getExitCode() { + return exitCode; + } + +} \ No newline at end of file diff --git a/lib/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/util/BrokenBusyboxException.java b/lib/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/util/BrokenBusyboxException.java new file mode 100644 index 000000000..e982b2421 --- /dev/null +++ b/lib/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/util/BrokenBusyboxException.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2012 Dominik Schürmann + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.sufficientlysecure.rootcommands.util; + +import java.io.IOException; + +public class BrokenBusyboxException extends IOException { + private static final long serialVersionUID = 8337358201589488409L; + + public BrokenBusyboxException() { + super(); + } + + public BrokenBusyboxException(String detailMessage) { + super(detailMessage); + } + +} diff --git a/lib/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/util/Log.java b/lib/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/util/Log.java new file mode 100644 index 000000000..337c49d41 --- /dev/null +++ b/lib/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/util/Log.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2012 Dominik Schürmann + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.sufficientlysecure.rootcommands.util; + +import org.sufficientlysecure.rootcommands.RootCommands; + +/** + * Wraps Android Logging to enable or disable debug output using Constants + */ +public final class Log { + + public static void v(String tag, String msg) { + if (RootCommands.DEBUG) { + android.util.Log.v(tag, msg); + } + } + + public static void v(String tag, String msg, Throwable tr) { + if (RootCommands.DEBUG) { + android.util.Log.v(tag, msg, tr); + } + } + + public static void d(String tag, String msg) { + if (RootCommands.DEBUG) { + android.util.Log.d(tag, msg); + } + } + + public static void d(String tag, String msg, Throwable tr) { + if (RootCommands.DEBUG) { + android.util.Log.d(tag, msg, tr); + } + } + + public static void i(String tag, String msg) { + if (RootCommands.DEBUG) { + android.util.Log.i(tag, msg); + } + } + + public static void i(String tag, String msg, Throwable tr) { + if (RootCommands.DEBUG) { + android.util.Log.i(tag, msg, tr); + } + } + + public static void w(String tag, String msg) { + android.util.Log.w(tag, msg); + } + + public static void w(String tag, String msg, Throwable tr) { + android.util.Log.w(tag, msg, tr); + } + + public static void w(String tag, Throwable tr) { + android.util.Log.w(tag, tr); + } + + public static void e(String tag, String msg) { + android.util.Log.e(tag, msg); + } + + public static void e(String tag, String msg, Throwable tr) { + android.util.Log.e(tag, msg, tr); + } + +} diff --git a/lib/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/util/RootAccessDeniedException.java b/lib/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/util/RootAccessDeniedException.java new file mode 100644 index 000000000..35f353d1b --- /dev/null +++ b/lib/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/util/RootAccessDeniedException.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2012 Dominik Schürmann + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.sufficientlysecure.rootcommands.util; + +import java.io.IOException; + +public class RootAccessDeniedException extends IOException { + private static final long serialVersionUID = 9088998884166225540L; + + public RootAccessDeniedException() { + super(); + } + + public RootAccessDeniedException(String detailMessage) { + super(detailMessage); + } + +} diff --git a/lib/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/util/UnsupportedArchitectureException.java b/lib/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/util/UnsupportedArchitectureException.java new file mode 100644 index 000000000..96ad0309e --- /dev/null +++ b/lib/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/util/UnsupportedArchitectureException.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2012 Dominik Schürmann + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.sufficientlysecure.rootcommands.util; + +public class UnsupportedArchitectureException extends Exception { + private static final long serialVersionUID = 7826528799780001655L; + + public UnsupportedArchitectureException() { + super(); + } + + public UnsupportedArchitectureException(String detailMessage) { + super(detailMessage); + } + +} diff --git a/lib/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/util/Utils.java b/lib/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/util/Utils.java new file mode 100644 index 000000000..e04d4dbd0 --- /dev/null +++ b/lib/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/util/Utils.java @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2012 Dominik Schürmann + * Copyright (c) 2012 Michael Elsdörfer (Android Autostarts) + * Copyright (c) 2012 Stephen Erickson, Chris Ravenscroft, Adam Shanks (RootTools) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.sufficientlysecure.rootcommands.util; + +import org.sufficientlysecure.rootcommands.RootCommands; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Map; + +public class Utils { + /* + * The emulator and ADP1 device both have a su binary in /system/xbin/su, but it doesn't allow + * apps to use it (su app_29 $ su su: uid 10029 not allowed to su). + * + * Cyanogen used to have su in /system/bin/su, in newer versions it's a symlink to + * /system/xbin/su. + * + * The Archos tablet has it in /data/bin/su, since they don't have write access to /system yet. + */ + static final String[] BinaryPlaces = {"/data/bin/", "/system/bin/", "/system/xbin/", "/sbin/", + "/data/local/xbin/", "/data/local/bin/", "/system/sd/xbin/", "/system/bin/failsafe/", + "/data/local/"}; + + /** + * Determine the path of the su executable. + *

+ * Code from https://github.com/miracle2k/android-autostarts, use under Apache License was + * agreed by Michael Elsdörfer + */ + public static String getSuPath() { + for (String p : BinaryPlaces) { + File su = new File(p + "su"); + if (su.exists()) { + Log.d(RootCommands.TAG, "su found at: " + p); + return su.getAbsolutePath(); + } else { + Log.v(RootCommands.TAG, "No su in: " + p); + } + } + Log.d(RootCommands.TAG, "No su found in a well-known location, " + "will just use \"su\"."); + return "su"; + } + + /** + * This code is adapted from java.lang.ProcessBuilder.start(). + *

+ * The problem is that Android doesn't allow us to modify the map returned by + * ProcessBuilder.environment(), even though the docstring indicates that it should. This is + * because it simply returns the SystemEnvironment object that System.getenv() gives us. The + * relevant portion in the source code is marked as "// android changed", so presumably it's not + * the case in the original version of the Apache Harmony project. + *

+ * Note that simply passing the environment variables we want to Process.exec won't be good + * enough, since that would override the environment we inherited completely. + *

+ * We needed to be able to set a CLASSPATH environment variable for our new process in order to + * use the "app_process" command directly. Note: "app_process" takes arguments passed on to the + * Dalvik VM as well; this might be an alternative way to set the class path. + *

+ * Code from https://github.com/miracle2k/android-autostarts, use under Apache License was + * agreed by Michael Elsdörfer + */ + public static Process runWithEnv(String command, ArrayList customAddedEnv, + String baseDirectory) throws IOException { + + Map environment = System.getenv(); + String[] envArray = new String[environment.size() + + (customAddedEnv != null ? customAddedEnv.size() : 0)]; + int i = 0; + for (Map.Entry entry : environment.entrySet()) { + envArray[i++] = entry.getKey() + "=" + entry.getValue(); + } + if (customAddedEnv != null) { + for (String entry : customAddedEnv) { + envArray[i++] = entry; + } + } + + Process process; + if (baseDirectory == null) { + process = Runtime.getRuntime().exec(command, envArray, null); + } else { + process = Runtime.getRuntime().exec(command, envArray, new File(baseDirectory)); + } + return process; + } +} diff --git a/settings.gradle b/settings.gradle index e7b4def49..1ae406402 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1 @@ -include ':app' +include ':app', ':lib:RootCommands' \ No newline at end of file