diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..23de6e2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,88 @@ +# Built application files +*.apk +*.aar +*.ap_ +*.aab + +# Files for the ART/Dalvik VM +*.dex + +# Java class files +*.class + +# Generated files +bin/ +gen/ +out/ +# Uncomment the following line in case you need and you don't have the release build type files in your app +# release/ + +# Gradle files +.gradle/ +build/ + +# Local configuration file (sdk path, etc) +local.properties + +# Proguard folder generated by Eclipse +proguard/ + +# Log Files +*.log + +# Android Studio Navigation editor temp files +.navigation/ + +# Android Studio captures folder +captures/ + +# IntelliJ +*.iml +.idea/workspace.xml +.idea/tasks.xml +.idea/gradle.xml +.idea/assetWizardSettings.xml +.idea/dictionaries +.idea/libraries +# Android Studio 3 in .gitignore file. +.idea/caches +.idea/modules.xml +# Comment next line if keeping position of elements in Navigation Editor is relevant for you +.idea/navEditor.xml + +# Keystore files +# Uncomment the following lines if you do not want to check your keystore files in. +#*.jks +#*.keystore + +# External native build folder generated in Android Studio 2.2 and later +.externalNativeBuild +.cxx/ + +# Google Services (e.g. APIs or Firebase) +# google-services.json + +# Freeline +freeline.py +freeline/ +freeline_project_description.json + +# fastlane +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots +fastlane/test_output +fastlane/readme.md + +# Version control +vcs.xml + +# lint +lint/intermediates/ +lint/generated/ +lint/outputs/ +lint/tmp/ +# lint/reports/ + +# Android Profiling +*.hprof diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 0000000..e0e4c70 --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +USB Gadget Tool \ No newline at end of file diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 0000000..681f41a --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,116 @@ + + + + + + + +
+ + + + xmlns:android + + ^$ + + + +
+
+ + + + xmlns:.* + + ^$ + + + BY_NAME + +
+
+ + + + .*:id + + http://schemas.android.com/apk/res/android + + + +
+
+ + + + .*:name + + http://schemas.android.com/apk/res/android + + + +
+
+ + + + name + + ^$ + + + +
+
+ + + + style + + ^$ + + + +
+
+ + + + .* + + ^$ + + + BY_NAME + +
+
+ + + + .* + + http://schemas.android.com/apk/res/android + + + ANDROID_ATTRIBUTE_ORDER + +
+
+ + + + .* + + .* + + + BY_NAME + +
+
+
+
+
+
\ No newline at end of file diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml new file mode 100644 index 0000000..a5f05cd --- /dev/null +++ b/.idea/jarRepositories.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..7bfef59 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml new file mode 100644 index 0000000..7f68460 --- /dev/null +++ b/.idea/runConfigurations.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..429f8ac --- /dev/null +++ b/README.md @@ -0,0 +1,16 @@ +# USB Gadget Tool +Convert your Android to any USB device you like! + +Following USB gadgets are integrated: +* Keyboard & Mouse (/dev/hidg0 & /dev/hidg1) +* FIDO CTAP (/dev/hidg0 - for WebAuthn) +* CCID (Chip Card Interface Device) + +## Roadmap +* root command logging in App +* Creating & enabling USB Gadgets during boot +* Import custom USB Gadget profiles + +## How does it work? +USB Gadget Tool uses ConfigFS - an userspace API inside the Linux Kernel - for creation of arbitrary USB composite devices. +https://www.kernel.org/doc/Documentation/filesystems/configfs/configfs.txt \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..3a1ea80 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,38 @@ +apply plugin: 'com.android.application' + +android { + compileSdkVersion 29 + + defaultConfig { + applicationId "net.tjado.usbgadget" + minSdkVersion 26 + targetSdkVersion 29 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } +} + +dependencies { + implementation fileTree(dir: "libs", include: ["*.jar"]) + implementation 'androidx.appcompat:appcompat:1.1.0' + implementation 'androidx.constraintlayout:constraintlayout:1.1.3' + implementation 'androidx.recyclerview:recyclerview:1.1.0' + implementation 'androidx.cardview:cardview:1.0.0' + testImplementation 'junit:junit:4.12' + androidTestImplementation 'androidx.test.ext:junit:1.1.1' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' + +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# 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 *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/androidTest/java/net/tjado/usbgadget/ExampleInstrumentedTest.java b/app/src/androidTest/java/net/tjado/usbgadget/ExampleInstrumentedTest.java new file mode 100644 index 0000000..06740ab --- /dev/null +++ b/app/src/androidTest/java/net/tjado/usbgadget/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package net.tjado.usbgadget; + +import android.content.Context; + +import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); + assertEquals("net.tjado.usbgadget", appContext.getPackageName()); + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..85cd48e --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/assets/configfs.sh b/app/src/main/assets/configfs.sh new file mode 100644 index 0000000..695a15f --- /dev/null +++ b/app/src/main/assets/configfs.sh @@ -0,0 +1 @@ +find /config/usb_gadget/ -exec ls -la {} \; 2>&1 \ No newline at end of file diff --git a/app/src/main/assets/usbGadgetProfiles/ccid.sh b/app/src/main/assets/usbGadgetProfiles/ccid.sh new file mode 100644 index 0000000..d778005 --- /dev/null +++ b/app/src/main/assets/usbGadgetProfiles/ccid.sh @@ -0,0 +1,32 @@ +#!/bin/sh + +CONFIGFS_DIR="/config" +GADGETS_PATH="${CONFIGFS_DIR}/usb_gadget" + +GADGET="ccid3" +GADGET_PATH=${GADGETS_PATH}/${GADGET} + +CONFIG_PATH="$GADGET_PATH/configs/c.1/" +STRINGS_PATH="$GADGET_PATH/strings/0x409/" + +mkdir -p $CONFIG_PATH +mkdir -p $STRINGS_PATH + +mkdir -p $GADGET_PATH/functions/ccid.usb0 +cd $GADGET_PATH/functions/ccid.usb0 + + +cd $GADGET_PATH +echo 0xa4ac > idVendor +echo 0x0525 > idProduct + +cd $STRINGS_PATH +echo "tejado" > manufacturer +echo "CCID" > product +echo "42" > serialnumber + +cd $CONFIG_PATH +echo 30 > MaxPower +echo "CCID Configuration" > strings/0x409/configuration + +ln -s ${GADGET_PATH}/functions/ccid.usb0 $CONFIG_PATH/ccid.usb0 diff --git a/app/src/main/assets/usbGadgetProfiles/ctap.sh b/app/src/main/assets/usbGadgetProfiles/ctap.sh new file mode 100644 index 0000000..e563bb7 --- /dev/null +++ b/app/src/main/assets/usbGadgetProfiles/ctap.sh @@ -0,0 +1,47 @@ +#!/bin/sh + +CONFIGFS_DIR="/config" +GADGETS_PATH="${CONFIGFS_DIR}/usb_gadget" + +GADGET="ctap3" +GADGET_PATH=${GADGETS_PATH}/${GADGET} + +CONFIG_PATH="$GADGET_PATH/configs/c.1/" +STRINGS_PATH="$GADGET_PATH/strings/0x409/" + +mkdir -p $CONFIG_PATH +mkdir -p $STRINGS_PATH + +mkdir -p $GADGET_PATH/functions/hid.usb0 +cd $GADGET_PATH/functions/hid.usb0 + +# HID protocol (according to USB spec: 1 for keyboard) +echo 0 > protocol +# device subclass +echo 0 > subclass +# number of bytes per record +echo 64 > report_length + +# writing report descriptor +echo -ne \\x06\\xD0\\xF1\\x09\\x01\\xA1\\x01\\x09\\x20\\x15\\x00\\x26\\xFF\\x00\\x75\\x08\\x95\\x40\\x81\\x02\\x09\\x21\\x15\\x00\\x26\\xFF\\x00\\x75\\x08\\x95\\x40\\x91\\x02\\xC0 > report_desc + +cd $GADGET_PATH +echo '0xa4ac' > idVendor +echo '0x0525' > idProduct + +echo '0x0512' > bcdDevice +echo '0x0200' > bcdUSB +echo 0 > bDeviceProtocol +echo 0 > bDeviceSubClass +echo 8 > bMaxPacketSize0 + +cd $STRINGS_PATH +echo "tejado" > manufacturer +echo "CTAP" > product +echo "42" > serialnumber + +cd $CONFIG_PATH +echo 30 > MaxPower +echo "HID Configuration" > strings/0x409/configuration + +ln -s ${GADGET_PATH}/functions/hid.usb0 $CONFIG_PATH/hid.usb0 \ No newline at end of file diff --git a/app/src/main/assets/usbGadgetProfiles/mouse+keyboard.sh b/app/src/main/assets/usbGadgetProfiles/mouse+keyboard.sh new file mode 100644 index 0000000..37d4792 --- /dev/null +++ b/app/src/main/assets/usbGadgetProfiles/mouse+keyboard.sh @@ -0,0 +1,56 @@ +#!/bin/sh + +CONFIGFS_DIR="/config" +GADGETS_PATH="${CONFIGFS_DIR}/usb_gadget" + +GADGET="keyboard" +GADGET_PATH=${GADGETS_PATH}/${GADGET} + +CONFIG_PATH="$GADGET_PATH/configs/c.1/" +STRINGS_PATH="$GADGET_PATH/strings/0x409/" + +mkdir -p $CONFIG_PATH +mkdir -p $STRINGS_PATH + +mkdir -p $GADGET_PATH/functions/hid.usb0 +cd $GADGET_PATH/functions/hid.usb0 + +# HID protocol (according to USB spec: 1 for keyboard) +echo 1 > protocol +# device subclass +echo 1 > subclass +# number of bytes per record +echo 8 > report_length + +# writing report descriptor +echo -ne \\x05\\x01\\x09\\x06\\xa1\\x01\\x05\\x07\\x19\\xe0\\x29\\xe7\\x15\\x00\\x25\\x01\\x75\\x01\\x95\\x08\\x81\\x02\\x95\\x01\\x75\\x08\\x81\\x03\\x95\\x05\\x75\\x01\\x05\\x08\\x19\\x01\\x29\\x05\\x91\\x02\\x95\\x01\\x75\\x03\\x91\\x03\\x95\\x06\\x75\\x08\\x15\\x00\\x25\\x65\\x05\\x07\\x19\\x00\\x29\\x65\\x81\\x00\\xc0 > report_desc + +mkdir -p $GADGET_PATH/functions/hid.usb1 +cd $GADGET_PATH/functions/hid.usb1 + +# HID protocol (according to USB spec: 2 for mouse) +echo 2 > protocol +# device subclass +echo 1 > subclass +# number of bytes per record +echo 4 > report_length + +# writing report descriptor +echo -ne \\x05\\x01\\x09\\x02\\xa1\\x01\\x09\\x01\\xa1\\x00\\x05\\x09\\x19\\x01\\x29\\x05\\x15\\x00\\x25\\x01\\x95\\x05\\x75\\x01\\x81\\x02\\x95\\x01\\x75\\x03\\x81\\x01\\x05\\x01\\x09\\x30\\x09\\x31\\x09\\x38\\x15\\x81\\x25\\x7F\\x75\\x08\\x95\\x03\\x81\\x06\\xc0\\xc0 > report_desc + + +cd $GADGET_PATH +echo 0xa4ac > idVendor +echo 0x0525 > idProduct + +cd $STRINGS_PATH +echo "tejado" > manufacturer +echo "HID" > product +echo "42" > serialnumber + +cd $CONFIG_PATH +echo 120 > MaxPower +echo "HID Configuration" > strings/0x409/configuration + +ln -s ${GADGET_PATH}/functions/hid.usb0 $CONFIG_PATH/hid.usb0 +ln -s ${GADGET_PATH}/functions/hid.usb1 $CONFIG_PATH/hid.usb1 \ No newline at end of file diff --git a/app/src/main/java/net/tjado/usbgadget/ExecuteAsRootUtil.java b/app/src/main/java/net/tjado/usbgadget/ExecuteAsRootUtil.java new file mode 100644 index 0000000..b17f0a1 --- /dev/null +++ b/app/src/main/java/net/tjado/usbgadget/ExecuteAsRootUtil.java @@ -0,0 +1,242 @@ +/** + * Authorizer + * + * Copyright 2016 by Tjado Mäcke + * Licensed under GNU General Public License 3.0. + * + * @license GPL-3.0 + */ +package net.tjado.usbgadget; + +import android.util.Log; +import android.util.Pair; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Scanner; + +/* + * Class by muzikant + * + * Reference: + * http://muzikant-android.blogspot.com/2011/02/how-to-get-root-access-and-execute.html + * http://stackoverflow.com/a/7102780 + */ + +public class ExecuteAsRootUtil +{ + + public static boolean canRunRootCommands() + { + boolean retval = false; + Process suProcess; + + try + { + suProcess = Runtime.getRuntime().exec("su"); + + DataOutputStream os = new DataOutputStream(suProcess.getOutputStream()); + DataInputStream osRes = new DataInputStream(suProcess.getInputStream()); + + if (null != os && null != osRes) + { + // Getting the id of the current user to check if this is root + os.writeBytes("id\n"); + os.flush(); + + String currUid = osRes.readLine(); + boolean exitSu = false; + if (null == currUid) + { + retval = false; + exitSu = false; + Log.d("ROOT", "Can't get root access or denied by user"); + } + else if (true == currUid.contains("uid=0")) + { + retval = true; + exitSu = true; + Log.d("ROOT", "Root access granted"); + } + else + { + retval = false; + exitSu = true; + Log.d("ROOT", "Root access rejected: " + currUid); + } + + if (exitSu) + { + os.writeBytes("exit\n"); + os.flush(); + } + } + } + catch (Exception e) + { + // Can't get root ! + // Probably broken pipe exception on trying to write to output stream (os) after su failed, meaning that the device is not rooted + + retval = false; + Log.d("ROOT", "Root access rejected [" + e.getClass().getName() + "] : " + e.getMessage()); + } + + return retval; + } + + public static final Pair execute(String command) { + return execute(new String[]{ command }); + } + + public static final Pair execute(String[] commands) + { + Boolean retval = false; + String output = null; + + try + { + if (commands != null && commands.length > 0) + { + Process suProcess = Runtime.getRuntime().exec("su -"); + + DataOutputStream stdin = new DataOutputStream(suProcess.getOutputStream()); + InputStream stdout = suProcess.getInputStream(); + InputStream stderr = suProcess.getErrorStream(); + + for (String cmd: commands) { + Log.d("root", String.format("Execute command: %s", cmd)); + stdin.writeBytes(cmd); + stdin.flush(); + } + + stdin.writeBytes("exit\n"); + stdin.flush(); + + stdin.close(); + + StringBuffer sb = new StringBuffer(); + Scanner scanner = new Scanner(stdout); + while (scanner.hasNext()) { + sb.append(scanner.nextLine()); + sb.append(System.lineSeparator()); + } + + Scanner scannerErr = new Scanner(stderr); + while (scannerErr.hasNext()) { + Log.d("root", scannerErr.nextLine()); + } + + output = sb.toString(); + Log.d("root", output); + + try + { + int suProcessRetval = suProcess.waitFor(); + if (255 != suProcessRetval) + { + // Root access granted + retval = true; + } + else + { + // Root access denied + retval = false; + } + } + catch (Exception ex) + { + Log.e("ROOT", "Error executing root action", ex); + } + } + } + catch (IOException ex) + { + Log.w("ROOT", "Can't get root access", ex); + } + catch (SecurityException ex) + { + Log.w("ROOT", "Can't get root access", ex); + } + catch (Exception ex) + { + Log.w("ROOT", "Error executing internal operation", ex); + } + + return new Pair(retval, output); + } + + + public static final Pair executeBinaryOut(String[] commands) + { + Boolean retval = false; + byte[] output = new byte[64];; + + try + { + if (commands != null && commands.length > 0) + { + int count = 64; + Process suProcess = Runtime.getRuntime().exec("su -"); + + DataOutputStream stdin = new DataOutputStream(suProcess.getOutputStream()); + InputStream stdout = suProcess.getInputStream(); + InputStream stderr = suProcess.getErrorStream(); + + for (String cmd: commands) { + Log.d("root", String.format("Execute command: %s", cmd)); + stdin.writeBytes(cmd); + stdin.flush(); + } + + stdin.writeBytes("exit\n"); + stdin.flush(); + stdin.close(); + + int tmp = stdout.read(output, 0, 64); + Log.d("muh", String.format("read %d of data", tmp)); + stdout.close(); + + Scanner scannerErr = new Scanner(stderr); + while (scannerErr.hasNext()) { + Log.d("root", scannerErr.nextLine()); + } + + try + { + int suProcessRetval = suProcess.waitFor(); + if (255 != suProcessRetval) + { + // Root access granted + retval = true; + } + else + { + // Root access denied + retval = false; + } + } + catch (Exception ex) + { + Log.e("ROOT", "Error executing root action", ex); + } + } + } + catch (IOException ex) + { + Log.w("ROOT", "Can't get root access", ex); + } + catch (SecurityException ex) + { + Log.w("ROOT", "Can't get root access", ex); + } + catch (Exception ex) + { + Log.w("ROOT", "Error executing internal operation", ex); + } + + return new Pair(retval, output); + } + +} \ No newline at end of file diff --git a/app/src/main/java/net/tjado/usbgadget/GadgetAdapter.java b/app/src/main/java/net/tjado/usbgadget/GadgetAdapter.java new file mode 100644 index 0000000..89cb9e3 --- /dev/null +++ b/app/src/main/java/net/tjado/usbgadget/GadgetAdapter.java @@ -0,0 +1,129 @@ +package net.tjado.usbgadget; + +import android.app.Activity; +import android.graphics.Color; +import android.text.Html; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.TextView; + +import java.util.Collections; +import java.util.List; + +import androidx.cardview.widget.CardView; +import androidx.recyclerview.widget.RecyclerView; + +public class GadgetAdapter extends RecyclerView.Adapter { + private Activity context; + private LayoutInflater inflater; + List data = Collections.emptyList(); + + public GadgetAdapter(Activity context, List data) { + this.context = context; + // inflater = LayoutInflater.from(context); + this.data = data; + } + + // Inflate the layout when viewholder created + @Override + public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + + View view = inflater.from(parent.getContext()). + inflate(R.layout.cardview_gadget, parent, false); + return new DefaultViewHolder(view); + } + + @Override + public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { + + initLayoutDefault((DefaultViewHolder) holder, position); + } + + + @Override + public int getItemCount() { + return data.size(); + } + + private void initLayoutDefault(DefaultViewHolder holder, int pos) { + holder.gadget = data.get(pos); + holder.manufacturer.setText(holder.gadget.getValue("manufacturer")); + holder.product.setText(holder.gadget.getValue("product")); + holder.serialnumber.setText(holder.gadget.getValue("serialnumber")); + holder.path.setText(holder.gadget.getValue("gadget_path")); + holder.functions.setText(Html.fromHtml(holder.gadget.getFormattedFunctions(), Html.FROM_HTML_MODE_LEGACY)); + + String udc = holder.gadget.getValue("udc"); + holder.udc.setText(udc); + if (holder.gadget.isActivated()) { + holder.card.setCardBackgroundColor(Color.GREEN); + holder.activate.setText(R.string.deactivate); + } else { + holder.activate.setText(R.string.activate); + holder.card.setCardBackgroundColor(Color.WHITE); + } + + } + + // Static inner class to initialize the views of rows + static class DefaultViewHolder extends RecyclerView.ViewHolder { + GadgetObject gadget; + CardView card; + TextView manufacturer; + TextView product; + TextView serialnumber; + TextView udc; + TextView functions; + TextView path; + Button activate; + + public DefaultViewHolder(View itemView) { + super(itemView); + card = (CardView) itemView.findViewById(R.id.cv_gadget); + manufacturer = (TextView) itemView.findViewById(R.id.tv_gadget_manufacturer); + product = (TextView) itemView.findViewById(R.id.tv_gadget_product); + serialnumber = (TextView) itemView.findViewById(R.id.tv_gadget_sn); + udc = (TextView) itemView.findViewById(R.id.tv_gadget_udc); + path = (TextView) itemView.findViewById(R.id.tv_gadget_path); + functions = (TextView) itemView.findViewById(R.id.tv_gadget_functions); + activate = (Button) itemView.findViewById(R.id.activate); + + activate.setOnClickListener(view -> { + String gadgetPath = gadget.getValue("gadget_path"); + String[] commands = {}; + + if (gadgetPath == null || !gadgetPath.startsWith("/config/")) { + Log.d("root", String.format("gadgetPath errornous value: %s", gadgetPath)); + return; + } + + if (gadget.isActivated()) { + // deactivate specific gadget + String cmdDeactivateUsb = String.format("echo \"\" > %s/UDC\n", gadgetPath); + + commands = new String[]{cmdDeactivateUsb}; + } else { + // deactivate all + String cmdDeactivateUsbAll = "find /config/usb_gadget/ -name UDC -type f -exec sh -c 'echo \"\" > \"$@\"' _ {} \\;\n"; + // activate clicked one + String cmdActivateUsb = String.format("getprop sys.usb.controller > %s/UDC\n", gadgetPath); + + commands = new String[]{cmdDeactivateUsbAll, cmdActivateUsb}; + } + + RootTask mTask = new RootTask(itemView.getContext(), commands, response -> { + //mTextView.setText((String) response); + }); + mTask.execute(); + + ((MainActivity) itemView.getContext()).getGadgetData(); + return; + }); + } + + } + +} \ No newline at end of file diff --git a/app/src/main/java/net/tjado/usbgadget/GadgetObject.java b/app/src/main/java/net/tjado/usbgadget/GadgetObject.java new file mode 100644 index 0000000..b702ad0 --- /dev/null +++ b/app/src/main/java/net/tjado/usbgadget/GadgetObject.java @@ -0,0 +1,89 @@ +package net.tjado.usbgadget; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.SortedSet; +import java.util.TreeSet; + +public class GadgetObject { + protected String mText1; + protected HashMap values = new HashMap<>(); + protected SortedSet functions = new TreeSet<>(); + protected SortedSet activeFunctions = new TreeSet<>(); + + private boolean isSelected; + + public GadgetObject() { + } + + public GadgetObject(String text) { + this.setmText1(text); + } + + public String getmText1() { + return mText1; + } + + public void setmText1(String mText1) { + this.mText1 = mText1; + } + + public void setValue(String key, String value) { + values.put(key, value); + } + + public String getValue(String key) { + try { + return values.get(key); + } + catch (Exception e) { + return "not set"; + } + } + + public void addFunction(String f) { + functions.add(f); + } + + public SortedSet getFunctions(String key) { + return functions; + } + + public void addActiveFunction(String f) { + activeFunctions.add(f); + } + + public SortedSet getActiveFunctions(String key) { + return activeFunctions; + } + + public Boolean isActivated() { + String udc = getValue("udc"); + if (udc.equals("") || udc.equals("not set")) { + return false; + } else { + return true; + } + } + + public String getFormattedFunctions() { + StringBuffer sb = new StringBuffer(); + String color = ""; + + Iterator it = functions.iterator(); + while( it.hasNext() ) { + String item = it.next(); + + if( activeFunctions.contains(item)) { + color = "#008000"; + } else { + color = "#ff0000"; + } + + sb.append(String.format("%s
", color, item)); + } + + return sb.toString(); + } + +} \ No newline at end of file diff --git a/app/src/main/java/net/tjado/usbgadget/MainActivity.java b/app/src/main/java/net/tjado/usbgadget/MainActivity.java new file mode 100644 index 0000000..a745021 --- /dev/null +++ b/app/src/main/java/net/tjado/usbgadget/MainActivity.java @@ -0,0 +1,193 @@ +package net.tjado.usbgadget; + +import android.content.DialogInterface; +import android.os.Bundle; +import android.util.Log; +import android.view.Menu; +import android.view.MenuItem; +import android.widget.ViewFlipper; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.StringReader; +import java.util.ArrayList; +import java.util.List; +import java.util.Scanner; + +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.app.AppCompatActivity; +import androidx.recyclerview.widget.GridLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +public class MainActivity extends AppCompatActivity { + + private GadgetAdapter gadgetAdapter; + private RecyclerView gadgetRecyclerView; + + List data; + String [] gadgetProfileList; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + + gadgetRecyclerView = (RecyclerView) findViewById(R.id.rv_gadgets); + data = new ArrayList<>(); + + if( ExecuteAsRootUtil.canRunRootCommands() ) { + getGadgetData(); + } else { + ((ViewFlipper) findViewById(R.id.flipper)).showNext(); + } + + + try { + gadgetProfileList = getAssets().list("usbGadgetProfiles/"); + } catch (IOException e) { + gadgetProfileList = null; + } + + gadgetAdapter = new GadgetAdapter(this, data); + gadgetRecyclerView.setAdapter(gadgetAdapter); + GridLayoutManager gridLayoutManager = new GridLayoutManager(this,1); + gadgetRecyclerView.setLayoutManager(gridLayoutManager); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.main, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + int id = item.getItemId(); + + switch (id) { + case R.id.action_add: + addGadget(); + break; + case R.id.action_refresh: + ((ViewFlipper) findViewById(R.id.flipper)).setDisplayedChild(0); + if( ExecuteAsRootUtil.canRunRootCommands() ) { + getGadgetData(); + } else { + ((ViewFlipper) findViewById(R.id.flipper)).showNext(); + } + return true; + case R.id.info: + showInfo(); + return true; + } + + return super.onOptionsItemSelected(item); + } + + public void getGadgetData() { + String cmd = "for dir in /config/usb_gadget/*/; do echo GADGET_PATH=$dir; cd $dir; if [ \"$?\" -ne \"0\" ]; then echo \"Error - not able to change dir to $dir... exit\"; exit 1; fi; echo UDC=$(cat UDC); find ./configs/ -type l -exec sh -c 'echo FUNCTIONS_ACTIVE=$(basename $(readlink \"$@\"))' _ {} \\;; for f in ./functions/*/; do echo FUNCTIONS=$(basename $f); done; cd ./strings/0x409/; for vars in *; do echo ${vars}=$(cat $vars); done; echo \"=============\"; done; \n"; + + RootTask mTask = new RootTask(this.getApplicationContext(), cmd, response -> { + //mTextView.setText((String) Response); + BufferedReader bufReader = new BufferedReader(new StringReader(response)); + data.clear(); + GadgetObject gadget = new GadgetObject(); + + try { + String line; + while ((line = bufReader.readLine()) != null) { + if(line.equals("=============")) { + data.add(gadget); + gadget = new GadgetObject(); + continue; + } + + String[] parts = line.split("=",2); + + if( parts[0].equals("FUNCTIONS") ) { + gadget.addFunction(parts[1]); + } else if( parts[0].equals("FUNCTIONS_ACTIVE") ) { + gadget.addActiveFunction(parts[1]); + } else { + gadget.setValue(parts[0].toLowerCase(), parts[1]); + } + } + } + catch (Exception e) { + Log.d("root", e.getMessage()); + e.printStackTrace(); + } + + gadgetAdapter.notifyDataSetChanged(); + + }); + mTask.execute(); + } + + protected void showInfo() { + final AlertDialog.Builder alertBuilder = new AlertDialog.Builder(this); + alertBuilder.setTitle(R.string.info_title); + alertBuilder.setCancelable(false); + + alertBuilder.setMessage(R.string.info_message); + alertBuilder.setPositiveButton("Ok", new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialoginterface, int i) { + }}); + + alertBuilder.show(); + } + + protected void addGadget() { + final AlertDialog.Builder alertBuilder = new AlertDialog.Builder(this); + alertBuilder.setTitle("Add Gadget"); + alertBuilder.setCancelable(true); + + alertBuilder.setItems(gadgetProfileList, (dialog, which) -> loadGadgetProfileFromAsset(gadgetProfileList[which])); + + alertBuilder.show(); + } + + protected String getGadgetProfile(String assetFile) { + BufferedReader reader = null; + try { + InputStream is = getAssets().open(String.format("usbGadgetProfiles/%s", assetFile)); + Scanner scanner = new Scanner(is); + + StringBuffer sb = new StringBuffer(); + while (scanner.hasNext()) { + sb.append(scanner.nextLine() + "\n"); + } + return sb.toString(); + } catch (IOException e) { + e.printStackTrace(); + return null; + } + } + + protected Boolean loadGadgetProfileFromAsset(String assetFile) { + String profile = getGadgetProfile(assetFile); + + if (profile == null || profile.equals("")) { + return false; + } + + RootTask mTask = new RootTask(this.getApplicationContext(), profile, response -> { + getGadgetData(); + }); + mTask.execute(); + + return true; + } + + protected void runShell() { + String cmd = "find /config/usb_gadget/ -exec ls -la {} \\; 2>&1\n"; + RootTask mTask = new RootTask(this.getApplicationContext(), cmd, response -> { + //mTextView.setText((String) response); + }); + mTask.execute(); + + //Pair cr = ExecuteAsRootUtil.execute("find /config/usb_gadget/ -exec ls -la {} \\; 2>&1\n"); + //mTextView.setText((String) cr.second); + } +} \ No newline at end of file diff --git a/app/src/main/java/net/tjado/usbgadget/RootTask.java b/app/src/main/java/net/tjado/usbgadget/RootTask.java new file mode 100644 index 0000000..a702a04 --- /dev/null +++ b/app/src/main/java/net/tjado/usbgadget/RootTask.java @@ -0,0 +1,45 @@ +package net.tjado.usbgadget; + +import android.content.Context; +import android.os.AsyncTask; +import android.util.Pair; + +public class RootTask extends AsyncTask { + OnRootTaskListener listener; + Context mContext; + String[] mCommands; + + public interface OnRootTaskListener { + public void OnRootTaskFinish(String Response); + } + + public RootTask(Context ctx, String command, OnRootTaskListener listener) { + mContext = ctx; + mCommands = new String[] {command}; + this.listener = listener; + } + + public RootTask(Context ctx, String[] commands, OnRootTaskListener listener) { + mContext = ctx; + mCommands = commands; + this.listener = listener; + } + + @Override + protected String doInBackground(Void... params) { + Pair cr = ExecuteAsRootUtil.execute(mCommands); + return (String) cr.second; + } + + @Override + protected void onPreExecute() { + super.onPreExecute(); + } + + @Override + protected void onPostExecute(String result) { + if( listener != null ) { + listener.OnRootTaskFinish(result); + } + } +} diff --git a/app/src/main/res/drawable-v24/baseline_add_circle_outline_24.xml b/app/src/main/res/drawable-v24/baseline_add_circle_outline_24.xml new file mode 100644 index 0000000..a1c721d --- /dev/null +++ b/app/src/main/res/drawable-v24/baseline_add_circle_outline_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable-v24/baseline_help_24.xml b/app/src/main/res/drawable-v24/baseline_help_24.xml new file mode 100644 index 0000000..c0c9268 --- /dev/null +++ b/app/src/main/res/drawable-v24/baseline_help_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable-v24/baseline_info_24.xml b/app/src/main/res/drawable-v24/baseline_info_24.xml new file mode 100644 index 0000000..17255b7 --- /dev/null +++ b/app/src/main/res/drawable-v24/baseline_info_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable-v24/baseline_loop_24.xml b/app/src/main/res/drawable-v24/baseline_loop_24.xml new file mode 100644 index 0000000..c2f773a --- /dev/null +++ b/app/src/main/res/drawable-v24/baseline_loop_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..8f12af7 --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/cardview_gadget.xml b/app/src/main/res/layout/cardview_gadget.xml new file mode 100644 index 0000000..65b6c01 --- /dev/null +++ b/app/src/main/res/layout/cardview_gadget.xml @@ -0,0 +1,162 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +