From 135452008607211d49f83be7dc8ae65a094c7217 Mon Sep 17 00:00:00 2001 From: joonhaengHeo <85541460+joonhaengHeo@users.noreply.github.com> Date: Sat, 13 May 2023 03:58:04 +0900 Subject: [PATCH] [Android] Implement pairing with code API (#26324) * Add pairWithCode API in Android * restyle * fix build error * Add java controller function --- .../com/google/chip/chiptool/ChipClient.kt | 2 +- .../controller/commands/common/Argument.kt | 17 + .../commands/pairing/PairCodeCommand.kt | 12 +- .../commands/pairing/PairCodeThreadCommand.kt | 12 +- .../commands/pairing/PairCodeWifiCommand.kt | 12 +- .../commands/pairing/PairingCommand.kt | 17 + .../java/CHIPDeviceController-JNI.cpp | 36 ++ .../ChipDeviceController.java | 24 + .../android/AndroidChipPlatform-JNI.cpp | 5 + src/platform/android/BLEManagerImpl.cpp | 22 +- src/platform/android/BLEManagerImpl.h | 3 + src/platform/android/BUILD.gn | 2 + .../android/BleConnectCallback-JNI.cpp | 54 ++ src/platform/android/BleConnectCallback-JNI.h | 29 ++ .../java/chip/platform/AndroidBleManager.java | 460 ++++++++++++++---- .../chip/platform/BleConnectCallback.java | 40 ++ .../java/chip/platform/BleManager.java | 3 +- 17 files changed, 642 insertions(+), 108 deletions(-) create mode 100644 src/platform/android/BleConnectCallback-JNI.cpp create mode 100644 src/platform/android/BleConnectCallback-JNI.h create mode 100644 src/platform/android/java/chip/platform/BleConnectCallback.java diff --git a/examples/android/CHIPTool/app/src/main/java/com/google/chip/chiptool/ChipClient.kt b/examples/android/CHIPTool/app/src/main/java/com/google/chip/chiptool/ChipClient.kt index add408b7af9693..84a14de4714e13 100644 --- a/examples/android/CHIPTool/app/src/main/java/com/google/chip/chiptool/ChipClient.kt +++ b/examples/android/CHIPTool/app/src/main/java/com/google/chip/chiptool/ChipClient.kt @@ -61,7 +61,7 @@ object ChipClient { if (!this::androidPlatform.isInitialized && context != null) { //force ChipDeviceController load jni ChipDeviceController.loadJni() - androidPlatform = AndroidChipPlatform(AndroidBleManager(), PreferencesKeyValueStoreManager(context), PreferencesConfigurationManager(context), NsdManagerServiceResolver(context), NsdManagerServiceBrowser(context), ChipMdnsCallbackImpl(), DiagnosticDataProviderImpl(context)) + androidPlatform = AndroidChipPlatform(AndroidBleManager(context), PreferencesKeyValueStoreManager(context), PreferencesConfigurationManager(context), NsdManagerServiceResolver(context), NsdManagerServiceBrowser(context), ChipMdnsCallbackImpl(), DiagnosticDataProviderImpl(context)) } return androidPlatform diff --git a/examples/java-matter-controller/java/src/com/matter/controller/commands/common/Argument.kt b/examples/java-matter-controller/java/src/com/matter/controller/commands/common/Argument.kt index 76e633a82a144a..77aa91a89aa31d 100644 --- a/examples/java-matter-controller/java/src/com/matter/controller/commands/common/Argument.kt +++ b/examples/java-matter-controller/java/src/com/matter/controller/commands/common/Argument.kt @@ -141,6 +141,23 @@ class Argument { isValidArgument = numLong.toInt() >= minValue && numLong.toInt() <= maxValue } + ArgumentType.STRING -> { + val stringBuffer = this.value as StringBuffer + stringBuffer.append(value) + val str = stringBuffer.toString() + isValidArgument = value == str + } + + ArgumentType.BOOL -> { + val atomicBoolean = this.value as AtomicBoolean + try { + atomicBoolean.set(value.toBoolean()) + isValidArgument = true + } catch (e: Exception) { + isValidArgument = false + } + } + ArgumentType.ADDRESS -> isValidArgument = try { val ipAddress = this.value as IPAddress ipAddress.setAddress(InetAddress.getByName(value)) diff --git a/examples/java-matter-controller/java/src/com/matter/controller/commands/pairing/PairCodeCommand.kt b/examples/java-matter-controller/java/src/com/matter/controller/commands/pairing/PairCodeCommand.kt index 7f2bc51e5ffb4a..bf6e18cceedafd 100644 --- a/examples/java-matter-controller/java/src/com/matter/controller/commands/pairing/PairCodeCommand.kt +++ b/examples/java-matter-controller/java/src/com/matter/controller/commands/pairing/PairCodeCommand.kt @@ -22,5 +22,15 @@ import com.matter.controller.commands.common.CredentialsIssuer class PairCodeCommand(controller: ChipDeviceController, credsIssue: CredentialsIssuer?) : PairingCommand(controller, "code", credsIssue, PairingModeType.CODE, PairingNetworkType.NONE) { - override fun runCommand() {} + override fun runCommand() { + currentCommissioner() + .pairDeviceWithCode( + getNodeId(), + getOnboardingPayload(), + null, + getWifiNetworkCredentials(), + ) + currentCommissioner().setCompletionListener(this) + waitCompleteMs(getTimeoutMillis()) + } } \ No newline at end of file diff --git a/examples/java-matter-controller/java/src/com/matter/controller/commands/pairing/PairCodeThreadCommand.kt b/examples/java-matter-controller/java/src/com/matter/controller/commands/pairing/PairCodeThreadCommand.kt index 2cd822e32d467f..419525733c496a 100644 --- a/examples/java-matter-controller/java/src/com/matter/controller/commands/pairing/PairCodeThreadCommand.kt +++ b/examples/java-matter-controller/java/src/com/matter/controller/commands/pairing/PairCodeThreadCommand.kt @@ -22,5 +22,15 @@ import com.matter.controller.commands.common.CredentialsIssuer class PairCodeThreadCommand(controller: ChipDeviceController, credsIssue: CredentialsIssuer?) : PairingCommand(controller, "code-thread", credsIssue, PairingModeType.CODE, PairingNetworkType.THREAD) { - override fun runCommand() {} + override fun runCommand() { + currentCommissioner() + .pairDeviceWithCode( + getNodeId(), + getOnboardingPayload(), + null, + getThreadNetworkCredentials(), + ) + currentCommissioner().setCompletionListener(this) + waitCompleteMs(getTimeoutMillis()) + } } \ No newline at end of file diff --git a/examples/java-matter-controller/java/src/com/matter/controller/commands/pairing/PairCodeWifiCommand.kt b/examples/java-matter-controller/java/src/com/matter/controller/commands/pairing/PairCodeWifiCommand.kt index 66f90fd7214cf9..ffecb2e9529df8 100644 --- a/examples/java-matter-controller/java/src/com/matter/controller/commands/pairing/PairCodeWifiCommand.kt +++ b/examples/java-matter-controller/java/src/com/matter/controller/commands/pairing/PairCodeWifiCommand.kt @@ -22,5 +22,15 @@ import com.matter.controller.commands.common.CredentialsIssuer class PairCodeWifiCommand(controller: ChipDeviceController, credsIssue: CredentialsIssuer?) : PairingCommand(controller, "code-wifi", credsIssue, PairingModeType.CODE, PairingNetworkType.WIFI) { - override fun runCommand() {} + override fun runCommand() { + currentCommissioner() + .pairDeviceWithCode( + getNodeId(), + getOnboardingPayload(), + null, + getWifiNetworkCredentials(), + ) + currentCommissioner().setCompletionListener(this) + waitCompleteMs(getTimeoutMillis()) + } } \ No newline at end of file diff --git a/examples/java-matter-controller/java/src/com/matter/controller/commands/pairing/PairingCommand.kt b/examples/java-matter-controller/java/src/com/matter/controller/commands/pairing/PairingCommand.kt index a6df1b0b450f6a..e7f90832d0c25c 100644 --- a/examples/java-matter-controller/java/src/com/matter/controller/commands/pairing/PairingCommand.kt +++ b/examples/java-matter-controller/java/src/com/matter/controller/commands/pairing/PairingCommand.kt @@ -18,6 +18,7 @@ package com.matter.controller.commands.pairing import chip.devicecontroller.ChipDeviceController +import chip.devicecontroller.NetworkCredentials import com.matter.controller.commands.common.CredentialsIssuer import com.matter.controller.commands.common.IPAddress import com.matter.controller.commands.common.MatterCommand @@ -227,6 +228,22 @@ abstract class PairingCommand( return timeoutMillis.get() } + fun getOnboardingPayload(): String { + return onboardingPayload.toString() + } + + fun getWifiNetworkCredentials(): NetworkCredentials { + return NetworkCredentials.forWiFi(NetworkCredentials.WiFiCredentials(ssid.toString(), password.toString())) + } + + fun getThreadNetworkCredentials(): NetworkCredentials { + return NetworkCredentials.forThread(NetworkCredentials.ThreadCredentials(operationalDataset.toString().hexToByteArray())) + } + + private fun String.hexToByteArray(): ByteArray { + return chunked(2).map { byteStr -> byteStr.toUByte(16).toByte() }.toByteArray() + } + companion object { private val logger = Logger.getLogger(PairingCommand::class.java.name) } diff --git a/src/controller/java/CHIPDeviceController-JNI.cpp b/src/controller/java/CHIPDeviceController-JNI.cpp index b12d31b7d81ca5..9b40721795d0a4 100644 --- a/src/controller/java/CHIPDeviceController-JNI.cpp +++ b/src/controller/java/CHIPDeviceController-JNI.cpp @@ -653,6 +653,42 @@ JNI_METHOD(void, pairDeviceWithAddress) } } +JNI_METHOD(void, pairDeviceWithCode) +(JNIEnv * env, jobject self, jlong handle, jlong deviceId, jstring setUpCode, jbyteArray csrNonce, jobject networkCredentials) +{ + chip::DeviceLayer::StackLock lock; + CHIP_ERROR err = CHIP_NO_ERROR; + AndroidDeviceControllerWrapper * wrapper = AndroidDeviceControllerWrapper::FromJNIHandle(handle); + + ChipLogProgress(Controller, "pairDeviceWithCode() called"); + + JniUtfString setUpCodeJniString(env, setUpCode); + + CommissioningParameters commissioningParams = wrapper->GetCommissioningParameters(); + if (csrNonce != nullptr) + { + JniByteArray jniCsrNonce(env, csrNonce); + commissioningParams.SetCSRNonce(jniCsrNonce.byteSpan()); + } + + if (networkCredentials != nullptr) + { + wrapper->ApplyNetworkCredentials(commissioningParams, networkCredentials); + } + + if (wrapper->GetDeviceAttestationDelegateBridge() != nullptr) + { + commissioningParams.SetDeviceAttestationDelegate(wrapper->GetDeviceAttestationDelegateBridge()); + } + err = wrapper->Controller()->PairDevice(deviceId, setUpCodeJniString.c_str(), commissioningParams, DiscoveryType::kAll); + + if (err != CHIP_NO_ERROR) + { + ChipLogError(Controller, "Failed to pair the device."); + JniReferences::GetInstance().ThrowError(env, sChipDeviceControllerExceptionCls, err); + } +} + JNI_METHOD(void, establishPaseConnection)(JNIEnv * env, jobject self, jlong handle, jlong deviceId, jint connObj, jlong pinCode) { chip::DeviceLayer::StackLock lock; diff --git a/src/controller/java/src/chip/devicecontroller/ChipDeviceController.java b/src/controller/java/src/chip/devicecontroller/ChipDeviceController.java index 652e53c038bfaa..ed8e09873fc02e 100644 --- a/src/controller/java/src/chip/devicecontroller/ChipDeviceController.java +++ b/src/controller/java/src/chip/devicecontroller/ChipDeviceController.java @@ -175,6 +175,23 @@ public void pairDeviceWithAddress( deviceControllerPtr, deviceId, address, port, discriminator, pinCode, csrNonce); } + /** + * Pair a device connected using the scanned QR code or manual entry code. + * + * @param deviceId the node ID to assign to the device + * @param setupCode the scanned QR code or manual entry code + * @param csrNonce the 32-byte CSR nonce to use, or null if we want to use an internally randomly + * generated CSR nonce. + * @param networkCredentials the credentials (Wi-Fi or Thread) to be provisioned + */ + public void pairDeviceWithCode( + long deviceId, + String setupCode, + @Nullable byte[] csrNonce, + @Nullable NetworkCredentials networkCredentials) { + pairDeviceWithCode(deviceControllerPtr, deviceId, setupCode, csrNonce, networkCredentials); + } + public void establishPaseConnection(long deviceId, int connId, long setupPincode) { if (connectionId == 0) { connectionId = connId; @@ -996,6 +1013,13 @@ private native void pairDeviceWithAddress( long pinCode, @Nullable byte[] csrNonce); + private native void pairDeviceWithCode( + long deviceControllerPtr, + long deviceId, + String setupCode, + @Nullable byte[] csrNonce, + @Nullable NetworkCredentials networkCredentials); + private native void establishPaseConnection( long deviceControllerPtr, long deviceId, int connId, long setupPincode); diff --git a/src/platform/android/AndroidChipPlatform-JNI.cpp b/src/platform/android/AndroidChipPlatform-JNI.cpp index f86482994cb308..b8fc9409636011 100644 --- a/src/platform/android/AndroidChipPlatform-JNI.cpp +++ b/src/platform/android/AndroidChipPlatform-JNI.cpp @@ -37,6 +37,7 @@ #include "AndroidChipPlatform-JNI.h" #include "BLEManagerImpl.h" +#include "BleConnectCallback-JNI.h" #include "CommissionableDataProviderImpl.h" #include "DiagnosticDataProviderImpl.h" #include "DnssdImpl.h" @@ -84,6 +85,9 @@ CHIP_ERROR AndroidChipPlatformJNI_OnLoad(JavaVM * jvm, void * reserved) SuccessOrExit(err); ChipLogProgress(DeviceLayer, "Java class references loaded."); + err = BleConnectCallbackJNI_OnLoad(jvm, reserved); + SuccessOrExit(err); + chip::InitializeTracing(); exit: @@ -99,6 +103,7 @@ CHIP_ERROR AndroidChipPlatformJNI_OnLoad(JavaVM * jvm, void * reserved) void AndroidChipPlatformJNI_OnUnload(JavaVM * jvm, void * reserved) { ChipLogProgress(DeviceLayer, "AndroidChipPlatform JNI_OnUnload() called"); + BleConnectCallbackJNI_OnUnload(jvm, reserved); chip::Platform::MemoryShutdown(); } diff --git a/src/platform/android/BLEManagerImpl.cpp b/src/platform/android/BLEManagerImpl.cpp index f2068dc5da7eb3..e811230c8638a6 100644 --- a/src/platform/android/BLEManagerImpl.cpp +++ b/src/platform/android/BLEManagerImpl.cpp @@ -125,7 +125,7 @@ void BLEManagerImpl::InitializeWithObject(jobject manager) env->ExceptionClear(); } - mOnNewConnectionMethod = env->GetMethodID(BLEManagerClass, "onNewConnection", "(I)V"); + mOnNewConnectionMethod = env->GetMethodID(BLEManagerClass, "onNewConnection", "(IZJJ)V"); if (mOnNewConnectionMethod == nullptr) { ChipLogError(DeviceLayer, "Failed to access BLEManager 'onNewConnection' method"); @@ -186,16 +186,19 @@ bool BLEManagerImpl::_IsAdvertising() CHIP_ERROR BLEManagerImpl::_SetAdvertisingMode(BLEAdvertisingMode mode) { + ChipLogDetail(DeviceLayer, "%s, %u", __FUNCTION__, static_cast(mode)); return CHIP_ERROR_NOT_IMPLEMENTED; } CHIP_ERROR BLEManagerImpl::_GetDeviceName(char * buf, size_t bufSize) { + ChipLogDetail(DeviceLayer, "%s, %s", __FUNCTION__, buf); return CHIP_ERROR_NOT_IMPLEMENTED; } CHIP_ERROR BLEManagerImpl::_SetDeviceName(const char * deviceName) { + ChipLogDetail(DeviceLayer, "%s, %s", __FUNCTION__, deviceName); return CHIP_ERROR_NOT_IMPLEMENTED; } @@ -449,6 +452,18 @@ void BLEManagerImpl::NotifyChipConnectionClosed(BLE_CONNECTION_OBJECT conId) // ===== start implement virtual methods on BleConnectionDelegate. +void BLEManagerImpl::OnConnectSuccess(void * appState, BLE_CONNECTION_OBJECT connObj) +{ + chip::DeviceLayer::StackLock lock; + BleConnectionDelegate::OnConnectionComplete(appState, connObj); +} + +void BLEManagerImpl::OnConnectFailed(void * appState, CHIP_ERROR err) +{ + chip::DeviceLayer::StackLock lock; + BleConnectionDelegate::OnConnectionError(appState, err); +} + void BLEManagerImpl::NewConnection(BleLayer * bleLayer, void * appState, const SetupDiscriminator & connDiscriminator) { chip::DeviceLayer::StackUnlock unlock; @@ -474,7 +489,10 @@ void BLEManagerImpl::NewConnection(BleLayer * bleLayer, void * appState, const S { discriminator = connDiscriminator.GetLongValue(); } - env->CallVoidMethod(mBLEManagerObject, mOnNewConnectionMethod, static_cast(discriminator)); + + env->CallVoidMethod(mBLEManagerObject, mOnNewConnectionMethod, static_cast(discriminator), + static_cast(connDiscriminator.IsShortDiscriminator()), reinterpret_cast(this), + reinterpret_cast(appState)); VerifyOrExit(!env->ExceptionCheck(), err = CHIP_JNI_ERROR_EXCEPTION_THROWN); exit: diff --git a/src/platform/android/BLEManagerImpl.h b/src/platform/android/BLEManagerImpl.h index 4d6a6bf94613e9..57fe6e9a0ca217 100644 --- a/src/platform/android/BLEManagerImpl.h +++ b/src/platform/android/BLEManagerImpl.h @@ -52,6 +52,9 @@ class BLEManagerImpl final : public BLEManager, void InitializeWithObject(jobject managerObject); + void OnConnectSuccess(void * appState, BLE_CONNECTION_OBJECT connObj); + void OnConnectFailed(void * appState, CHIP_ERROR err); + private: // ===== Members that implement the BLEManager internal interface. diff --git a/src/platform/android/BUILD.gn b/src/platform/android/BUILD.gn index 8c49d86050aa87..05ab96b3e5258f 100644 --- a/src/platform/android/BUILD.gn +++ b/src/platform/android/BUILD.gn @@ -34,6 +34,7 @@ static_library("android") { "AndroidConfig.h", "BLEManagerImpl.cpp", "BLEManagerImpl.h", + "BleConnectCallback-JNI.cpp", "BlePlatformConfig.h", "CHIPDevicePlatformEvent.h", "CHIPP256KeypairBridge.cpp", @@ -81,6 +82,7 @@ android_library("java") { "java/chip/platform/AndroidChipPlatform.java", "java/chip/platform/AndroidChipPlatformException.java", "java/chip/platform/BleCallback.java", + "java/chip/platform/BleConnectCallback.java", "java/chip/platform/BleManager.java", "java/chip/platform/ChipMdnsCallback.java", "java/chip/platform/ChipMdnsCallbackImpl.java", diff --git a/src/platform/android/BleConnectCallback-JNI.cpp b/src/platform/android/BleConnectCallback-JNI.cpp new file mode 100644 index 00000000000000..fac0e255075b97 --- /dev/null +++ b/src/platform/android/BleConnectCallback-JNI.cpp @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2023 Project CHIP Authors + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +#include +#include +#include + +#include "BLEManagerImpl.h" + +using namespace chip; +using namespace chip::DeviceLayer::Internal; + +#define JNI_METHOD(RETURN, METHOD_NAME) extern "C" JNIEXPORT RETURN JNICALL Java_chip_platform_BleConnectCallback_##METHOD_NAME + +CHIP_ERROR BleConnectCallbackJNI_OnLoad(JavaVM * jvm, void * reserved) +{ + ChipLogProgress(DeviceLayer, "BleConnectCallbackJNI_OnLoad"); + return CHIP_NO_ERROR; +} + +void BleConnectCallbackJNI_OnUnload(JavaVM * jvm, void * reserved) {} + +JNI_METHOD(void, onConnectSuccess)(JNIEnv * env, jobject self, jlong managerImplPtr, jlong appStatePtr, jint connId) +{ +#if CHIP_DEVICE_CONFIG_ENABLE_CHIPOBLE + BLEManagerImpl * impl = reinterpret_cast(managerImplPtr); + void * appState = reinterpret_cast(appStatePtr); + impl->OnConnectSuccess(appState, reinterpret_cast(connId)); +#endif +} + +JNI_METHOD(void, onConnectFailed)(JNIEnv * env, jobject self, jlong managerImplPtr, jlong appStatePtr) +{ +#if CHIP_DEVICE_CONFIG_ENABLE_CHIPOBLE + BLEManagerImpl * impl = reinterpret_cast(managerImplPtr); + void * appState = reinterpret_cast(appStatePtr); + impl->OnConnectFailed(appState, BLE_ERROR_NO_CONNECTION_RECEIVED_CALLBACK); +#endif +} diff --git a/src/platform/android/BleConnectCallback-JNI.h b/src/platform/android/BleConnectCallback-JNI.h new file mode 100644 index 00000000000000..3d5c558a3257b4 --- /dev/null +++ b/src/platform/android/BleConnectCallback-JNI.h @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2020-2021 Project CHIP Authors + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +/** + * @file + * Implementation of JNI bridge for CHIP Device Controller for Android apps + * + */ + +#pragma once + +CHIP_ERROR BleConnectCallbackJNI_OnLoad(JavaVM * jvm, void * reserved); + +void BleConnectCallbackJNI_OnUnload(JavaVM * jvm, void * reserved); diff --git a/src/platform/android/java/chip/platform/AndroidBleManager.java b/src/platform/android/java/chip/platform/AndroidBleManager.java index 3bc484985d231d..1d440b67b18da3 100644 --- a/src/platform/android/java/chip/platform/AndroidBleManager.java +++ b/src/platform/android/java/chip/platform/AndroidBleManager.java @@ -17,15 +17,30 @@ */ package chip.platform; +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothGatt; import android.bluetooth.BluetoothGattCallback; import android.bluetooth.BluetoothGattCharacteristic; import android.bluetooth.BluetoothGattDescriptor; import android.bluetooth.BluetoothGattService; +import android.bluetooth.BluetoothManager; import android.bluetooth.BluetoothProfile; +import android.bluetooth.le.BluetoothLeScanner; +import android.bluetooth.le.ScanCallback; +import android.bluetooth.le.ScanFilter; +import android.bluetooth.le.ScanResult; +import android.bluetooth.le.ScanSettings; +import android.content.Context; import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.os.ParcelUuid; import android.util.Log; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.UUID; @@ -63,115 +78,143 @@ private static class BleMtuDenylist { private BluetoothGattCallback mGattCallback; private AndroidChipPlatform mPlatform; + private Context mContext; + private BluetoothAdapter mBluetoothAdapter; + + private BleConnectCallback mBleConnectCallback; + + private BleConnectionHandler mConnectionHandler; + private ScanCallback mScanCallback; + + private static final int MSG_BLE_SCAN = 0; + private static final int MSG_BLE_CONNECT = 1; + private static final int MSG_BLE_CONNECT_SUCCESS = 2; + private static final int MSG_BLE_FAIL = 99; + + private static final int BLE_TIMEOUT_MS = 10000; + private static final int BLUETOOTH_ENABLE_TIMEOUT_MS = 1000; + + private static final String MSG_BUNDLE_SERVICE_DATA = "serviceData"; + private static final String MSG_BUNDLE_SERVICE_DATA_MASK = "serviceDataMask"; + + public AndroidBleManager(Context context) { + this(); + mContext = context; + @SuppressWarnings("unchecked") + BluetoothManager manager = + (BluetoothManager) context.getSystemService(Context.BLUETOOTH_SERVICE); + mBluetoothAdapter = manager.getAdapter(); + mConnectionHandler = new BleConnectionHandler(Looper.getMainLooper()); + } + public AndroidBleManager() { mConnections = new ArrayList<>(INITIAL_CONNECTIONS); - mGattCallback = - new BluetoothGattCallback() { - @Override - public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) { - int connId = 0; - - if (newState == BluetoothProfile.STATE_DISCONNECTED) { - connId = getConnId(gatt); - if (connId > 0) { - Log.d(TAG, "onConnectionStateChange Disconnected"); - mPlatform.handleConnectionError(connId); - } else { - Log.e(TAG, "onConnectionStateChange disconnected: no active connection"); - } - } - } + mGattCallback = new AndroidBluetoothGattCallback(); + } - @Override - public void onServicesDiscovered(BluetoothGatt gatt, int status) {} + class AndroidBluetoothGattCallback extends BluetoothGattCallback { + @Override + public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) { + int connId = 0; + + if (newState == BluetoothProfile.STATE_DISCONNECTED) { + connId = getConnId(gatt); + if (connId > 0) { + Log.d(TAG, "onConnectionStateChange Disconnected"); + mPlatform.handleConnectionError(connId); + } else { + Log.e(TAG, "onConnectionStateChange disconnected: no active connection"); + } + } + } - @Override - public void onCharacteristicRead( - BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {} + @Override + public void onServicesDiscovered(BluetoothGatt gatt, int status) {} + + @Override + public void onCharacteristicRead( + BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {} + + @Override + public void onCharacteristicWrite( + BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { + byte[] svcIdBytes = convertUUIDToBytes(characteristic.getService().getUuid()); + byte[] charIdBytes = convertUUIDToBytes(characteristic.getUuid()); + + if (status != BluetoothGatt.GATT_SUCCESS) { + Log.e( + TAG, + "onCharacteristicWrite for " + + characteristic.getUuid().toString() + + " failed with status: " + + status); + return; + } - @Override - public void onCharacteristicWrite( - BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { - byte[] svcIdBytes = convertUUIDToBytes(characteristic.getService().getUuid()); - byte[] charIdBytes = convertUUIDToBytes(characteristic.getUuid()); - - if (status != BluetoothGatt.GATT_SUCCESS) { - Log.e( - TAG, - "onCharacteristicWrite for " - + characteristic.getUuid().toString() - + " failed with status: " - + status); - return; - } - - int connId = getConnId(gatt); - if (connId > 0) { - mPlatform.handleWriteConfirmation( - connId, svcIdBytes, charIdBytes, status == BluetoothGatt.GATT_SUCCESS); - } else { - Log.e(TAG, "onCharacteristicWrite no active connection"); - return; - } - } + int connId = getConnId(gatt); + if (connId > 0) { + mPlatform.handleWriteConfirmation( + connId, svcIdBytes, charIdBytes, status == BluetoothGatt.GATT_SUCCESS); + } else { + Log.e(TAG, "onCharacteristicWrite no active connection"); + return; + } + } - @Override - public void onCharacteristicChanged( - BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { - byte[] svcIdBytes = convertUUIDToBytes(characteristic.getService().getUuid()); - byte[] charIdBytes = convertUUIDToBytes(characteristic.getUuid()); - int connId = getConnId(gatt); - if (connId > 0) { - mPlatform.handleIndicationReceived( - connId, svcIdBytes, charIdBytes, characteristic.getValue()); - } else { - Log.e(TAG, "onCharacteristicChanged no active connection"); - return; - } - } + @Override + public void onCharacteristicChanged( + BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { + byte[] svcIdBytes = convertUUIDToBytes(characteristic.getService().getUuid()); + byte[] charIdBytes = convertUUIDToBytes(characteristic.getUuid()); + int connId = getConnId(gatt); + if (connId > 0) { + mPlatform.handleIndicationReceived( + connId, svcIdBytes, charIdBytes, characteristic.getValue()); + } else { + Log.e(TAG, "onCharacteristicChanged no active connection"); + return; + } + } - @Override - public void onDescriptorWrite( - BluetoothGatt gatt, BluetoothGattDescriptor desc, int status) { - BluetoothGattCharacteristic characteristic = desc.getCharacteristic(); - - byte[] svcIdBytes = convertUUIDToBytes(characteristic.getService().getUuid()); - byte[] charIdBytes = convertUUIDToBytes(characteristic.getUuid()); - - if (status != BluetoothGatt.GATT_SUCCESS) { - Log.e( - TAG, - "onDescriptorWrite for " - + desc.getUuid().toString() - + " failed with status: " - + status); - } - - int connId = getConnId(gatt); - if (connId == 0) { - Log.e(TAG, "onDescriptorWrite no active connection"); - return; - } - - if (desc.getValue() == BluetoothGattDescriptor.ENABLE_INDICATION_VALUE) { - mPlatform.handleSubscribeComplete( - connId, svcIdBytes, charIdBytes, status == BluetoothGatt.GATT_SUCCESS); - } else if (desc.getValue() == BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE) { - mPlatform.handleSubscribeComplete( - connId, svcIdBytes, charIdBytes, status == BluetoothGatt.GATT_SUCCESS); - } else if (desc.getValue() == BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE) { - mPlatform.handleUnsubscribeComplete( - connId, svcIdBytes, charIdBytes, status == BluetoothGatt.GATT_SUCCESS); - } else { - Log.d(TAG, "Unexpected onDescriptorWrite()."); - } - } + @Override + public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor desc, int status) { + BluetoothGattCharacteristic characteristic = desc.getCharacteristic(); - @Override - public void onDescriptorRead( - BluetoothGatt gatt, BluetoothGattDescriptor desc, int status) {} - }; + byte[] svcIdBytes = convertUUIDToBytes(characteristic.getService().getUuid()); + byte[] charIdBytes = convertUUIDToBytes(characteristic.getUuid()); + + if (status != BluetoothGatt.GATT_SUCCESS) { + Log.e( + TAG, + "onDescriptorWrite for " + + desc.getUuid().toString() + + " failed with status: " + + status); + } + + int connId = getConnId(gatt); + if (connId == 0) { + Log.e(TAG, "onDescriptorWrite no active connection"); + return; + } + + if (desc.getValue() == BluetoothGattDescriptor.ENABLE_INDICATION_VALUE) { + mPlatform.handleSubscribeComplete( + connId, svcIdBytes, charIdBytes, status == BluetoothGatt.GATT_SUCCESS); + } else if (desc.getValue() == BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE) { + mPlatform.handleSubscribeComplete( + connId, svcIdBytes, charIdBytes, status == BluetoothGatt.GATT_SUCCESS); + } else if (desc.getValue() == BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE) { + mPlatform.handleUnsubscribeComplete( + connId, svcIdBytes, charIdBytes, status == BluetoothGatt.GATT_SUCCESS); + } else { + Log.d(TAG, "Unexpected onDescriptorWrite()."); + } + } + + @Override + public void onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor desc, int status) {} } @Override @@ -417,14 +460,229 @@ public void onNotifyChipConnectionClosed(int connId) { } @Override - public void onNewConnection(int discriminator) { + public void onNewConnection( + int discriminator, boolean isShortDiscriminator, long implPtr, long appStatePtr) { + Log.d(TAG, "onNewConnection : " + discriminator + ", " + isShortDiscriminator); + if (mContext == null) { + return; + } + mBleConnectCallback = new BleConnectCallback(implPtr, appStatePtr); + mConnectionHandler.sendEmptyMessageDelayed(MSG_BLE_FAIL, BLE_TIMEOUT_MS); + Message msg = mConnectionHandler.obtainMessage(); + msg.what = MSG_BLE_SCAN; + Bundle bundle = new Bundle(); + byte[] serviceData = getServiceData(discriminator); + byte[] serviceDataMask = getServiceDataMask(isShortDiscriminator); + bundle.putByteArray(MSG_BUNDLE_SERVICE_DATA, serviceData); + bundle.putByteArray(MSG_BUNDLE_SERVICE_DATA_MASK, serviceDataMask); + msg.setData(bundle); + if (!mBluetoothAdapter.isEnabled()) { + mBluetoothAdapter.enable(); + // TODO: Check Bluetooth enable intent + mConnectionHandler.sendMessageDelayed(msg, BLUETOOTH_ENABLE_TIMEOUT_MS); + return; + } + mConnectionHandler.sendMessage(msg); return; } + class BleConnectionHandler extends Handler { + public BleConnectionHandler(Looper looper) { + super(looper); + } + + @Override + public void handleMessage(Message msg) { + super.handleMessage(msg); + + switch (msg.what) { + case MSG_BLE_SCAN: + startBleScan(msg.getData()); + break; + case MSG_BLE_CONNECT: + stopBleScan(); + connectBLE(msg.obj); + break; + case MSG_BLE_CONNECT_SUCCESS: + bleConnectSuccess(msg.obj); + break; + case MSG_BLE_FAIL: + default: + stopBleScan(); + bleConnectFail(); + break; + } + } + } + + private void startBleScan(Bundle bundle) { + BluetoothLeScanner scanner = mBluetoothAdapter.getBluetoothLeScanner(); + if (scanner == null) { + Log.d(TAG, "No bluetooth scanner found"); + return; + } + + mScanCallback = + new ScanCallback() { + @Override + public void onScanResult(int callbackType, ScanResult result) { + BluetoothDevice device = result.getDevice(); + Log.i( + TAG, + "Bluetooth Device Scanned Addr: " + device.getAddress() + ", " + device.getName()); + Message msg = mConnectionHandler.obtainMessage(); + msg.what = MSG_BLE_CONNECT; + msg.obj = (Object) device; + mConnectionHandler.sendMessage(msg); + } + + @Override + public void onScanFailed(int errorCode) { + Log.e(TAG, "Scan failed " + errorCode); + } + }; + + byte[] serviceData = bundle.getByteArray(MSG_BUNDLE_SERVICE_DATA); + byte[] serviceDataMask = bundle.getByteArray(MSG_BUNDLE_SERVICE_DATA_MASK); + ScanFilter scanFilter = + new ScanFilter.Builder() + .setServiceData( + new ParcelUuid(UUID.fromString(CHIP_UUID)), serviceData, serviceDataMask) + .build(); + ScanSettings scanSettings = + new ScanSettings.Builder().setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY).build(); + Log.i(TAG, "Starting Bluetooth scan"); + scanner.startScan(Arrays.asList(scanFilter), scanSettings, mScanCallback); + } + + private void stopBleScan() { + if (mScanCallback != null) { + BluetoothLeScanner scanner = mBluetoothAdapter.getBluetoothLeScanner(); + if (scanner == null) { + Log.d(TAG, "No bluetooth scanner found"); + return; + } + scanner.stopScan(mScanCallback); + mScanCallback = null; + } + } + + private void connectBLE(Object bluetoothDeviceObj) { + if (bluetoothDeviceObj == null) { + return; + } + + // Fail Timer reset. + mConnectionHandler.removeMessages(MSG_BLE_FAIL); + mConnectionHandler.sendEmptyMessageDelayed(MSG_BLE_FAIL, BLE_TIMEOUT_MS); + + @SuppressWarnings("unchecked") + BluetoothDevice device = (BluetoothDevice) bluetoothDeviceObj; + + Log.i(TAG, "Connecting"); + BluetoothGatt gatt = device.connectGatt(mContext, false, new ConnectionGattCallback()); + } + + class ConnectionGattCallback extends AndroidBluetoothGattCallback { + @Override + public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) { + Log.i(TAG, "onConnectionStateChange status = " + status + ", newState= + " + newState); + super.onConnectionStateChange(gatt, status, newState); + if (newState == BluetoothProfile.STATE_CONNECTED && status == BluetoothGatt.GATT_SUCCESS) { + Log.i(TAG, "Discovering Services..."); + gatt.discoverServices(); + return; + } else if (newState == BluetoothProfile.STATE_DISCONNECTED) { + Log.i(TAG, "Services Disconnected"); + } + mConnectionHandler.sendEmptyMessage(MSG_BLE_FAIL); + } + + @Override + public void onServicesDiscovered(BluetoothGatt gatt, int status) { + Log.d(TAG, "onServicesDiscovered status = " + status); + super.onServicesDiscovered(gatt, status); + + Log.i(TAG, "Services Discovered"); + gatt.requestMtu(247); + } + + @Override + public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) { + super.onMtuChanged(gatt, mtu, status); + String deviceName = ""; + if (gatt != null && gatt.getDevice() != null) { + deviceName = gatt.getDevice().getName(); + } + Log.d(TAG, deviceName + ".onMtuChanged: connecting to CHIP device : " + status); + + Message msg = mConnectionHandler.obtainMessage(); + msg.what = MSG_BLE_CONNECT_SUCCESS; + msg.obj = (Object) gatt; + + mConnectionHandler.sendMessage(msg); + } + } + + private void bleConnectSuccess(Object gattObj) { + Log.d(TAG, "bleConnectSuccess"); + mConnectionHandler.removeMessages(MSG_BLE_FAIL); + @SuppressWarnings("unchecked") + BluetoothGatt gatt = (BluetoothGatt) gattObj; + int connId = addConnection(gatt); + + setBleCallback( + new BleCallback() { + @Override + public void onCloseBleComplete(int connId) { + Log.d(TAG, "onCloseBleComplete : " + connId); + } + + @Override + public void onNotifyChipConnectionClosed(int connId) { + Log.d(TAG, "onNotifyChipConnectionClosed : " + connId); + } + }); + + if (mBleConnectCallback != null) { + mBleConnectCallback.onConnectSuccess(connId); + } else { + // Already fail returned + Log.d(TAG, "Already timeout"); + gatt.disconnect(); + removeConnection(connId); + } + mBleConnectCallback = null; + } + + private void bleConnectFail() { + Log.d(TAG, "bleConnectFail"); + if (mBleConnectCallback != null) { + mBleConnectCallback.onConnectFailed(); + } + mBleConnectCallback = null; + } + + private byte[] getServiceData(int discriminator) { + int opcode = 0; + int version = 0; + int versionDiscriminator = ((version & 0xf) << 12) | (discriminator & 0xfff); + return new byte[] { + (byte) (opcode & 0xFF), + (byte) (versionDiscriminator & 0xFF), + (byte) ((versionDiscriminator >> 8) & 0xFF) + }; + } + + private byte[] getServiceDataMask(boolean isShortDiscriminator) { + return new byte[] {(byte) 0xFF, (byte) (isShortDiscriminator ? 0x00 : 0xFF), (byte) 0xFF}; + } + // CLIENT_CHARACTERISTIC_CONFIG is the well-known UUID of the client characteristic descriptor // that has the flags for enabling and disabling notifications and indications. // c.f. https://www.bluetooth.org/en-us/specification/assigned-numbers/generic-attribute-profile private static String CLIENT_CHARACTERISTIC_CONFIG = "00002902-0000-1000-8000-00805f9b34fb"; + private static String CHIP_UUID = "0000FFF6-0000-1000-8000-00805F9B34FB"; private static byte[] convertUUIDToBytes(UUID uuid) { byte[] idBytes = new byte[16]; diff --git a/src/platform/android/java/chip/platform/BleConnectCallback.java b/src/platform/android/java/chip/platform/BleConnectCallback.java new file mode 100644 index 00000000000000..073a8b95594037 --- /dev/null +++ b/src/platform/android/java/chip/platform/BleConnectCallback.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2021 Project CHIP Authors + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package chip.platform; + +public class BleConnectCallback { + private long implPtr; + private long appStatePtr; + + public BleConnectCallback(long implPtr, long appStatePtr) { + this.implPtr = implPtr; + this.appStatePtr = appStatePtr; + } + + public void onConnectSuccess(int connectionId) { + onConnectSuccess(implPtr, appStatePtr, connectionId); + } + + public void onConnectFailed() { + onConnectFailed(implPtr, appStatePtr); + } + + private native void onConnectSuccess(long implPtr, long appStatePtr, int connectionId); + + private native void onConnectFailed(long implPtr, long appStatePtr); +} diff --git a/src/platform/android/java/chip/platform/BleManager.java b/src/platform/android/java/chip/platform/BleManager.java index 687bd9e5d51fc2..34bcb662b139c5 100644 --- a/src/platform/android/java/chip/platform/BleManager.java +++ b/src/platform/android/java/chip/platform/BleManager.java @@ -56,5 +56,6 @@ public interface BleManager { void onNotifyChipConnectionClosed(int connId); // BleConnectionDelegate - void onNewConnection(int discriminator); + void onNewConnection( + int discriminator, boolean isShortDiscriminator, long implPtr, long appStatePtr); }