Commit 824c4279 authored by Evan W. Patton's avatar Evan W. Patton Committed by Evan W. Patton

Inline BluetoothReflection functionality

BluetoothReflection was needed on versions of Android prior to Android
SDK 5. However, we now have a minimum SDK of 7, so the functionality
that was implemented by reflection can now be implemented directly
without issue. This also helps by letting the Android linting tools
verify that we have the right permissions for different functionality,
which is especially relevant given the changes in Android 12 to the
Bluetooth permission model.

Change-Id: I84b2f086c680b48a217da0e398a1847dafdd19da
parent 02a2d5e0
// -*- mode: java; c-basic-offset: 2; -*-
// Copyright 2009-2011 Google, All Rights reserved
// Copyright 2011-2012 MIT, All rights reserved
// Copyright 2011-2023 MIT, All rights reserved
// Released under the Apache License, Version 2.0
// http://www.apache.org/licenses/LICENSE-2.0
package com.google.appinventor.components.runtime;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothSocket;
import android.os.Build;
import android.util.Log;
import com.google.appinventor.components.annotations.DesignerProperty;
import com.google.appinventor.components.annotations.PropertyCategory;
import com.google.appinventor.components.annotations.SimpleEvent;
import com.google.appinventor.components.annotations.SimpleFunction;
import com.google.appinventor.components.annotations.SimpleObject;
import com.google.appinventor.components.annotations.SimpleProperty;
import com.google.appinventor.components.common.PropertyTypeConstants;
import com.google.appinventor.components.runtime.util.BluetoothReflection;
import com.google.appinventor.components.runtime.util.ErrorMessages;
import com.google.appinventor.components.runtime.util.SdkLevel;
import com.google.appinventor.components.runtime.util.SUtil;
import com.google.appinventor.components.runtime.util.YailList;
import android.util.Log;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
......@@ -43,32 +47,32 @@ public abstract class BluetoothConnectionBase extends AndroidNonvisibleComponent
protected final String logTag;
private final List<BluetoothConnectionListener> bluetoothConnectionListeners =
new ArrayList<BluetoothConnectionListener>();
new ArrayList<>();
private ByteOrder byteOrder;
private String encoding;
private byte delimiter;
protected boolean disconnectOnError;
protected boolean secure;
protected final BluetoothAdapter adapter;
private Object connectedBluetoothSocket;
private BluetoothSocket socket;
private InputStream inputStream;
private OutputStream outputStream;
private final int sdkLevel;
/**
* Creates a new BluetoothConnectionBase.
*/
protected BluetoothConnectionBase(ComponentContainer container, String logTag) {
this(container.$form(), logTag, SdkLevel.getLevel());
this(container.$form(), logTag);
form.registerForOnDestroy(this);
}
private BluetoothConnectionBase(Form form, String logTag, int sdkLevel) {
private BluetoothConnectionBase(Form form, String logTag) {
super(form);
this.logTag = logTag;
this.sdkLevel = sdkLevel;
this.disconnectOnError = false;
this.adapter = SUtil.getAdapter(form);
HighByteFirst(false); // Lego Mindstorms NXT is low-endian, so false is a good default.
CharacterEncoding("UTF-8");
......@@ -80,8 +84,8 @@ public abstract class BluetoothConnectionBase extends AndroidNonvisibleComponent
* This constructor is for testing purposes only.
*/
protected BluetoothConnectionBase(OutputStream outputStream, InputStream inputStream) {
this((Form) null, (String) null, SdkLevel.LEVEL_ECLAIR_MR1);
this.connectedBluetoothSocket = "Not Null";
this((Form) null, null);
this.socket = null;
this.outputStream = outputStream;
this.inputStream = inputStream;
}
......@@ -141,11 +145,7 @@ public abstract class BluetoothConnectionBase extends AndroidNonvisibleComponent
@SimpleProperty(description = "Whether Bluetooth is available on the device",
category = PropertyCategory.BEHAVIOR)
public boolean Available() {
Object bluetoothAdapter = BluetoothReflection.getBluetoothAdapter();
if (bluetoothAdapter != null) {
return true;
}
return false;
return adapter != null;
}
/**
......@@ -156,21 +156,16 @@ public abstract class BluetoothConnectionBase extends AndroidNonvisibleComponent
@SimpleProperty(description = "Whether Bluetooth is enabled",
category = PropertyCategory.BEHAVIOR)
public boolean Enabled() {
Object bluetoothAdapter = BluetoothReflection.getBluetoothAdapter();
if (bluetoothAdapter != null) {
if (BluetoothReflection.isBluetoothEnabled(bluetoothAdapter)) {
return true;
}
if (adapter == null) {
return false;
}
return false;
return adapter.isEnabled();
}
protected final void setConnection(Object bluetoothSocket) throws IOException {
connectedBluetoothSocket = bluetoothSocket;
inputStream = new BufferedInputStream(
BluetoothReflection.getInputStream(connectedBluetoothSocket));
outputStream = new BufferedOutputStream(
BluetoothReflection.getOutputStream(connectedBluetoothSocket));
protected final void setConnection(BluetoothSocket bluetoothSocket) throws IOException {
socket = bluetoothSocket;
inputStream = new BufferedInputStream(socket.getInputStream());
outputStream = new BufferedOutputStream(socket.getOutputStream());
fireAfterConnectEvent();
}
......@@ -179,15 +174,15 @@ public abstract class BluetoothConnectionBase extends AndroidNonvisibleComponent
*/
@SimpleFunction(description = "Disconnect from the connected Bluetooth device.")
public final void Disconnect() {
if (connectedBluetoothSocket != null) {
if (socket != null) {
fireBeforeDisconnectEvent();
try {
BluetoothReflection.closeBluetoothSocket(connectedBluetoothSocket);
socket.close();
Log.i(logTag, "Disconnected from Bluetooth device.");
} catch (IOException e) {
Log.w(logTag, "Error while disconnecting: " + e.getMessage());
}
connectedBluetoothSocket = null;
socket = null;
}
inputStream = null;
outputStream = null;
......@@ -201,11 +196,11 @@ public abstract class BluetoothConnectionBase extends AndroidNonvisibleComponent
"this property returned is accurate. But on old devices with API level lower than 14, " +
"it may not return the current state of connection(e.g., it might be disconnected but you " +
"may not know until you attempt to read/write the socket.")
public final boolean IsConnected() {
if (sdkLevel >= SdkLevel.LEVEL_ICE_CREAM_SANDWICH) {
return (connectedBluetoothSocket != null && BluetoothReflection.isBluetoothSocketConnected(connectedBluetoothSocket));
public boolean IsConnected() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
return (socket != null && socket.isConnected());
} else {
return (connectedBluetoothSocket != null);
return (socket != null);
}
}
......@@ -852,7 +847,7 @@ public abstract class BluetoothConnectionBase extends AndroidNonvisibleComponent
}
private void prepareToDie() {
if (connectedBluetoothSocket != null) {
if (socket != null) {
Disconnect();
}
}
......
......@@ -10,6 +10,13 @@ import static android.Manifest.permission.BLUETOOTH;
import static android.Manifest.permission.BLUETOOTH_ADMIN;
import static android.Manifest.permission.BLUETOOTH_ADVERTISE;
import android.bluetooth.BluetoothServerSocket;
import android.bluetooth.BluetoothSocket;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import com.google.appinventor.components.annotations.DesignerComponent;
import com.google.appinventor.components.annotations.PropertyCategory;
import com.google.appinventor.components.annotations.SimpleEvent;
......@@ -17,19 +24,18 @@ import com.google.appinventor.components.annotations.SimpleFunction;
import com.google.appinventor.components.annotations.SimpleObject;
import com.google.appinventor.components.annotations.SimpleProperty;
import com.google.appinventor.components.annotations.UsesPermissions;
import com.google.appinventor.components.common.ComponentCategory;
import com.google.appinventor.components.common.YaVersion;
import com.google.appinventor.components.runtime.util.AsynchUtil;
import com.google.appinventor.components.runtime.util.BluetoothReflection;
import com.google.appinventor.components.runtime.util.ErrorMessages;
import com.google.appinventor.components.runtime.util.SUtil;
import com.google.appinventor.components.runtime.util.SdkLevel;
import android.os.Handler;
import android.util.Log;
import java.io.IOException;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicReference;
/**
......@@ -50,15 +56,15 @@ public final class BluetoothServer extends BluetoothConnectionBase {
private final Handler androidUIHandler;
private final AtomicReference<Object> arBluetoothServerSocket;
private final AtomicReference<BluetoothServerSocket> arBluetoothServerSocket;
/**
* Creates a new BluetoothServer.
*/
public BluetoothServer(ComponentContainer container) {
super(container, "BluetoothServer");
androidUIHandler = new Handler();
arBluetoothServerSocket = new AtomicReference<Object>();
androidUIHandler = new Handler(Looper.getMainLooper());
arBluetoothServerSocket = new AtomicReference<>();
}
/**
......@@ -88,14 +94,13 @@ public final class BluetoothServer extends BluetoothConnectionBase {
})) {
return;
}
final Object bluetoothAdapter = BluetoothReflection.getBluetoothAdapter();
if (bluetoothAdapter == null) {
if (adapter == null) {
form.dispatchErrorOccurredEvent(this, functionName,
ErrorMessages.ERROR_BLUETOOTH_NOT_AVAILABLE);
return;
}
if (!BluetoothReflection.isBluetoothEnabled(bluetoothAdapter)) {
if (!adapter.isEnabled()) {
form.dispatchErrorOccurredEvent(this, functionName,
ErrorMessages.ERROR_BLUETOOTH_NOT_ENABLED);
return;
......@@ -111,16 +116,14 @@ public final class BluetoothServer extends BluetoothConnectionBase {
}
try {
Object bluetoothServerSocket;
if (!secure && SdkLevel.getLevel() >= SdkLevel.LEVEL_GINGERBREAD_MR1) {
BluetoothServerSocket socket;
if (!secure && Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD_MR1) {
// listenUsingInsecureRfcommWithServiceRecord was introduced in level 10
bluetoothServerSocket = BluetoothReflection.listenUsingInsecureRfcommWithServiceRecord(
bluetoothAdapter, name, uuid);
socket = adapter.listenUsingInsecureRfcommWithServiceRecord(name, uuid);
} else {
bluetoothServerSocket = BluetoothReflection.listenUsingRfcommWithServiceRecord(
bluetoothAdapter, name, uuid);
socket = adapter.listenUsingRfcommWithServiceRecord(name, uuid);
}
arBluetoothServerSocket.set(bluetoothServerSocket);
arBluetoothServerSocket.set(socket);
} catch (IOException e) {
form.dispatchErrorOccurredEvent(this, functionName,
ErrorMessages.ERROR_BLUETOOTH_UNABLE_TO_LISTEN);
......@@ -129,13 +132,13 @@ public final class BluetoothServer extends BluetoothConnectionBase {
AsynchUtil.runAsynchronously(new Runnable() {
public void run() {
Object acceptedBluetoothSocket = null;
BluetoothSocket acceptedSocket = null;
Object bluetoothServerSocket = arBluetoothServerSocket.get();
if (bluetoothServerSocket != null) {
BluetoothServerSocket serverSocket = arBluetoothServerSocket.get();
if (serverSocket != null) {
try {
try {
acceptedBluetoothSocket = BluetoothReflection.accept(bluetoothServerSocket);
acceptedSocket = serverSocket.accept();
} catch (IOException e) {
androidUIHandler.post(new Runnable() {
public void run() {
......@@ -150,9 +153,9 @@ public final class BluetoothServer extends BluetoothConnectionBase {
}
}
if (acceptedBluetoothSocket != null) {
if (acceptedSocket != null) {
// Call setConnection and signal the event on the main thread.
final Object bluetoothSocket = acceptedBluetoothSocket;
final BluetoothSocket bluetoothSocket = acceptedSocket;
androidUIHandler.post(new Runnable() {
public void run() {
try {
......@@ -186,10 +189,10 @@ public final class BluetoothServer extends BluetoothConnectionBase {
*/
@SimpleFunction(description = "Stop accepting an incoming connection.")
public void StopAccepting() {
Object bluetoothServerSocket = arBluetoothServerSocket.getAndSet(null);
if (bluetoothServerSocket != null) {
BluetoothServerSocket serverSocket = arBluetoothServerSocket.getAndSet(null);
if (serverSocket != null) {
try {
BluetoothReflection.closeBluetoothServerSocket(bluetoothServerSocket);
serverSocket.close();
} catch (IOException e) {
Log.w(logTag, "Error while closing bluetooth server socket: " + e.getMessage());
}
......
......@@ -12,6 +12,13 @@ import static android.Manifest.permission.BLUETOOTH_ADVERTISE;
import static android.Manifest.permission.BLUETOOTH_CONNECT;
import static android.Manifest.permission.BLUETOOTH_SCAN;
import static android.content.Context.BLUETOOTH_SERVICE;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothManager;
import android.content.Context;
import android.os.Build;
import com.google.appinventor.components.runtime.BluetoothClient;
......@@ -139,6 +146,21 @@ public class SUtil {
return performRequest(form, source, caller, permsNeeded, continuation);
}
/**
* Obtains a reference to the device's preferred BluetoothAdapter.
*
* @param context an Android context to use for accessing the Bluetooth service
* @return a BluetoothAdapter reference, or null if Bluetooth is not supported
*/
public static BluetoothAdapter getAdapter(Context context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
BluetoothManager manager = (BluetoothManager) context.getSystemService(BLUETOOTH_SERVICE);
return manager.getAdapter();
} else {
return BluetoothAdapter.getDefaultAdapter();
}
}
private static boolean performRequest(Form form, Component source, String caller,
final List<String> permsNeeded, final PermissionResultHandler continuation) {
boolean ready = true;
......
......@@ -6,40 +6,56 @@
package com.google.appinventor.components.runtime;
import static org.junit.Assert.assertEquals;
import com.google.appinventor.components.runtime.util.ErrorMessages;
import com.google.appinventor.components.runtime.util.YailList;
import junit.framework.TestCase;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.util.ArrayList;
import java.util.List;
import org.junit.Test;
import org.robolectric.annotation.Config;
import org.robolectric.shadows.ShadowBluetoothAdapter;
/**
* Tests BluetoothConnectionBase.java.
*
* @author lizlooney@google.com (Liz Looney)
*/
public class BluetoothConnectionBaseTest extends TestCase {
@Config(shadows = { ShadowBluetoothAdapter.class })
public class BluetoothConnectionBaseTest extends RobolectricTestBase {
private BluetoothConnectionBase connection;
private ByteArrayOutputStream outputStream;
private int recordedErrorNumber;
private PipedOutputStream pipe;
@Override
protected void setUp() throws Exception {
public void setUp() {
super.setUp();
outputStream = new ByteArrayOutputStream();
pipe = new PipedOutputStream();
connection = new BluetoothConnectionBase(outputStream, new PipedInputStream(pipe)) {
PipedInputStream inputStream;
try {
inputStream = new PipedInputStream(pipe);
} catch (IOException e) {
throw new IllegalStateException("Unable to create piped input stream");
}
connection = new BluetoothConnectionBase(outputStream, inputStream) {
@Override
protected void bluetoothError(String functionName, int errorNumber, Object... messageArgs) {
recordedErrorNumber = errorNumber;
}
@Override
protected void write(String functionName, byte b) {
super.write(functionName, b);
......@@ -49,6 +65,7 @@ public class BluetoothConnectionBaseTest extends TestCase {
throw new RuntimeException(e);
}
}
@Override
protected void write(String functionName, byte[] bytes) {
super.write(functionName, bytes);
......@@ -58,9 +75,15 @@ public class BluetoothConnectionBaseTest extends TestCase {
throw new RuntimeException(e);
}
}
@Override
public boolean IsConnected() {
return true;
}
};
}
@Test
public void testSendAndReceiveText() {
connection.SendText("Hello");
assertEquals(5, connection.BytesAvailableToReceive());
......@@ -111,6 +134,7 @@ public class BluetoothConnectionBaseTest extends TestCase {
assertEquals((byte) 10, bytes[i++]); // line feed
}
@Test
public void testSendandReceive1ByteNumber() {
connection.Send1ByteNumber("0");
assertEquals(0, connection.ReceiveUnsigned1ByteNumber());
......@@ -157,6 +181,7 @@ public class BluetoothConnectionBaseTest extends TestCase {
assertEquals((byte) 0xAB, bytes[i++]); // 0xab
}
@Test
public void testSendAndReceive2ByteNumber() {
connection.HighByteFirst(true);
connection.Send2ByteNumber("0");
......@@ -258,6 +283,7 @@ public class BluetoothConnectionBaseTest extends TestCase {
}
@Test
public void testSendAndReceive4ByteNumber() {
connection.HighByteFirst(true);
connection.Send4ByteNumber("0");
......@@ -398,6 +424,7 @@ public class BluetoothConnectionBaseTest extends TestCase {
assertEquals((byte) 0x00, bytes[i++]);
}
@Test
public void testSendAndReceiveBytes() {
List<Object> list = new ArrayList<Object>();
list.add((byte) 0);
......
......@@ -260,7 +260,8 @@ set the following properties:
numberOfBytes parameter when calling ReceiveText, ReceiveSignedBytes, or
ReceiveUnsignedBytes.</dd>
<dt id="BluetoothClient.DisconnectOnError" class="boolean"><em>DisconnectOnError</em></dt>
<dd>Specifies whether BluetoothClient/BluetoothServer should be disconnected automatically when an error occurs.</dd>
<dd>Specifies whether BluetoothClient should be disconnected
automatically when an error occurs.</dd>
<dt id="BluetoothClient.Enabled" class="boolean ro bo"><em>Enabled</em></dt>
<dd>Returns <code class="logic block highlighter-rouge">true</code> if Bluetooth is enabled, <code class="logic block highlighter-rouge">false</code> otherwise.</dd>
<dt id="BluetoothClient.HighByteFirst" class="boolean"><em>HighByteFirst</em></dt>
......
......@@ -144,7 +144,8 @@ Use `BluetoothClient` to connect your device to other devices using Bluetooth. T
ReceiveUnsignedBytes.
{:id="BluetoothClient.DisconnectOnError" .boolean} *DisconnectOnError*
: Specifies whether BluetoothClient/BluetoothServer should be disconnected automatically when an error occurs.
: Specifies whether BluetoothClient should be disconnected
automatically when an error occurs.
{:id="BluetoothClient.Enabled" .boolean .ro .bo} *Enabled*
: Returns `true`{:.logic.block} if Bluetooth is enabled, `false`{:.logic.block} otherwise.
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment