Skip to content

Commit 5dd8e1c

Browse files
committed
IOIO: make permission handling at least workable.
1 parent 3dab3ea commit 5dd8e1c

File tree

7 files changed

+157
-59
lines changed

7 files changed

+157
-59
lines changed

ioio/ioio/src/main/AndroidManifest.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
<uses-feature android:name="android.hardware.usb.accessory" android:required="true" />
44
<uses-permission android:name="android.permission.USB_PERMISSION" />
55
<application>
6-
<receiver android:name="ioio.smallbasic.android.AccessoryPermissionCheck" android:exported="true">
6+
<receiver android:name="ioio.smallbasic.android.AccessoryPermissionCheck" android:exported="false">
77
<intent-filter>
8-
<action android:name="android.hardware.usb.action.USB_PERMISSION" />
8+
<action android:name="ioio.smallbasic.android.USB_PERMISSION" />
99
</intent-filter>
1010
</receiver>
1111
<meta-data android:name="android.hardware.usb.action.USB_ACCESSORY_ATTACHED" android:resource="@xml/device_filter" />

ioio/ioio/src/main/java/ioio/smallbasic/IOLock.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
public class IOLock<I> {
1313
private final Object mutex = new Object();
14-
private static final int TIMEOUT_SECS = 5;
14+
private static final int TIMEOUT_SECS = 10;
1515
private Consumer<I> consumer;
1616

1717
public void invoke(Consumer<I> consumer) {
@@ -90,6 +90,7 @@ private CountDownLatch beginLatch() {
9090
private void endLatch(CountDownLatch latch) {
9191
try {
9292
if (!latch.await(TIMEOUT_SECS, TimeUnit.SECONDS)) {
93+
IOService.setHardReset(true);
9394
throw new IOIOException("Timeout waiting for device");
9495
}
9596
} catch (InterruptedException e) {

ioio/ioio/src/main/java/ioio/smallbasic/IOService.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import java.io.IOException;
44
import java.util.ArrayList;
55
import java.util.List;
6+
import java.util.concurrent.atomic.AtomicBoolean;
67

78
import ioio.lib.api.IOIO;
89
import ioio.lib.api.exception.ConnectionLostException;
@@ -13,6 +14,7 @@
1314
public class IOService implements IOIOLooperProvider {
1415
private static final String TAG = "IOService";
1516
private static final int MAX_PINS = 46;
17+
private static final AtomicBoolean HARD_RESET = new AtomicBoolean(false);
1618
private static IOService instance = null;
1719

1820
private final ConnectionController connectionController;
@@ -27,13 +29,21 @@ private IOService() {
2729
usedPins = new Boolean[MAX_PINS + 1];
2830
}
2931

32+
public static boolean getHardReset() {
33+
return HARD_RESET.get();
34+
}
35+
3036
public static IOService getInstance() {
3137
if (instance == null) {
3238
instance = new IOService();
3339
}
3440
return instance;
3541
}
3642

43+
public static void setHardReset(boolean hardReset) {
44+
IOService.HARD_RESET.set(hardReset);
45+
}
46+
3747
public void addTask(IOTask ioTask) throws IOException {
3848
registerPin(ioTask.getPin());
3949
ioTasks.add(ioTask);

ioio/ioio/src/main/java/ioio/smallbasic/android/AccessoryPermissionCheck.java

Lines changed: 14 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -12,27 +12,17 @@
1212
import android.os.Handler;
1313
import android.os.Looper;
1414

15-
import java.util.concurrent.CountDownLatch;
16-
import java.util.concurrent.TimeUnit;
17-
import java.util.concurrent.atomic.AtomicBoolean;
18-
1915
import ioio.lib.spi.Log;
2016
import ioio.smallbasic.IOIOException;
2117

2218
public class AccessoryPermissionCheck extends BroadcastReceiver {
2319
private static final String TAG = AccessoryPermissionCheck.class.getSimpleName();
24-
private static final String ACTION_USB_PERMISSION = "android.hardware.usb.action.USB_PERMISSION";
25-
private static final String PERMISSION_ERROR = "Not permitted to use usb accessory.";
26-
private static final long TIMEOUT_SECS = 30;
27-
private final CountDownLatch latch;
28-
private final AtomicBoolean permitted;
20+
private static final String ACTION_USB_PERMISSION = "ioio.smallbasic.android.USB_PERMISSION";
21+
private static final String PERMISSION_ERROR = "Not permitted";
2922

3023
@TargetApi(Build.VERSION_CODES.TIRAMISU)
3124
public AccessoryPermissionCheck() {
3225
Log.d(TAG, "AccessoryPermissionCheck entered");
33-
this.permitted = new AtomicBoolean(false);
34-
this.latch = new CountDownLatch(1);
35-
3626
UsbManager usbManager = (UsbManager) IOIOLoader.getContext().getSystemService(Context.USB_SERVICE);
3727
UsbAccessory[] accessories = usbManager.getAccessoryList();
3828
UsbAccessory accessory = (accessories == null ? null : accessories[0]);
@@ -43,43 +33,27 @@ public AccessoryPermissionCheck() {
4333
new Handler(Looper.getMainLooper()).post(() -> {
4434
Context context = IOIOLoader.getContext();
4535
IntentFilter filter = new IntentFilter(ACTION_USB_PERMISSION);
46-
context.registerReceiver(this, filter, Context.RECEIVER_EXPORTED);
36+
filter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY - 1);
37+
context.registerReceiver(this, filter, Context.RECEIVER_NOT_EXPORTED);
4738
int flags = PendingIntent.FLAG_IMMUTABLE;
4839
Intent intent = new Intent(ACTION_USB_PERMISSION);
4940
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, intent, flags);
5041
usbManager.requestPermission(accessory, pendingIntent);
5142
});
52-
Log.d(TAG, "waiting for permission");
53-
try {
54-
if (!latch.await(TIMEOUT_SECS, TimeUnit.SECONDS)) {
55-
permissionError();
56-
} else {
57-
IOIOLoader.getContext().unregisterReceiver(this);
58-
}
59-
}
60-
catch (InterruptedException e) {
61-
permissionError();
62-
throw new IOIOException(PERMISSION_ERROR);
63-
}
64-
if (!permitted.get()) {
65-
permissionError();
66-
}
43+
// for some reason using a latch caused an ANR here
44+
Log.d(TAG, "requesting permission");
45+
throw new IOIOException(PERMISSION_ERROR);
6746
}
6847
}
6948

7049
@Override
71-
public void onReceive(Context context, Intent intent) {
72-
Log.d(TAG, intent.getAction());
73-
synchronized (this) {
74-
if (ACTION_USB_PERMISSION.equals(intent.getAction())) {
75-
permitted.set(intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false));
76-
latch.countDown();
77-
}
50+
public synchronized void onReceive(final Context context, Intent intent) {
51+
Log.d(TAG, "onReceive entered");
52+
if (ACTION_USB_PERMISSION.equals(intent.getAction())) {
53+
final BroadcastReceiver receiver = this;
54+
new Handler(Looper.getMainLooper()).post(() -> {
55+
context.unregisterReceiver(receiver);
56+
});
7857
}
7958
}
80-
81-
private void permissionError() {
82-
IOIOLoader.getContext().unregisterReceiver(this);
83-
throw new IOIOException(PERMISSION_ERROR);
84-
}
8559
}
Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,19 @@
11
package ioio.smallbasic.android;
22

3+
import android.annotation.TargetApi;
4+
import android.os.Build;
35
import android.util.Log;
46

57
public class AndroidLogger implements ioio.lib.spi.Log.ILogger {
68
public AndroidLogger() {
79
super();
810
}
911

12+
@TargetApi(Build.VERSION_CODES.O)
1013
@Override
1114
public void write(int level, String tag, String message) {
12-
Log.println(level, tag, message);
15+
long id = Thread.currentThread().getId();
16+
String text = tag + ": [#" + id + "] " + message;
17+
Log.println(Log.ERROR, "smallbasic", text);
1318
}
1419
}

ioio/ioio/src/main/java/ioio/smallbasic/android/BluetoothConnection.java

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -41,14 +41,18 @@
4141

4242
import ioio.lib.api.IOIOConnection;
4343
import ioio.lib.api.exception.ConnectionLostException;
44-
import ioio.lib.impl.FixedReadBufferedInputStream;
4544
import ioio.lib.spi.Log;
4645
import ioio.smallbasic.IOIOException;
46+
import ioio.smallbasic.IOService;
4747

4848
class BluetoothConnection implements IOIOConnection {
4949
private static final String TAG = BluetoothConnection.class.getSimpleName();
50+
private static final int HARD_RESET = 0x00;
51+
private static final int SOFT_RESET = 0x01;
52+
private static final int ESTABLISH_CONNECTION = 0x00;
53+
5054
private ConnectionState state;
51-
private InputStream inputStream;
55+
private FixedReadBufferedInputStream inputStream;
5256
private OutputStream outputStream;
5357
private ParcelFileDescriptor fileDescriptor;
5458

@@ -126,22 +130,27 @@ private boolean open() {
126130
inputStream = new FixedReadBufferedInputStream(new FileInputStream(fd), 1024);
127131
outputStream = new BufferedOutputStream(new FileOutputStream(fd), 1024);
128132

129-
// Soft-open the connection
130-
outputStream.write(0x00);
133+
// open the connection
134+
if (IOService.getHardReset()) {
135+
outputStream.write(HARD_RESET);
136+
outputStream.write('I');
137+
outputStream.write('O');
138+
outputStream.write('I');
139+
outputStream.write('O');
140+
IOService.setHardReset(false);
141+
} else {
142+
outputStream.write(SOFT_RESET);
143+
}
131144
outputStream.flush();
132145

133-
Log.i(TAG, "created streams");
134-
135-
// Ytai mentions http://code.google.com/p/android/issues/detail?id=20545
136-
// which is now closed, but waiting still seems to be required.
137-
// "We're going to block now. We're counting on the IOIO to
138-
// write back a byte, or otherwise we're locked until
139-
// physical disconnection"
140-
while (inputStream.read() != 1) {
141-
wait(1000);
146+
// ensure IOIOProtocol.run() first see's ESTABLISH_CONNECTION(0x00) and not SOFT_RESET(0x01)
147+
// to avoid an NPE in IncomingState.handleSoftReset(). This method relies on initialisation in
148+
// IncomingState.handleEstablishConnection
149+
if (inputStream.positionTo(ESTABLISH_CONNECTION)) {
150+
Log.i(TAG, "created streams");
151+
} else {
152+
Log.i(TAG, "created streams - failed to position data");
142153
}
143-
144-
Log.i(TAG, "success");
145154
result = true;
146155
} catch (Exception e) {
147156
Log.v(TAG, "Failed to open streams", e);
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
package ioio.smallbasic.android;
2+
3+
import java.io.BufferedInputStream;
4+
import java.io.IOException;
5+
import java.io.InputStream;
6+
7+
/**
8+
* Similar to a {@link BufferedInputStream}, but guarantees that all reads from the source stream
9+
* are exactly the specified size of the buffer.
10+
* It turns out, {@link BufferedInputStream} does not actually have such a guarantee.
11+
*/
12+
public class FixedReadBufferedInputStream extends InputStream {
13+
private int bufferIndex_ = 0;
14+
private int validData_ = 0;
15+
private final byte[] buffer_;
16+
private final InputStream source_;
17+
18+
public FixedReadBufferedInputStream(InputStream source, int size) {
19+
buffer_ = new byte[size];
20+
source_ = source;
21+
}
22+
23+
@Override
24+
public int available() throws IOException {
25+
return validData_ - bufferIndex_;
26+
}
27+
28+
@Override
29+
public void close() throws IOException {
30+
source_.close();
31+
}
32+
33+
@Override
34+
public int read(byte[] buffer, int offset, int length) throws IOException {
35+
fillIfEmpty();
36+
if (validData_ == -1) {
37+
return -1;
38+
}
39+
length = Math.min(length, validData_ - bufferIndex_);
40+
System.arraycopy(buffer_, bufferIndex_, buffer, offset, length);
41+
bufferIndex_ += length;
42+
return length;
43+
}
44+
45+
@Override
46+
public int read(byte[] buffer) throws IOException {
47+
return read(buffer, 0, buffer.length);
48+
}
49+
50+
@Override
51+
public int read() throws IOException {
52+
fillIfEmpty();
53+
if (validData_ == -1) {
54+
return -1;
55+
}
56+
return buffer_[bufferIndex_++] & 0xFF;
57+
}
58+
59+
@Override
60+
public long skip(long byteCount) throws IOException {
61+
long skipped = 0;
62+
while (byteCount > 0) {
63+
fillIfEmpty();
64+
if (validData_ == -1) {
65+
return skipped;
66+
}
67+
int count = (int) Math.min(available(), byteCount);
68+
byteCount -= count;
69+
bufferIndex_ += count;
70+
skipped += count;
71+
}
72+
return skipped;
73+
}
74+
75+
protected boolean positionTo(int value) throws IOException {
76+
fillIfEmpty();
77+
if (validData_ != -1) {
78+
while (bufferIndex_ < buffer_.length) {
79+
if (buffer_[bufferIndex_] == value) {
80+
break;
81+
} else {
82+
bufferIndex_++;
83+
}
84+
}
85+
}
86+
return validData_ != -1;
87+
}
88+
89+
private void fill() throws IOException {
90+
bufferIndex_ = 0;
91+
validData_ = source_.read(buffer_);
92+
}
93+
94+
private void fillIfEmpty() throws IOException {
95+
while (available() == 0 && validData_ != -1) {
96+
fill();
97+
}
98+
}
99+
}

0 commit comments

Comments
 (0)