├── .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 | [](https://jitpack.io/#green-green-avk/LibUsbManager)
4 | [](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 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
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 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
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 |
--------------------------------------------------------------------------------