Skip to content

Commit 3dab3ea

Browse files
committed
IOIO: update USB permission handling
1 parent 835f4c1 commit 3dab3ea

File tree

9 files changed

+284
-361
lines changed

9 files changed

+284
-361
lines changed
Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,13 @@
11
<?xml version="1.0" encoding="utf-8"?>
22
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
3-
<uses-feature android:name="android.hardware.usb.accessory" android:required="false" />
3+
<uses-feature android:name="android.hardware.usb.accessory" android:required="true" />
4+
<uses-permission android:name="android.permission.USB_PERMISSION" />
5+
<application>
6+
<receiver android:name="ioio.smallbasic.android.AccessoryPermissionCheck" android:exported="true">
7+
<intent-filter>
8+
<action android:name="android.hardware.usb.action.USB_PERMISSION" />
9+
</intent-filter>
10+
</receiver>
11+
<meta-data android:name="android.hardware.usb.action.USB_ACCESSORY_ATTACHED" android:resource="@xml/device_filter" />
12+
</application>
413
</manifest>

ioio/ioio/src/main/java/ioio/smallbasic/ConnectionController.java

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,16 @@
88
public class ConnectionController extends IOIOBaseApplicationHelper {
99
private final IOIOConnectionManager manager = new IOIOConnectionManager(this);
1010
private static final boolean isAndroid;
11-
private static final String ANDROID_BOOTSTRAP = "ioio.smallbasic.android.AccessoryConnectionBootstrap";
12-
private static final String PERMISSION_MANAGER = "ioio.smallbasic.android.AccessoryPermissionManager";
13-
private static final String DESKTOP_BOOTSTRAP = "ioio.smallbasic.pc.SerialPortIOIOConnectionBootstrap";
11+
private static final String ACCESSORY_BOOTSTRAP = "ioio.smallbasic.android.AccessoryConnectionBootstrap";
12+
private static final String SERIAL_PORT_BOOTSTRAP = "ioio.smallbasic.pc.SerialPortIOIOConnectionBootstrap";
13+
private static final String PERMISSION_CHECK = "ioio.smallbasic.android.AccessoryPermissionCheck";
1414

1515
static {
1616
isAndroid = getIsAndroidBuild();
1717
if (isAndroid) {
18-
IOIOConnectionRegistry.addBootstraps(new String[] { ANDROID_BOOTSTRAP });
18+
IOIOConnectionRegistry.addBootstraps(new String[] { ACCESSORY_BOOTSTRAP });
1919
} else {
20-
IOIOConnectionRegistry.addBootstraps(new String[] { DESKTOP_BOOTSTRAP });
20+
IOIOConnectionRegistry.addBootstraps(new String[] { SERIAL_PORT_BOOTSTRAP });
2121
}
2222
}
2323

@@ -27,9 +27,9 @@ public ConnectionController(IOIOLooperProvider provider) {
2727

2828
public void start() {
2929
if (isAndroid) {
30-
validateAccessoryPermission();
30+
permitAccessory();
3131
}
32-
//manager.start();
32+
manager.start();
3333
}
3434

3535
public void stop() {
@@ -39,20 +39,20 @@ public void stop() {
3939
private static boolean getIsAndroidBuild() {
4040
boolean result;
4141
try {
42-
Class.forName(ANDROID_BOOTSTRAP);
42+
Class.forName(ACCESSORY_BOOTSTRAP);
4343
result = true;
4444
} catch (ClassNotFoundException e) {
4545
result = false;
4646
}
4747
return result;
4848
}
4949

50-
private static void validateAccessoryPermission() {
50+
private static void permitAccessory() {
5151
try {
52-
Class.forName(PERMISSION_MANAGER).newInstance();
52+
Class.forName(PERMISSION_CHECK).newInstance();
5353
}
5454
catch (Exception e) {
55-
throw new IOIOException(e.toString());
55+
throw new IOIOException(e);
5656
}
5757
}
5858
}

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

Lines changed: 10 additions & 255 deletions
Original file line numberDiff line numberDiff line change
@@ -29,57 +29,32 @@
2929

3030
package ioio.smallbasic.android;
3131

32-
import android.app.PendingIntent;
3332
import android.content.Context;
3433
import android.hardware.usb.UsbAccessory;
3534
import android.hardware.usb.UsbManager;
36-
import android.os.ParcelFileDescriptor;
3735

38-
import java.io.BufferedOutputStream;
39-
import java.io.FileDescriptor;
40-
import java.io.FileInputStream;
41-
import java.io.FileOutputStream;
42-
import java.io.IOException;
43-
import java.io.InputStream;
44-
import java.io.OutputStream;
4536
import java.util.Collection;
4637

4738
import ioio.lib.api.IOIOConnection;
48-
import ioio.lib.api.exception.ConnectionLostException;
49-
import ioio.lib.impl.FixedReadBufferedInputStream;
5039
import ioio.lib.spi.IOIOConnectionBootstrap;
5140
import ioio.lib.spi.IOIOConnectionFactory;
5241
import ioio.lib.spi.Log;
5342
import ioio.lib.spi.NoRuntimeSupportException;
5443

55-
public class AccessoryConnectionBootstrap //extends BroadcastReceiver
56-
implements IOIOConnectionBootstrap, IOIOConnectionFactory {
44+
public class AccessoryConnectionBootstrap implements IOIOConnectionBootstrap, IOIOConnectionFactory {
5745
private static final String TAG = AccessoryConnectionBootstrap.class.getSimpleName();
58-
private static final String ACTION_USB_PERMISSION = "ioio.lib.accessory.action.USB_PERMISSION";
59-
private static final String EXTRA_PERMISSION_GRANTED = UsbManager.EXTRA_PERMISSION_GRANTED;
60-
61-
private final Context activity;
62-
private final UsbManager usbManager;
63-
private boolean shouldTryOpen = false;
64-
private PendingIntent pendingIntent;
65-
private ParcelFileDescriptor fileDescriptor;
66-
private InputStream inputStream;
67-
private OutputStream outputStream;
68-
69-
private enum InstanceState {
70-
INIT, CONNECTED, DEAD
71-
}
7246

7347
public AccessoryConnectionBootstrap() throws NoRuntimeSupportException {
7448
Log.d(TAG, "creating AccessoryConnectionBootstrap");
75-
this.activity = IOIOLoader.getContext();
76-
this.usbManager = (UsbManager) activity.getSystemService(Context.USB_SERVICE);
77-
// registerReceiver();
7849
}
7950

8051
@Override
8152
public IOIOConnection createConnection() {
82-
return new Connection();
53+
Log.i(TAG, "createConnection");
54+
Context activity = IOIOLoader.getContext();
55+
UsbManager usbManager = (UsbManager) activity.getSystemService(Context.USB_SERVICE);
56+
UsbAccessory accessory = usbManager.getAccessoryList()[0];
57+
return new BluetoothConnection(getUsbManager().openAccessory(accessory));
8358
}
8459

8560
@Override
@@ -94,231 +69,11 @@ public void getFactories(Collection<IOIOConnectionFactory> result) {
9469

9570
@Override
9671
public String getType() {
97-
return Connection.class.getCanonicalName();
98-
}
99-
100-
// //@Override
101-
// public void onDestroy() {
102-
// activity.unregisterReceiver(this);
103-
// }
104-
105-
// @Override
106-
// public synchronized void onReceive(Context context, Intent intent) {
107-
// final String action = intent.getAction();
108-
// if (ACTION_USB_PERMISSION.equals(action)) {
109-
// pendingIntent = null;
110-
// if (intent.getBooleanExtra(EXTRA_PERMISSION_GRANTED, false)) {
111-
// notifyAll();
112-
// } else {
113-
// Log.e(TAG, "Permission denied");
114-
// }
115-
// }
116-
// }
117-
118-
//@Override
119-
public synchronized void open() {
120-
notifyAll();
72+
return BluetoothConnection.class.getCanonicalName();
12173
}
12274

123-
//@Override
124-
public synchronized void reopen() {
125-
notifyAll();
126-
}
127-
128-
private synchronized void disconnect() {
129-
// This should abort any current open attempt.
130-
Log.d(TAG, "private disconnect");
131-
shouldTryOpen = false;
132-
notifyAll();
133-
134-
// And this should kill any ongoing connections.
135-
if (fileDescriptor != null) {
136-
try {
137-
fileDescriptor.close();
138-
} catch (IOException e) {
139-
Log.e(TAG, "Failed to close file descriptor.", e);
140-
}
141-
fileDescriptor = null;
142-
}
143-
144-
if (pendingIntent != null) {
145-
pendingIntent.cancel();
146-
pendingIntent = null;
147-
}
148-
Log.d(TAG, "leaving private disconnect");
149-
}
150-
151-
private void forceWait() {
152-
try {
153-
wait();
154-
} catch (InterruptedException e) {
155-
Log.e(TAG, "Do not interrupt me!");
156-
}
157-
}
158-
159-
// @TargetApi(Build.VERSION_CODES.TIRAMISU)
160-
// private void registerReceiver() {
161-
// IntentFilter filter = new IntentFilter(ACTION_USB_PERMISSION);
162-
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
163-
// activity.registerReceiver(this, filter, Context.RECEIVER_NOT_EXPORTED);
164-
// }
165-
// }
166-
167-
private boolean tryOpen() {
168-
// Find the accessory.
169-
UsbAccessory[] accessories = usbManager.getAccessoryList();
170-
UsbAccessory accessory = (accessories == null ? null : accessories[0]);
171-
172-
if (accessory == null) {
173-
Log.v(TAG, "No accessory found.");
174-
return false;
175-
}
176-
177-
// Check for permission to access the accessory.
178-
if (!usbManager.hasPermission(accessory)) {
179-
// if (pendingIntent == null) {
180-
// Log.v(TAG, "Requesting permission.");
181-
// pendingIntent = PendingIntent.getBroadcast(activity, 0, new Intent(ACTION_USB_PERMISSION), PendingIntent.FLAG_IMMUTABLE);
182-
// usbManager.requestPermission(accessory, pendingIntent);
183-
// }
184-
return false;
185-
}
186-
187-
boolean success = false;
188-
189-
// From this point on, if anything goes wrong, we're responsible for canceling the intent.
190-
try {
191-
// Obtain a file descriptor.
192-
fileDescriptor = usbManager.openAccessory(accessory);
193-
if (fileDescriptor == null) {
194-
Log.v(TAG, "Failed to open file descriptor.");
195-
return false;
196-
}
197-
198-
// From this point on, if anything goes wrong, we're responsible for closing the file
199-
// descriptor.
200-
try {
201-
FileDescriptor fd = fileDescriptor.getFileDescriptor();
202-
// Apparently, some Android devices (e.g. Nexus 5) only support read operations of
203-
// multiples of the endpoint buffer size. So there you have it!
204-
inputStream = new FixedReadBufferedInputStream(new FileInputStream(fd), 1024);
205-
outputStream = new BufferedOutputStream(new FileOutputStream(fd), 1024);
206-
207-
// Soft-open the connection
208-
outputStream.write(0x00);
209-
outputStream.flush();
210-
211-
// We're going to block now. We're counting on the IOIO to
212-
// write back a byte, or otherwise we're locked until
213-
// physical disconnection. This is a known OpenAccessory
214-
// bug:
215-
// http://code.google.com/p/android/issues/detail?id=20545
216-
while (inputStream.read() != 1) {
217-
trySleep();
218-
}
219-
220-
success = true;
221-
return true;
222-
} catch (IOException e) {
223-
Log.v(TAG, "Failed to open streams", e);
224-
return false;
225-
} finally {
226-
if (!success) {
227-
try {
228-
fileDescriptor.close();
229-
} catch (IOException e) {
230-
Log.e(TAG, "Failed to close file descriptor.", e);
231-
}
232-
fileDescriptor = null;
233-
}
234-
}
235-
} finally {
236-
if (!success && pendingIntent != null) {
237-
pendingIntent.cancel();
238-
pendingIntent = null;
239-
}
240-
}
241-
}
242-
243-
private void trySleep() {
244-
synchronized (AccessoryConnectionBootstrap.this) {
245-
try {
246-
AccessoryConnectionBootstrap.this.wait(1000);
247-
} catch (InterruptedException e) {
248-
Log.e(TAG, e.toString());
249-
}
250-
}
251-
}
252-
253-
private synchronized void waitForConnect(Connection connection) throws ConnectionLostException {
254-
// In order to simplify the connection process in face of many different sequences of events
255-
// that might occur, we collapsed the entire sequence into one non-blocking method,
256-
// tryOpen(), which tries the entire process from the beginning, undoes everything if
257-
// something along the way fails and always returns immediately.
258-
// This method, simply calls tryOpen() in a loop until it succeeds or until we're no longer
259-
// interested. Between attempts, it waits until "something interesting" has happened, which
260-
// may be permission granted, the client telling us to try again (via reopen()) or stop
261-
// trying, etc.
262-
shouldTryOpen = true;
263-
while (shouldTryOpen) {
264-
if (tryOpen()) {
265-
// Success!
266-
return;
267-
}
268-
forceWait();
269-
}
270-
throw new ConnectionLostException();
271-
}
272-
273-
private class Connection implements IOIOConnection {
274-
private InstanceState instanceState_ = InstanceState.INIT;
275-
276-
@Override
277-
public boolean canClose() {
278-
return false;
279-
}
280-
281-
@Override
282-
public void disconnect() {
283-
Log.d(TAG, "disconnect");
284-
synchronized(AccessoryConnectionBootstrap.this) {
285-
if (instanceState_ != InstanceState.DEAD) {
286-
AccessoryConnectionBootstrap.this.disconnect();
287-
instanceState_ = InstanceState.DEAD;
288-
}
289-
}
290-
}
291-
292-
@Override
293-
public InputStream getInputStream() {
294-
return inputStream;
295-
}
296-
297-
@Override
298-
public OutputStream getOutputStream() {
299-
return outputStream;
300-
}
301-
302-
@Override
303-
public void waitForConnect() throws ConnectionLostException {
304-
synchronized(AccessoryConnectionBootstrap.this) {
305-
if (instanceState_ != InstanceState.INIT) {
306-
throw new IllegalStateException("waitForConnect() may only be called once");
307-
}
308-
309-
try {
310-
AccessoryConnectionBootstrap.this.waitForConnect(this);
311-
instanceState_ = InstanceState.CONNECTED;
312-
} catch (ConnectionLostException e) {
313-
instanceState_ = InstanceState.DEAD;
314-
throw e;
315-
}
316-
}
317-
}
318-
319-
@Override
320-
protected void finalize() {
321-
disconnect();
322-
}
75+
private UsbManager getUsbManager() {
76+
Context activity = IOIOLoader.getContext();
77+
return (UsbManager) activity.getSystemService(Context.USB_SERVICE);
32378
}
32479
}

0 commit comments

Comments
 (0)