Skip to content

Commit 3ad7566

Browse files
committed
ANDROID: added experimental usb communication support
- fix full screen display on ANDROID 35+
1 parent 6832d87 commit 3ad7566

File tree

6 files changed

+224
-10
lines changed

6 files changed

+224
-10
lines changed

ChangeLog

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
2025-02-18 (12.28)
2+
ANDROID: added experimental usb communication support
3+
14
2025-02-02 (12.28)
25
TEENSY: added experimental runtime platform for teensy mcu
36

src/platform/android/app/src/main/java/net/sourceforge/smallbasic/MainActivity.java

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ public class MainActivity extends NativeActivity {
107107
private LocationAdapter _locationAdapter = null;
108108
private TextToSpeechAdapter _tts;
109109
private Storage _storage;
110+
private UsbConnection _usbConnection;
110111

111112
static {
112113
System.loadLibrary("smallbasic");
@@ -210,6 +211,10 @@ public boolean closeLibHandlers() {
210211
if (_tts != null) {
211212
_tts.stop();
212213
}
214+
if (_usbConnection != null) {
215+
_usbConnection.close();
216+
_usbConnection = null;
217+
}
213218
return removeLocationUpdates();
214219
}
215220

@@ -646,9 +651,37 @@ public void speak(final byte[] textBytes) {
646651
}
647652
}
648653

654+
public String usbConnect(int vendorId) {
655+
String result;
656+
try {
657+
_usbConnection = new UsbConnection(getApplicationContext(), vendorId);
658+
result = "connected";
659+
} catch (IOException e) {
660+
result = e.toString();
661+
}
662+
return result;
663+
}
664+
665+
public String usbReceive() {
666+
String result;
667+
if (_usbConnection != null) {
668+
result = _usbConnection.receive();
669+
} else {
670+
result = "not connected";
671+
}
672+
return result;
673+
}
674+
675+
public void usbSend(final byte[] bytes) {
676+
if (_usbConnection != null) {
677+
_usbConnection.send(bytes);
678+
}
679+
}
680+
649681
@Override
650682
protected void onCreate(Bundle savedInstanceState) {
651683
super.onCreate(savedInstanceState);
684+
setImmersiveMode();
652685
setupStorageEnvironment();
653686
if (!libraryMode()) {
654687
processIntent();
@@ -666,6 +699,7 @@ protected void onPause() {
666699
@Override
667700
protected void onResume() {
668701
super.onResume();
702+
setImmersiveMode();
669703
onActivityPaused(false);
670704
}
671705

@@ -912,6 +946,19 @@ private String saveSchemeData(final String buffer) throws IOException {
912946
return outputFile.getAbsolutePath();
913947
}
914948

949+
//
950+
// Sets true full-screen on API 35+
951+
//
952+
private void setImmersiveMode() {
953+
getWindow().getDecorView()
954+
.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
955+
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
956+
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN |
957+
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
958+
View.SYSTEM_UI_FLAG_FULLSCREEN |
959+
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
960+
}
961+
915962
private void setupStorageEnvironment() {
916963
_storage = new Storage();
917964
setenv("EXTERNAL_DIR", _storage.getExternal());
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
package net.sourceforge.smallbasic;
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.UsbEndpoint;
11+
import android.hardware.usb.UsbInterface;
12+
import android.hardware.usb.UsbManager;
13+
import android.os.Build;
14+
import android.os.Handler;
15+
import android.os.Looper;
16+
import android.util.Log;
17+
import android.widget.Toast;
18+
19+
import java.io.IOException;
20+
21+
/**
22+
* Usb host mode communications
23+
*/
24+
public class UsbConnection extends BroadcastReceiver {
25+
private static final String TAG = "smallbasic";
26+
private static final String ACTION_USB_PERMISSION = "smallbasic.android.USB_PERMISSION";
27+
private static final String PERMISSION_ERROR = "Not permitted";
28+
private static final String USB_DEVICE_NOT_FOUND = "Usb device not found";
29+
private static final int TIMEOUT_MILLIS = 1000;
30+
private static final int BUFFER_SIZE = 64;
31+
32+
private final UsbDeviceConnection _connection;
33+
private final UsbInterface _usbInterface;
34+
private final UsbDevice _usbDevice;
35+
private final UsbManager _usbManager;
36+
private final UsbEndpoint _endpointIn;
37+
private final UsbEndpoint _endpointOut;
38+
39+
/**
40+
* Constructs a new UsbConnection
41+
*/
42+
public UsbConnection(Context context, int vendorId) throws IOException {
43+
_usbManager = getUsbManager(context);
44+
_usbDevice = getDevice(_usbManager, vendorId);
45+
if (_usbDevice == null) {
46+
throw new IOException(USB_DEVICE_NOT_FOUND);
47+
}
48+
49+
if (!_usbManager.hasPermission(_usbDevice)) {
50+
requestPermission(context);
51+
throw new IOException(PERMISSION_ERROR);
52+
}
53+
54+
_usbInterface = _usbDevice.getInterface(0);
55+
_endpointIn = _usbInterface.getEndpoint(0);
56+
_endpointOut = _usbInterface.getEndpoint(1);
57+
_connection = _usbManager.openDevice(_usbDevice);
58+
_connection.claimInterface(_usbInterface, true);
59+
}
60+
61+
/**
62+
* Closes the USB connection
63+
*/
64+
public void close() {
65+
if (_connection != null && _usbInterface != null) {
66+
_connection.releaseInterface(_usbInterface);
67+
_connection.close();
68+
}
69+
}
70+
71+
/**
72+
* Handles receiving the request permission response event
73+
*/
74+
@Override
75+
public void onReceive(Context context, Intent intent) {
76+
Log.d(TAG, "onReceive entered");
77+
if (ACTION_USB_PERMISSION.equals(intent.getAction())) {
78+
boolean permitted = _usbManager.hasPermission(_usbDevice);
79+
String name = _usbDevice.getProductName();
80+
final String message = "USB connection [" + name + "] access " + (permitted ? "permitted" : "denied");
81+
final BroadcastReceiver receiver = this;
82+
new Handler(Looper.getMainLooper()).post(() -> {
83+
Toast.makeText(context, message, Toast.LENGTH_LONG).show();
84+
context.unregisterReceiver(receiver);
85+
});
86+
}
87+
}
88+
89+
/**
90+
* Receives the next packet of data from the usb connection
91+
*/
92+
public String receive() {
93+
String result;
94+
byte[] dataIn = new byte[BUFFER_SIZE];
95+
int bytesRead = _connection.bulkTransfer(_endpointIn, dataIn, dataIn.length, TIMEOUT_MILLIS);
96+
if (bytesRead > 0) {
97+
result = new String(dataIn, 0, bytesRead);
98+
} else {
99+
result = "";
100+
}
101+
return result;
102+
}
103+
104+
/**
105+
* Sends the given data to the usb connection
106+
*/
107+
public void send(byte[] dataOut) {
108+
_connection.bulkTransfer(_endpointOut, dataOut, dataOut.length, TIMEOUT_MILLIS);
109+
}
110+
111+
/**
112+
* Returns the UsbDevice matching the given vendorId
113+
*/
114+
private UsbDevice getDevice(UsbManager usbManager, int vendorId) {
115+
UsbDevice result = null;
116+
for (UsbDevice device : usbManager.getDeviceList().values()) {
117+
if (device.getVendorId() == vendorId) {
118+
result = device;
119+
break;
120+
}
121+
}
122+
return result;
123+
}
124+
125+
/**
126+
* Returns the UsbManager
127+
*/
128+
private static UsbManager getUsbManager(Context context) {
129+
return (UsbManager) context.getSystemService(Context.USB_SERVICE);
130+
}
131+
132+
/**
133+
* Invokes the display of a permission prompt
134+
*/
135+
private void requestPermission(Context context) {
136+
new Handler(Looper.getMainLooper()).post(() -> {
137+
IntentFilter filter = new IntentFilter(ACTION_USB_PERMISSION);
138+
filter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY - 1);
139+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
140+
context.registerReceiver(this, filter, Context.RECEIVER_NOT_EXPORTED);
141+
}
142+
int flags = PendingIntent.FLAG_IMMUTABLE;
143+
Intent intent = new Intent(ACTION_USB_PERMISSION);
144+
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, intent, flags);
145+
_usbManager.requestPermission(_usbDevice, pendingIntent);
146+
});
147+
Log.d(TAG, "requesting permission");
148+
}
149+
}

src/platform/android/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ buildscript {
55
mavenCentral()
66
}
77
dependencies {
8-
classpath 'com.android.tools.build:gradle:8.8.0'
8+
classpath 'com.android.tools.build:gradle:8.8.1'
99
}
1010
}
1111

src/platform/android/jni/Android.mk

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,24 @@
55
# Download the GNU Public License (GPL) from www.gnu.org
66
#
77

8-
JNI_PATH := $(call my-dir)
9-
SB_HOME := $(JNI_PATH)/../../../..
10-
SDK := $(HOME)/android-sdk
11-
FREETYPE_HOME := $(SDK)/freetype-2.13.3
12-
138
#
9+
# 1. Building oboe for audio support
1410
# modify build_all_android.sh > DBUILD_SHARED_LIBS=false
1511
# export ANDROID_NDK=~/android-sdk/sdk/ndk/27.0.12077973
1612
# ./build_all_android.sh
1713
#
18-
OBOE_LIB_DIR := $(SDK)/oboe/build/$(TARGET_ARCH_ABI)/staging/lib/$(TARGET_ARCH_ABI)
19-
OBOE_INCLUDE_PATH := $(SDK)/oboe/include
14+
# 2. Building freetype - see README.md
15+
#
16+
17+
FREETYPE_VER=freetype-2.13.3
18+
OBOE_VER=oboe-1.9.3
19+
20+
JNI_PATH := $(call my-dir)
21+
SB_HOME := $(JNI_PATH)/../../../..
22+
SDK := $(HOME)/android-sdk
23+
FREETYPE_HOME := $(SDK)/$(FREETYPE_VER)
24+
OBOE_LIB_DIR := $(SDK)/oboe/${OBOE_VER}/build/$(TARGET_ARCH_ABI)/staging/lib/$(TARGET_ARCH_ABI)
25+
OBOE_INCLUDE_PATH := $(SDK)/oboe/${OBOE_VER}/include
2026

2127
include $(call all-subdir-makefiles)
2228
LOCAL_PATH := $(JNI_PATH)

src/ui/screen.cpp

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,15 @@
1515
#define MAX_HEIGHT 10000
1616
#define TEXT_ROWS 1000
1717

18+
// for hit testing menu button press
19+
#if defined(_ANDROID)
20+
#define MENU_WIDTH_SCALE 4
21+
#define MENU_HEIGHT_SCALE 3
22+
#else
23+
#define MENU_WIDTH_SCALE 3
24+
#define MENU_HEIGHT_SCALE 2
25+
#endif
26+
1827
#define DRAW_SHAPE \
1928
Shape *rect = (*it); \
2029
if (rect->_y >= _scrollY && \
@@ -307,8 +316,8 @@ bool Screen::overLabel(int px, int py) {
307316

308317
// whether the point overlaps the menu widget
309318
bool Screen::overMenu(int px, int py) {
310-
int w = _charWidth * 3;
311-
int h = _charHeight * 2;
319+
int w = _charWidth * MENU_HEIGHT_SCALE;
320+
int h = _charHeight * MENU_WIDTH_SCALE;
312321
return (!OUTSIDE_RECT(px, py, _width - w, _height - h, w, h));
313322
}
314323

0 commit comments

Comments
 (0)