├── .idea ├── .name ├── compiler.xml ├── vcs.xml ├── misc.xml ├── gradle.xml ├── jarRepositories.xml ├── codeStyles │ └── Project.xml └── inspectionProfiles │ └── Project_Default.xml ├── jitpack.yml ├── libusbmanager ├── .gitignore ├── src │ └── main │ │ ├── AndroidManifest.xml │ │ ├── res │ │ └── values │ │ │ └── strings.xml │ │ └── java │ │ └── green_green_avk │ │ └── libusbmanager │ │ └── LibUsbManager.java └── build.gradle ├── settings.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .gitignore ├── LICENSE ├── README.md ├── gradle.properties ├── gradlew.bat └── gradlew /.idea/.name: -------------------------------------------------------------------------------- 1 | LibUsb Manager -------------------------------------------------------------------------------- /jitpack.yml: -------------------------------------------------------------------------------- 1 | jdk: 2 | - openjdk17 3 | -------------------------------------------------------------------------------- /libusbmanager/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name='LibUsb Manager' 2 | include ':libusbmanager' 3 | -------------------------------------------------------------------------------- /libusbmanager/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/green-green-avk/LibUsbManager/master/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sun Jul 03 15:59:15 PDT 2022 2 | distributionBase=GRADLE_USER_HOME 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-all.zip 4 | distributionPath=wrapper/dists 5 | zipStorePath=wrapper/dists 6 | zipStoreBase=GRADLE_USER_HOME 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | /.idea/shelf/ 11 | .DS_Store 12 | .directory 13 | /build 14 | /captures 15 | .externalNativeBuild 16 | .cxx 17 | -------------------------------------------------------------------------------- /libusbmanager/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | LibUSB helper: %s 4 | Cannot obtain USB service 5 | No device found: %s 6 | Unable to open device: %s 7 | Spoofing detected! 8 | 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2023 Aleksandr Kiselev 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 19 | 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LibUsb Manager 2 | 3 | [![JitPack](https://jitpack.io/v/green-green-avk/LibUsbManager.svg)](https://jitpack.io/#green-green-avk/LibUsbManager) 4 | [![javadoc](https://img.shields.io/badge/javadoc-latest-green)](https://javadoc.jitpack.io/com/github/green-green-avk/LibUsbManager/latest/javadoc/) 5 | 6 | A **libusb** manager for **Termux**-like applications. 7 | 8 | Device enumeration and hot plug/unplug events are supported. 9 | 10 | To be used with **green-green-avk/libusb**: 11 | 12 | Branches: 13 | 14 | * [v1.0.23-android-libusbmanager](https://github.com/green-green-avk/libusb/tree/v1.0.23-android-libusbmanager) 15 | * [v1.0.26-android-libusbmanager](https://github.com/green-green-avk/libusb/tree/v1.0.26-android-libusbmanager) 16 | 17 | *Please, request if you want to support other versions.* 18 | -------------------------------------------------------------------------------- /.idea/jarRepositories.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | 19 | 20 | 24 | 25 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048M -Dkotlin.daemon.jvm.options\="-Xmx2048M" 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app's APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Automatically convert third-party libraries to use AndroidX 19 | android.enableJetifier=true 20 | # Enables namespacing of each library's R class so that its R class includes only the 21 | # resources declared in the library itself and none from the library's dependencies, 22 | # thereby reducing the size of the R class for that library 23 | android.nonTransitiveRClass=true 24 | android.defaults.buildfeatures.buildconfig=false 25 | android.nonFinalResIds=false 26 | -------------------------------------------------------------------------------- /libusbmanager/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.library' 3 | id 'maven-publish' 4 | } 5 | 6 | group = 'com.github.green-green-avk' 7 | version = '1.11' 8 | 9 | android { 10 | namespace 'green_green_avk.libusbmanager' 11 | 12 | compileSdk 33 13 | 14 | compileOptions { 15 | sourceCompatibility JavaVersion.VERSION_1_8 16 | targetCompatibility JavaVersion.VERSION_1_8 17 | } 18 | 19 | defaultConfig { 20 | minSdk 14 21 | targetSdk 33 22 | versionName version 23 | } 24 | 25 | publishing { 26 | singleVariant('release') { 27 | // withJavadocJar() // Useless as it can't Java fields and {@code ...} at least. 28 | withSourcesJar() 29 | } 30 | } 31 | } 32 | 33 | dependencies { 34 | implementation 'androidx.annotation:annotation:1.5.0' 35 | } 36 | 37 | tasks.register('javadoc', Javadoc) { 38 | failOnError true 39 | source = android.sourceSets.main.java.srcDirs 40 | classpath += project.files(android.getBootClasspath().join(File.pathSeparator)) 41 | options { 42 | addStringOption('link', 'https://developer.android.com/reference') 43 | } 44 | android.libraryVariants.configureEach { variant -> 45 | if (variant.name == 'release') { 46 | owner.classpath += variant.javaCompileProvider.get().classpath 47 | } 48 | } 49 | } 50 | 51 | tasks.register('javadocJar', Jar) { 52 | dependsOn javadoc 53 | archiveClassifier.set 'javadoc' 54 | from javadoc.destinationDir 55 | } 56 | 57 | publishing { 58 | publications { 59 | release(MavenPublication) { 60 | afterEvaluate { 61 | from components.release 62 | artifact(javadocJar) 63 | } 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
7 | 8 | 9 | 10 | xmlns:android 11 | 12 | ^$ 13 | 14 | 15 | 16 |
17 |
18 | 19 | 20 | 21 | xmlns:.* 22 | 23 | ^$ 24 | 25 | 26 | BY_NAME 27 | 28 |
29 |
30 | 31 | 32 | 33 | .*:id 34 | 35 | http://schemas.android.com/apk/res/android 36 | 37 | 38 | 39 |
40 |
41 | 42 | 43 | 44 | .*:name 45 | 46 | http://schemas.android.com/apk/res/android 47 | 48 | 49 | 50 |
51 |
52 | 53 | 54 | 55 | name 56 | 57 | ^$ 58 | 59 | 60 | 61 |
62 |
63 | 64 | 65 | 66 | style 67 | 68 | ^$ 69 | 70 | 71 | 72 |
73 |
74 | 75 | 76 | 77 | .* 78 | 79 | ^$ 80 | 81 | 82 | BY_NAME 83 | 84 |
85 |
86 | 87 | 88 | 89 | .* 90 | 91 | http://schemas.android.com/apk/res/android 92 | 93 | 94 | ANDROID_ATTRIBUTE_ORDER 95 | 96 |
97 |
98 | 99 | 100 | 101 | .* 102 | 103 | .* 104 | 105 | 106 | BY_NAME 107 | 108 |
109 |
110 |
111 |
112 |
113 |
-------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 105 | -------------------------------------------------------------------------------- /libusbmanager/src/main/java/green_green_avk/libusbmanager/LibUsbManager.java: -------------------------------------------------------------------------------- 1 | package green_green_avk.libusbmanager; 2 | 3 | import android.app.PendingIntent; 4 | import android.content.BroadcastReceiver; 5 | import android.content.Context; 6 | import android.content.Intent; 7 | import android.content.IntentFilter; 8 | import android.hardware.usb.UsbDevice; 9 | import android.hardware.usb.UsbDeviceConnection; 10 | import android.hardware.usb.UsbManager; 11 | import android.net.LocalServerSocket; 12 | import android.net.LocalSocket; 13 | import android.os.Handler; 14 | import android.os.HandlerThread; 15 | import android.os.Looper; 16 | import android.os.ParcelFileDescriptor; 17 | import android.os.Process; 18 | import android.widget.Toast; 19 | 20 | import androidx.annotation.NonNull; 21 | import androidx.annotation.Nullable; 22 | 23 | import java.io.DataInputStream; 24 | import java.io.DataOutputStream; 25 | import java.io.FileDescriptor; 26 | import java.io.IOException; 27 | import java.io.InputStream; 28 | import java.io.InterruptedIOException; 29 | import java.util.Map; 30 | 31 | /** 32 | * A libusb manager for Termux-like applications. 33 | *

34 | * Device enumeration and hot plug/unplug events are supported. 35 | *

36 | * To be used with green-green-avk/libusb branches: 37 | *

41 | */ 42 | @SuppressWarnings("WeakerAccess,unused") 43 | public class LibUsbManager { 44 | private static final String ACTION_USB_PERMISSION_SUFFIX = ".USB_PERMISSION"; 45 | 46 | private static final class ParseException extends RuntimeException { 47 | private ParseException(final String message) { 48 | super(message); 49 | } 50 | } 51 | 52 | private static final class ProcessException extends RuntimeException { 53 | private ProcessException(final String message) { 54 | super(message); 55 | } 56 | } 57 | 58 | /** 59 | * The application context. 60 | */ 61 | @NonNull 62 | protected final Context context; 63 | @NonNull 64 | private final String socketName; 65 | @NonNull 66 | private final String actionUsbPermission; 67 | @NonNull 68 | private final Thread lth; 69 | 70 | /** 71 | * Something just-to-be-reported happened. 72 | * 73 | * @param e The cause. 74 | */ 75 | protected void onClientException(@NonNull final Throwable e) { 76 | new Handler(Looper.getMainLooper()).post(() -> 77 | Toast.makeText(context, context.getString( 78 | R.string.libusbmanager_msg_error_s, 79 | e.getMessage() 80 | ), Toast.LENGTH_LONG).show()); 81 | } 82 | 83 | /** 84 | * Something sinister happened... 85 | * 86 | * @param e The cause. 87 | */ 88 | protected void onClientError(@NonNull final Throwable e) { 89 | rethrow(e); 90 | } 91 | 92 | /** 93 | * Something sinister happened... 94 | * 95 | * @param e The cause. 96 | */ 97 | protected void onServerError(@NonNull final Throwable e) { 98 | rethrow(e); 99 | } 100 | 101 | /** 102 | * Called after final cleanup. 103 | */ 104 | protected void onServerExit() { 105 | } 106 | 107 | private static void rethrow(@NonNull final Throwable e) { 108 | if (e instanceof RuntimeException) 109 | throw (RuntimeException) e; 110 | if (e instanceof Error) 111 | throw (Error) e; 112 | throw new RuntimeException(e); 113 | } 114 | 115 | @NonNull 116 | private UsbManager getUsbManager() { 117 | final UsbManager usbManager = 118 | (UsbManager) context.getSystemService(Context.USB_SERVICE); 119 | if (usbManager == null) 120 | throw new ProcessException(context.getString( 121 | R.string.libusbmanager_msg_cannot_obtain_usb_service)); 122 | return usbManager; 123 | } 124 | 125 | private static final Object obtainUsbPermissionLock = new Object(); 126 | 127 | private boolean obtainUsbPermission(@NonNull final UsbDevice dev) { 128 | synchronized (obtainUsbPermissionLock) { 129 | final boolean[] result = new boolean[2]; 130 | final UsbManager usbManager = getUsbManager(); 131 | final BroadcastReceiver receiver = new BroadcastReceiver() { 132 | @Override 133 | public void onReceive(final Context context, final Intent intent) { 134 | if (!actionUsbPermission.equals(intent.getAction())) 135 | return; 136 | synchronized (result) { 137 | result[0] = true; 138 | result[1] = intent.getBooleanExtra( 139 | UsbManager.EXTRA_PERMISSION_GRANTED, 140 | false 141 | ); 142 | result.notifyAll(); 143 | } 144 | } 145 | }; 146 | synchronized (result) { 147 | context.registerReceiver(receiver, 148 | new IntentFilter(actionUsbPermission)); 149 | usbManager.requestPermission(dev, PendingIntent.getBroadcast( 150 | context, 0, 151 | new Intent(actionUsbPermission), 152 | PendingIntent.FLAG_MUTABLE)); 153 | try { 154 | while (!result[0]) { 155 | result.wait(); 156 | } 157 | } catch (final InterruptedException ignored) { 158 | } finally { 159 | context.unregisterReceiver(receiver); 160 | } 161 | } 162 | return result[1]; 163 | } 164 | } 165 | 166 | private static final int DEV_ATTACHED = 0; 167 | private static final int DEV_DETACHED = 1; 168 | private static final int DEV_EXISTS = 2; 169 | 170 | private void tryWriteDeviceName(@NonNull final LocalSocket socket, 171 | @Nullable final UsbDevice dev, final int state) { 172 | try { 173 | final DataOutputStream out = new DataOutputStream(socket.getOutputStream()); 174 | if (state != DEV_EXISTS) 175 | out.writeByte(state); 176 | out.writeUTF(dev == null ? "" : dev.getDeviceName()); 177 | } catch (final Throwable e) { 178 | onClientException(e); 179 | } 180 | } 181 | 182 | private void devListClient(@NonNull final LocalSocket socket) throws IOException { 183 | final HandlerThread outTh = new HandlerThread("libUsbEventsOutput"); 184 | outTh.start(); 185 | final Handler outH = new Handler(outTh.getLooper()); 186 | outH.post(() -> { 187 | for (final UsbDevice dev : getUsbManager().getDeviceList().values()) 188 | tryWriteDeviceName(socket, dev, DEV_EXISTS); 189 | tryWriteDeviceName(socket, null, DEV_EXISTS); 190 | }); 191 | final BroadcastReceiver receiver = new BroadcastReceiver() { 192 | @Override 193 | public void onReceive(final Context context, @NonNull final Intent intent) { 194 | final String action = intent.getAction(); 195 | if (action == null) 196 | return; 197 | switch (action) { 198 | case UsbManager.ACTION_USB_DEVICE_ATTACHED: 199 | tryWriteDeviceName(socket, 200 | intent.getParcelableExtra(UsbManager.EXTRA_DEVICE), 201 | DEV_ATTACHED); 202 | break; 203 | case UsbManager.ACTION_USB_DEVICE_DETACHED: 204 | tryWriteDeviceName(socket, 205 | intent.getParcelableExtra(UsbManager.EXTRA_DEVICE), 206 | DEV_DETACHED); 207 | break; 208 | } 209 | } 210 | }; 211 | final IntentFilter iflt = new IntentFilter(UsbManager.ACTION_USB_DEVICE_ATTACHED); 212 | iflt.addAction(UsbManager.ACTION_USB_DEVICE_DETACHED); 213 | context.registerReceiver(receiver, iflt, null, outH); 214 | try { 215 | while (socket.getInputStream().read() != -1) ; // Wait for closing by the client... 216 | } finally { 217 | context.unregisterReceiver(receiver); 218 | outTh.quit(); 219 | } 220 | } 221 | 222 | private void client(@NonNull final LocalSocket socket) { 223 | UsbDeviceConnection devConn = null; 224 | try { 225 | // TODO: Check conditions 226 | if (Process.myUid() != socket.getPeerCredentials().getUid()) 227 | throw new ParseException(context.getString( 228 | R.string.libusbmanager_msg_spoofing_detected)); 229 | // ======= 230 | final InputStream cis = socket.getInputStream(); 231 | final DataInputStream dis = new DataInputStream(cis); 232 | final String devName = dis.readUTF(); 233 | if (devName.length() <= 0) { 234 | devListClient(socket); 235 | return; 236 | } 237 | final UsbManager usbManager = getUsbManager(); 238 | final Map devList = usbManager.getDeviceList(); 239 | final UsbDevice dev = devList.get(devName); 240 | if (dev == null) 241 | throw new ProcessException(context.getString( 242 | R.string.libusbmanager_msg_no_device_found_s, devName)); 243 | obtainUsbPermission(dev); 244 | devConn = usbManager.openDevice(dev); 245 | if (devConn == null) 246 | throw new ProcessException(context.getString( 247 | R.string.libusbmanager_msg_unable_to_open_device_s, devName)); 248 | // We need to `dup()` here: stupid `ParcelFileDescriptor.adoptFd()` semantics... 249 | // It was possible to use it though before `fdsan` went such aggressive 250 | // in Android 13. 251 | // I prefer not go native to resolve it rightly for the sake of the library size. 252 | final ParcelFileDescriptor devFd = 253 | ParcelFileDescriptor.fromFd(devConn.getFileDescriptor()); 254 | try { 255 | socket.setFileDescriptorsForSend(new FileDescriptor[]{ 256 | devFd.getFileDescriptor() 257 | }); 258 | socket.getOutputStream().write(0); 259 | } finally { 260 | devFd.close(); 261 | } 262 | while (cis.read() != -1) ; // Wait for closing by the client... 263 | } catch (final InterruptedIOException ignored) { 264 | } catch (final SecurityException | IOException | 265 | ParseException | ProcessException e) { 266 | onClientException(e); 267 | } finally { 268 | if (devConn != null) 269 | devConn.close(); 270 | } 271 | } 272 | 273 | private final Runnable server = new Runnable() { 274 | @Override 275 | public void run() { 276 | LocalServerSocket serverSocket = null; 277 | try { 278 | serverSocket = new LocalServerSocket(socketName); 279 | while (!Thread.interrupted()) { 280 | final LocalSocket socket = serverSocket.accept(); 281 | final Thread cth = new Thread() { 282 | @Override 283 | public void run() { 284 | try { 285 | client(socket); 286 | } catch (final Throwable e) { 287 | onClientError(e); 288 | } finally { 289 | try { 290 | socket.close(); 291 | } catch (final IOException ignored) { 292 | } catch (final Throwable e) { 293 | onClientError(e); 294 | } 295 | } 296 | } 297 | }; 298 | cth.setDaemon(false); 299 | cth.start(); 300 | } 301 | } catch (final InterruptedIOException ignored) { 302 | } catch (final Throwable e) { 303 | onServerError(e); 304 | } finally { 305 | if (serverSocket != null) 306 | try { 307 | serverSocket.close(); 308 | } catch (final Throwable ignored) { 309 | } 310 | onServerExit(); 311 | } 312 | } 313 | }; 314 | 315 | /** 316 | * Like {@link #LibUsbManager(Context context, String socketName)} but with 317 | * {@code socketName = context.getApplicationContext().getPackageName() + ".libusb"}. 318 | * 319 | * @param context A context which application context to use. 320 | */ 321 | public LibUsbManager(@NonNull final Context context) { 322 | this(context, context.getApplicationContext().getPackageName() + ".libusb"); 323 | } 324 | 325 | /** 326 | * Creates and starts a libusb manager instance for your application. 327 | * (Yes, this simple) 328 | *

Usage:

329 | *
{@code
330 |      * ...
331 |      * {@literal @}Keep // Protect from unwanted collection in a case minifier is used.
332 |      * {@literal @}NonNull
333 |      * private usbMan;
334 |      * ...
335 |      * usbMan = new LibUsbManager(context);
336 |      * ...
337 |      * }
338 | * 339 | * @param context A context which application context to use. 340 | * @param socketName A socket name to use (in the Linux abstract namespace, 341 | * see {@link LocalServerSocket#LocalServerSocket(String)}). 342 | */ 343 | public LibUsbManager(@NonNull final Context context, @NonNull final String socketName) { 344 | this.context = context.getApplicationContext(); 345 | this.socketName = socketName; 346 | actionUsbPermission = socketName + ACTION_USB_PERMISSION_SUFFIX; 347 | lth = new Thread(server, "LibUsbServer"); 348 | lth.setDaemon(true); 349 | lth.start(); 350 | } 351 | 352 | /** 353 | * Stops the service from accepting new connections. 354 | *

355 | * Already existing connections and provided USB device descriptors are not affected. 356 | *

357 | * The effect is irreversible. 358 | */ 359 | public void recycle() { 360 | lth.interrupt(); 361 | } 362 | 363 | @Override 364 | protected void finalize() throws Throwable { 365 | lth.interrupt(); 366 | super.finalize(); 367 | } 368 | } 369 | --------------------------------------------------------------------------------