From 39d39de914a70b1662d416cb2544fcf685330f06 Mon Sep 17 00:00:00 2001 From: Mark Qvist Date: Sat, 17 Sep 2022 16:00:55 +0200 Subject: [PATCH] Added USB support and foreground service basics --- sbapp/Makefile | 22 +- sbapp/buildozer.spec | 7 +- sbapp/main.py | 9 +- sbapp/patches/AndroidManifest.tmpl.xml | 147 ++++++++++++ sbapp/patches/HIDDeviceUSB.java | 314 +++++++++++++++++++++++++ sbapp/patches/device_filter.xml | 39 +++ sbapp/patches/intent-filter.xml | 6 + sbapp/services/sidebandservice.py | 14 ++ 8 files changed, 553 insertions(+), 5 deletions(-) create mode 100644 sbapp/patches/AndroidManifest.tmpl.xml create mode 100644 sbapp/patches/HIDDeviceUSB.java create mode 100644 sbapp/patches/device_filter.xml create mode 100644 sbapp/patches/intent-filter.xml create mode 100644 sbapp/services/sidebandservice.py diff --git a/sbapp/Makefile b/sbapp/Makefile index 213440b..ff4b019 100644 --- a/sbapp/Makefile +++ b/sbapp/Makefile @@ -15,17 +15,35 @@ cleanall: clean cleanlibs activate: (. venv/bin/activate) +pacthfiles: patchsdl injectxml + +patchsdl: + cp patches/HIDDeviceUSB.java .buildozer/android/platform/build-arm64-v8a/build/bootstrap_builds/sdl2/jni/SDL/android-project/app/src/main/java/org/libsdl/app/HIDDeviceUSB.java + cp patches/HIDDeviceUSB.java .buildozer/android/platform/build-arm64-v8a/dists/sideband/src/main/java/org/libsdl/app/HIDDeviceUSB.java + cp patches/HIDDeviceUSB.java .buildozer/android/platform/build-arm64-v8a/dists/sideband/jni/SDL/android-project/app/src/main/java/org/libsdl/app/HIDDeviceUSB.java + +injectxml: + mkdir -p .buildozer/android/platform/build-arm64-v8a/dists/sideband/src/main/res/xml + mkdir -p .buildozer/android/platform/build-arm64-v8a/dists/sideband/templates + cp patches/device_filter.xml .buildozer/android/platform/build-arm64-v8a/dists/sideband/src/main/res/xml/ + cp patches/AndroidManifest.tmpl.xml .buildozer/android/platform/build-arm64-v8a/dists/sideband/templates/ + debug: buildozer android debug +prebake: + -(buildozer android release) + @(echo Prebake finished, applying patches and rebuilding...) + -(sleep 2) + release: buildozer android release postbuild: cleanrns -apk: prepare release postbuild +apk: prepare prebake pacthfiles release postbuild -devapk: prepare debug postbuild +devapk: prepare prebake pacthfiles debug postbuild version: @(echo $$(python ./gv.py)) diff --git a/sbapp/buildozer.spec b/sbapp/buildozer.spec index b49e67c..6f53110 100644 --- a/sbapp/buildozer.spec +++ b/sbapp/buildozer.spec @@ -13,7 +13,7 @@ version.filename = %(source.dir)s/main.py android.numeric_version = 1 -requirements = python3==3.9.5,hostpython3==3.9.5,cryptography,cffi,pycparser,kivy==2.1.0,pygments,sdl2_ttf==2.0.15,pillow,lxmf==0.1.7,netifaces,libbz2,pydenticon +requirements = python3==3.9.5,hostpython3==3.9.5,cryptography,cffi,pycparser,kivy==2.1.0,pygments,sdl2,sdl2_ttf==2.0.15,pillow,lxmf==0.1.7,netifaces,libbz2,pydenticon p4a.local_recipes = ../Others/python-for-android/pythonforandroid/recipes requirements.source.kivymd = ../../Others/KivyMD-master # requirements.source.plyer = ../../Others/plyer @@ -25,7 +25,7 @@ android.presplash_color = #00000000 orientation = all fullscreen = 0 -android.permissions = INTERNET,POST_NOTIFICATIONS +android.permissions = INTERNET,POST_NOTIFICATIONS,WAKE_LOCK,FOREGROUND_SERVICE android.api = 30 android.minapi = 27 android.ndk = 19b @@ -34,6 +34,9 @@ android.accept_sdk_license = True android.arch = arm64-v8a #android.logcat_filters = *:S python:D +# services = sidebandservice:services/sidebandservice.py:foreground +android.manifest.intent_filters = patches/intent-filter.xml + [buildozer] log_level = 2 warn_on_root = 0 diff --git a/sbapp/main.py b/sbapp/main.py index 942e0a1..99f2e5b 100644 --- a/sbapp/main.py +++ b/sbapp/main.py @@ -83,6 +83,13 @@ class SidebandApp(MDApp): Clock.schedule_once(dismiss_splash, 0) + def start_android_service(self): + service = autoclass('io.unsigned.sideband.ServiceSidebandservice') + mActivity = autoclass('org.kivy.android.PythonActivity').mActivity + argument = "" + service.start(mActivity, argument) + + ################################################# # General helpers # ################################################# @@ -859,7 +866,7 @@ If you are not connected to the network, it is still possible for other people t The Propagation Nodes also distribute copies of messages between each other, such that even the failure of almost every node in the network will still allow users to sync their waiting messages. If all Propagation Nodes disappear or are destroyed, users can still communicate directly. Reticulum and LXMF will degrade gracefully all the way down to single users communicating directly via long-range data radios. Anyone can start up new propagation nodes and integrate them into existing networks without permission or coordination. Even a small and cheap device like a Rasperry Pi can handle messages for millions of users. LXMF networks are designed to be quite resilient, as long as there are people using them. -Connections in Reticulum networks can be wired or wireless, span many intermediary hops, run over fast links or ultra-low bandwidth radio, tunnel over the Invisible Internet (I2P), private networks, satellite connections, serial lines or anything else that Reticulum can carry data over. In most cases it will not be possible to know what path data takes in a Reticulum network, and no transmitted data carries any identifying characteristics, apart from a destination address. There is no source addresses in Reticulum. As long as you do not establish a link between you personal identity and your LXMF address through some other method, you can remain anonymous. Sending messages to others does not reveal [i]your[/i] address. +Connections in Reticulum networks can be wired or wireless, span many intermediary hops, run over fast links or ultra-low bandwidth radio, tunnel over the Invisible Internet (I2P), private networks, satellite connections, serial lines or anything else that Reticulum can carry data over. In most cases it will not be possible to know what path data takes in a Reticulum network, and no transmitted data carries any identifying characteristics, apart from a destination address. There is no source addresses in Reticulum. As long as you do not reveal any connecting details between your person and your LXMF address, you can remain anonymous. Sending messages to others does not reveal [i]your[/i] address to anyone else than the intended recipient. That being said, you [b]must remember[/b] that LXMF and Reticulum is not a technology that can guarantee anonymising connections that are already de-anonymised! If you use Sideband to connect to TCP Reticulum hubs over the clear Internet, from a network that can be tied to your personal identity, an adversary may learn that you are generating LXMF traffic. If you want to avoid this, it is recommended to use I2P to connect to Reticulum hubs on the Internet. Or only connecting from within pure Reticulum networks, that take one or more hops to reach connections that span the Internet. This is a complex topic, with many more nuances than can not be covered here. You are encouraged to ask on the various Reticulum discussion forums if you are in doubt. diff --git a/sbapp/patches/AndroidManifest.tmpl.xml b/sbapp/patches/AndroidManifest.tmpl.xml new file mode 100644 index 0000000..e17a732 --- /dev/null +++ b/sbapp/patches/AndroidManifest.tmpl.xml @@ -0,0 +1,147 @@ + + + + + = 9 %} + android:xlargeScreens="true" + {% endif %} + /> + + + + + + + + + + + {% for perm in args.permissions %} + {% if '.' in perm %} + + {% else %} + + {% endif %} + {% endfor %} + + {% if args.wakelock %} + + {% endif %} + + {% if args.billing_pubkey %} + + {% endif %} + + {{ args.extra_manifest_xml }} + + + + + {% for l in args.android_used_libs %} + + {% endfor %} + + {% for m in args.meta_data %} + {% endfor %} + + + + + {% if args.launcher %} + + + + + + {% else %} + + + + + {% endif %} + + {%- if args.intent_filters -%} + {{- args.intent_filters -}} + {%- endif -%} + + + {% if args.launcher %} + + + + + + + + + {% endif %} + + {% if service or args.launcher %} + + {% endif %} + {% for name in service_names %} + + {% endfor %} + {% for name in native_services %} + + {% endfor %} + + {% if args.billing_pubkey %} + + + + + + + + + {% endif %} + {% for a in args.add_activity %} + + {% endfor %} + + + diff --git a/sbapp/patches/HIDDeviceUSB.java b/sbapp/patches/HIDDeviceUSB.java new file mode 100644 index 0000000..00c10e9 --- /dev/null +++ b/sbapp/patches/HIDDeviceUSB.java @@ -0,0 +1,314 @@ +// Patched HID device handling in relation to: +// https://github.com/libsdl-org/SDL/issues/3850 + +package org.libsdl.app; + +import android.hardware.usb.*; +import android.os.Build; +import android.util.Log; +import java.util.Arrays; + +class HIDDeviceUSB implements HIDDevice { + + private static final String TAG = "hidapi"; + + protected HIDDeviceManager mManager; + protected UsbDevice mDevice; + protected int mInterface; + protected int mDeviceId; + protected UsbDeviceConnection mConnection; + protected UsbEndpoint mInputEndpoint; + protected UsbEndpoint mOutputEndpoint; + protected InputThread mInputThread; + protected boolean mRunning; + protected boolean mFrozen; + + public HIDDeviceUSB(HIDDeviceManager manager, UsbDevice usbDevice, int interface_number) { + mManager = manager; + mDevice = usbDevice; + mInterface = interface_number; + mDeviceId = manager.getDeviceIDForIdentifier(getIdentifier()); + mRunning = false; + } + + public String getIdentifier() { + return String.format("%s/%x/%x", mDevice.getDeviceName(), mDevice.getVendorId(), mDevice.getProductId()); + } + + @Override + public int getId() { + return mDeviceId; + } + + @Override + public int getVendorId() { + return mDevice.getVendorId(); + } + + @Override + public int getProductId() { + return mDevice.getProductId(); + } + + @Override + public String getSerialNumber() { + String result = null; + if (Build.VERSION.SDK_INT >= 21) { + try { + result = mDevice.getSerialNumber(); + } catch (SecurityException e) { + + } + } + if (result == null) { + result = ""; + } + return result; + } + + @Override + public int getVersion() { + return 0; + } + + @Override + public String getManufacturerName() { + String result = null; + if (Build.VERSION.SDK_INT >= 21) { + result = mDevice.getManufacturerName(); + } + if (result == null) { + result = String.format("%x", getVendorId()); + } + return result; + } + + @Override + public String getProductName() { + String result = null; + if (Build.VERSION.SDK_INT >= 21) { + result = mDevice.getProductName(); + } + if (result == null) { + result = String.format("%x", getProductId()); + } + return result; + } + + public UsbDevice getDevice() { + return mDevice; + } + + public String getDeviceName() { + return getManufacturerName() + " " + getProductName() + "(0x" + String.format("%x", getVendorId()) + "/0x" + String.format("%x", getProductId()) + ")"; + } + + @Override + public boolean open() { + mConnection = mManager.getUSBManager().openDevice(mDevice); + if (mConnection == null) { + Log.w(TAG, "Unable to open USB device " + getDeviceName()); + return false; + } + + // Force claim all interfaces + for (int i = 0; i < mDevice.getInterfaceCount(); i++) { + UsbInterface iface = mDevice.getInterface(i); + + if (!mConnection.claimInterface(iface, true)) { + Log.w(TAG, "Failed to claim interfaces on USB device " + getDeviceName()); + close(); + return false; + } + } + + // Find the endpoints + UsbInterface iface = mDevice.getInterface(mInterface); + for (int j = 0; j < iface.getEndpointCount(); j++) { + UsbEndpoint endpt = iface.getEndpoint(j); + switch (endpt.getDirection()) { + case UsbConstants.USB_DIR_IN: + if (mInputEndpoint == null) { + mInputEndpoint = endpt; + } + break; + case UsbConstants.USB_DIR_OUT: + if (mOutputEndpoint == null) { + mOutputEndpoint = endpt; + } + break; + } + } + + // Make sure the required endpoints were present + if (mInputEndpoint == null || mOutputEndpoint == null) { + Log.w(TAG, "Missing required endpoint on USB device " + getDeviceName()); + close(); + return false; + } + + // Start listening for input + mRunning = true; + mInputThread = new InputThread(); + mInputThread.start(); + + return true; + } + + @Override + public int sendFeatureReport(byte[] report) { + int res = -1; + int offset = 0; + int length = report.length; + boolean skipped_report_id = false; + byte report_number = report[0]; + + if (report_number == 0x0) { + ++offset; + --length; + skipped_report_id = true; + } + + res = mConnection.controlTransfer( + UsbConstants.USB_TYPE_CLASS | 0x01 /*RECIPIENT_INTERFACE*/ | UsbConstants.USB_DIR_OUT, + 0x09/*HID set_report*/, + (3/*HID feature*/ << 8) | report_number, + 0, + report, offset, length, + 1000/*timeout millis*/); + + if (res < 0) { + Log.w(TAG, "sendFeatureReport() returned " + res + " on device " + getDeviceName()); + return -1; + } + + if (skipped_report_id) { + ++length; + } + return length; + } + + @Override + public int sendOutputReport(byte[] report) { + int r = mConnection.bulkTransfer(mOutputEndpoint, report, report.length, 1000); + if (r != report.length) { + Log.w(TAG, "sendOutputReport() returned " + r + " on device " + getDeviceName()); + } + return r; + } + + @Override + public boolean getFeatureReport(byte[] report) { + int res = -1; + int offset = 0; + int length = report.length; + boolean skipped_report_id = false; + byte report_number = report[0]; + + if (report_number == 0x0) { + /* Offset the return buffer by 1, so that the report ID + will remain in byte 0. */ + ++offset; + --length; + skipped_report_id = true; + } + + res = mConnection.controlTransfer( + UsbConstants.USB_TYPE_CLASS | 0x01 /*RECIPIENT_INTERFACE*/ | UsbConstants.USB_DIR_IN, + 0x01/*HID get_report*/, + (3/*HID feature*/ << 8) | report_number, + 0, + report, offset, length, + 1000/*timeout millis*/); + + if (res < 0) { + Log.w(TAG, "getFeatureReport() returned " + res + " on device " + getDeviceName()); + return false; + } + + if (skipped_report_id) { + ++res; + ++length; + } + + byte[] data; + if (res == length) { + data = report; + } else { + data = Arrays.copyOfRange(report, 0, res); + } + mManager.HIDDeviceFeatureReport(mDeviceId, data); + + return true; + } + + @Override + public void close() { + mRunning = false; + if (mInputThread != null) { + while (mInputThread.isAlive()) { + mInputThread.interrupt(); + try { + mInputThread.join(); + } catch (InterruptedException e) { + // Keep trying until we're done + } + } + mInputThread = null; + } + if (mConnection != null) { + for (int i = 0; i < mDevice.getInterfaceCount(); i++) { + UsbInterface iface = mDevice.getInterface(i); + mConnection.releaseInterface(iface); + } + mConnection.close(); + mConnection = null; + } + } + + @Override + public void shutdown() { + close(); + mManager = null; + } + + @Override + public void setFrozen(boolean frozen) { + mFrozen = frozen; + } + + protected class InputThread extends Thread { + @Override + public void run() { + int packetSize = mInputEndpoint.getMaxPacketSize(); + byte[] packet = new byte[packetSize]; + while (mRunning) { + int r; + try + { + r = mConnection.bulkTransfer(mInputEndpoint, packet, packetSize, 1000); + } + catch (Exception e) + { + Log.v(TAG, "Exception in UsbDeviceConnection bulktransfer: " + e); + break; + } + if (r < 0) { + // Could be a timeout or an I/O error + } + if (r > 0) { + byte[] data; + if (r == packetSize) { + data = packet; + } else { + data = Arrays.copyOfRange(packet, 0, r); + } + + if (!mFrozen) { + mManager.HIDDeviceInputReport(mDeviceId, data); + } + } + } + } + } +} diff --git a/sbapp/patches/device_filter.xml b/sbapp/patches/device_filter.xml new file mode 100644 index 0000000..b857817 --- /dev/null +++ b/sbapp/patches/device_filter.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/sbapp/patches/intent-filter.xml b/sbapp/patches/intent-filter.xml new file mode 100644 index 0000000..4b3f9a2 --- /dev/null +++ b/sbapp/patches/intent-filter.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/sbapp/services/sidebandservice.py b/sbapp/services/sidebandservice.py new file mode 100644 index 0000000..5073f0d --- /dev/null +++ b/sbapp/services/sidebandservice.py @@ -0,0 +1,14 @@ +import time + +class sidebandservice(): + + def __init__(self): + print("Sideband Service created") + self.run() + + def run(self): + while True: + print("Service ping") + time.sleep(3) + +sbs = sidebandservice() \ No newline at end of file