diff --git a/tools/test_suite/android/.gitignore b/tools/test_suite/android/.gitignore
new file mode 100755
index 0000000..c7fb9e3
--- /dev/null
+++ b/tools/test_suite/android/.gitignore
@@ -0,0 +1,16 @@
+*.iml
+.gradle
+/local.properties
+/.idea/
+/.idea/caches
+/.idea/libraries
+/.idea/modules.xml
+/.idea/workspace.xml
+/.idea/navEditor.xml
+/.idea/assetWizardSettings.xml
+.DS_Store
+/build
+/captures
+.externalNativeBuild
+.cxx
+local.properties
diff --git a/tools/test_suite/android/app/.gitignore b/tools/test_suite/android/app/.gitignore
new file mode 100755
index 0000000..796b96d
--- /dev/null
+++ b/tools/test_suite/android/app/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/tools/test_suite/android/app/build.gradle b/tools/test_suite/android/app/build.gradle
new file mode 100755
index 0000000..b84f8dd
--- /dev/null
+++ b/tools/test_suite/android/app/build.gradle
@@ -0,0 +1,52 @@
+apply plugin: 'com.android.application'
+
+android {
+ namespace 'com.openvela.bluetoothtest'
+ compileSdk 34
+
+ defaultConfig {
+ applicationId "com.openvela.bluetoothtest"
+ minSdk 29
+ targetSdk 34
+ versionName "1.0"
+
+ testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
+ vectorDrawables.useSupportLibrary = true
+ multiDexEnabled true
+ }
+
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ debug {
+ jniDebuggable true
+ }
+ }
+
+ android.applicationVariants.all { variant ->
+ variant.outputs.all {
+ outputFileName = "BluetoothTest_${buildType.name}_v${defaultConfig.versionName}.apk"
+ }
+ }
+}
+
+dependencies {
+ implementation fileTree(include: ['*.jar'], dir: 'libs')
+ testImplementation 'junit:junit:4.13.2'
+ androidTestImplementation 'androidx.test.ext:junit:1.2.1'
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1'
+
+ implementation project(':core')
+ implementation 'androidx.appcompat:appcompat:1.6.1'
+ implementation 'com.google.android.material:material:1.12.0'
+ implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
+
+}
+
diff --git a/tools/test_suite/android/app/proguard-rules.pro b/tools/test_suite/android/app/proguard-rules.pro
new file mode 100755
index 0000000..481bb43
--- /dev/null
+++ b/tools/test_suite/android/app/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/tools/test_suite/android/app/src/androidTest/java/com/xiaomi/velabluetooth/ExampleInstrumentedTest.java b/tools/test_suite/android/app/src/androidTest/java/com/xiaomi/velabluetooth/ExampleInstrumentedTest.java
new file mode 100755
index 0000000..d78645e
--- /dev/null
+++ b/tools/test_suite/android/app/src/androidTest/java/com/xiaomi/velabluetooth/ExampleInstrumentedTest.java
@@ -0,0 +1,25 @@
+package com.openvela.bluetoothtest;
+
+import android.content.Context;
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.*;
+
+/**
+ * Instrumentation test, which will execute on an Android device.
+ *
+ * @see Testing documentation
+ */
+@RunWith(AndroidJUnit4.class)
+public class ExampleInstrumentedTest {
+ @Test
+ public void useAppContext() throws Exception {
+ // Context of the app under test.
+ Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+ assertEquals("com.openvela.bluetoothtest", appContext.getPackageName());
+ }
+}
diff --git a/tools/test_suite/android/app/src/main/AndroidManifest.xml b/tools/test_suite/android/app/src/main/AndroidManifest.xml
new file mode 100755
index 0000000..cdef959
--- /dev/null
+++ b/tools/test_suite/android/app/src/main/AndroidManifest.xml
@@ -0,0 +1,52 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tools/test_suite/android/app/src/main/ic_launcher-velabluetooth.png b/tools/test_suite/android/app/src/main/ic_launcher-velabluetooth.png
new file mode 100755
index 0000000..6453047
Binary files /dev/null and b/tools/test_suite/android/app/src/main/ic_launcher-velabluetooth.png differ
diff --git a/tools/test_suite/android/app/src/main/java/com/openvela/bluetoothtest/MainActivity.java b/tools/test_suite/android/app/src/main/java/com/openvela/bluetoothtest/MainActivity.java
new file mode 100755
index 0000000..16dba9a
--- /dev/null
+++ b/tools/test_suite/android/app/src/main/java/com/openvela/bluetoothtest/MainActivity.java
@@ -0,0 +1,127 @@
+/****************************************************************************
+ * Copyright (C) 2024 Xiaomi Corporation
+ *
+ * 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 com.openvela.bluetoothtest;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import android.Manifest;
+import android.content.Intent;
+import android.os.Build;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.View;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import android.bluetooth.BluetoothAdapter;
+
+import androidx.activity.result.contract.ActivityResultContracts;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.appcompat.app.AppCompatActivity;
+
+import com.openvela.bluetooth.BluetoothStateObserver;
+import com.openvela.bluetooth.callback.BluetoothStateCallback;
+import com.openvela.bluetoothtest.ble.BleScanActivity;
+import com.openvela.bluetoothtest.ble.BlePeripheralActivity;
+import com.openvela.bluetoothtest.bredr.BredrInquiryActivity;
+
+public class MainActivity extends AppCompatActivity {
+ private final String TAG = MainActivity.class.getSimpleName();
+ private final int REQUEST_ENABLE_BT = 1;
+
+ private LinearLayout llBluetoothAdapterTip;
+ private BluetoothStateObserver btStateObserver;
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_main);
+ initView();
+ requestBluetoothPermission();
+ listenBluetoothState();
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ btStateObserver.unregisterReceiver();
+ }
+
+ private void initView() {
+ llBluetoothAdapterTip = findViewById(R.id.ll_adapter_tip);
+ TextView tvAdapterStates = findViewById(R.id.tv_adapter_states);
+
+ tvAdapterStates.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ startActivityForResult(new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE), REQUEST_ENABLE_BT);
+ }
+ });
+ }
+
+ @RequiresApi(api = Build.VERSION_CODES.S)
+ private void requestBluetoothPermission() {
+ List permissions = new ArrayList<>();
+ permissions.add(Manifest.permission.BLUETOOTH_SCAN);
+ permissions.add(Manifest.permission.BLUETOOTH_ADVERTISE);
+ permissions.add(Manifest.permission.BLUETOOTH_CONNECT);
+ permissions.add(Manifest.permission.ACCESS_COARSE_LOCATION);
+ permissions.add(Manifest.permission.ACCESS_FINE_LOCATION);
+
+ registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(), map -> {
+ if (!isBluetoothEnabled()) {
+ llBluetoothAdapterTip.setVisibility(View.VISIBLE);
+ }
+ }).launch(permissions.toArray(new String[0]));
+ }
+
+ private void listenBluetoothState() {
+ btStateObserver = new BluetoothStateObserver(this);
+ btStateObserver.registerReceiver(new BluetoothStateCallback() {
+ @Override
+ public void onEnabled() {
+ Log.i(TAG, "BluetoothAdapter is enabled!");
+ llBluetoothAdapterTip.setVisibility(View.GONE);
+ }
+
+ @Override
+ public void onDisabled() {
+ Log.i(TAG, "BluetoothAdapter is disabled!");
+ llBluetoothAdapterTip.setVisibility(View.VISIBLE);
+ }
+ });
+ }
+
+ private boolean isBluetoothEnabled() {
+ BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
+ return bluetoothAdapter != null && bluetoothAdapter.isEnabled();
+ }
+
+ public void entryBredrInquiryActivity(View view) {
+ startActivity(new Intent(this, BredrInquiryActivity.class));
+ }
+
+ public void entryBleCentralActivity(View view) {
+ startActivity(new Intent(this, BleScanActivity.class));
+ }
+
+ public void entryBlePeripheralActivity(View view) {
+ startActivity(new Intent(this, BlePeripheralActivity.class));
+ }
+}
diff --git a/tools/test_suite/android/app/src/main/java/com/openvela/bluetoothtest/ble/BleCentralActivity.java b/tools/test_suite/android/app/src/main/java/com/openvela/bluetoothtest/ble/BleCentralActivity.java
new file mode 100755
index 0000000..fb073f7
--- /dev/null
+++ b/tools/test_suite/android/app/src/main/java/com/openvela/bluetoothtest/ble/BleCentralActivity.java
@@ -0,0 +1,132 @@
+/****************************************************************************
+ * Copyright (C) 2024 Xiaomi Corporation
+ *
+ * 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 com.openvela.bluetoothtest.ble;
+
+import android.os.Bundle;
+import android.widget.Button;
+import android.widget.TextView;
+
+import android.bluetooth.BluetoothProfile;
+
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.recyclerview.widget.DividerItemDecoration;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.openvela.bluetooth.BtDevice;
+import com.openvela.bluetooth.callback.BleConnectCallback;
+import com.openvela.bluetoothtest.R;
+
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+public class BleCentralActivity extends AppCompatActivity {
+ private final String TAG = BleCentralActivity.class.getSimpleName();
+ public static final String EXTRA_TAG = "device";
+ private TextView tvConnectState;
+ private Button btnConnect;
+ private @NotNull BtDevice currentDevice;
+ private GattClientAdapter gattClientAdapter;
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_ble_central);
+ gattClientAdapter = new GattClientAdapter(this);
+ initView();
+
+ currentDevice = getIntent().getParcelableExtra(EXTRA_TAG);
+ getSupportActionBar().setSubtitle(currentDevice.getAddress());
+ gattClientAdapter.connect(currentDevice, connectCallback);
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ if (currentDevice.getConnectionState() != BluetoothProfile.STATE_CONNECTING) {
+ gattClientAdapter.cancelConnect(currentDevice);
+ } else if (currentDevice.getConnectionState() != BluetoothProfile.STATE_CONNECTED){
+ gattClientAdapter.disconnect(currentDevice);
+ }
+ }
+
+ private void initView() {
+ tvConnectState = findViewById(R.id.tv_connect_state);
+ btnConnect = findViewById(R.id.btn_connect);
+ RecyclerView recyclerView = findViewById(R.id.recyclerView);
+
+ btnConnect.setOnClickListener(v -> {
+ if (currentDevice.getConnectionState() == BluetoothProfile.STATE_DISCONNECTED) {
+ gattClientAdapter.connect(currentDevice, connectCallback);
+ } else if (currentDevice.getConnectionState() == BluetoothProfile.STATE_CONNECTING) {
+ gattClientAdapter.cancelConnect(currentDevice);
+ } else {
+ gattClientAdapter.disconnect(currentDevice);
+ }
+ });
+
+ recyclerView.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false));
+ recyclerView.addItemDecoration(new DividerItemDecoration(this,DividerItemDecoration.VERTICAL));
+ recyclerView.getItemAnimator().setChangeDuration(300);
+ recyclerView.getItemAnimator().setMoveDuration(300);
+ recyclerView.setAdapter(gattClientAdapter);
+ }
+
+ private final BleConnectCallback connectCallback = new BleConnectCallback() {
+ @Override
+ public void onConnectionChanged(String address, int newState) {
+ if (!address.equals(currentDevice.getAddress())) {
+ return;
+ }
+ currentDevice.setConnectionState(newState);
+ if (newState == BluetoothProfile.STATE_CONNECTED) {
+ tvConnectState.setText("Connected");
+ btnConnect.setText("DISCONNECT");
+ } else if (newState == BluetoothProfile.STATE_DISCONNECTED){
+ tvConnectState.setText("Disconnected");
+ btnConnect.setText("CONNECT");
+ } else if (newState == BluetoothProfile.STATE_CONNECTING) {
+ tvConnectState.setText("Connecting...");
+ btnConnect.setText("DISCONNECT");
+ } else if (newState == BluetoothProfile.STATE_DISCONNECTING){
+ tvConnectState.setText("Disconnecting...");
+ btnConnect.setText("DISCONNECT");
+ }
+ }
+
+ @Override
+ public void onConnectFailed(String address, int errorCode) {
+ super.onConnectFailed(address, errorCode);
+ if (errorCode == BleConnectCallback.FAILED_DEVICE_NOT_FOUND) {
+ tvConnectState.setText("Connect Failed:" + "device not found");
+ } else if (errorCode == BleConnectCallback.FAILED_TIMEOUT) {
+ tvConnectState.setText("Connect Failed:" + "timeout");
+ } else {
+ tvConnectState.setText("Connect Failed:" + errorCode);
+ }
+ currentDevice.setConnectionState(BluetoothProfile.STATE_DISCONNECTED);
+ btnConnect.setText("CONNECT");
+ }
+
+ @Override
+ public void onServicesDiscovered(String address) {
+ super.onServicesDiscovered(address);
+ tvConnectState.setText("Discovered");
+ }
+ };
+
+}
diff --git a/tools/test_suite/android/app/src/main/java/com/openvela/bluetoothtest/ble/BlePeripheralActivity.java b/tools/test_suite/android/app/src/main/java/com/openvela/bluetoothtest/ble/BlePeripheralActivity.java
new file mode 100755
index 0000000..18ca4ca
--- /dev/null
+++ b/tools/test_suite/android/app/src/main/java/com/openvela/bluetoothtest/ble/BlePeripheralActivity.java
@@ -0,0 +1,112 @@
+/****************************************************************************
+ * Copyright (C) 2024 Xiaomi Corporation
+ *
+ * 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 com.openvela.bluetoothtest.ble;
+
+import android.os.Bundle;
+import android.util.Log;
+import android.widget.EditText;
+import android.widget.TextView;
+import android.widget.Button;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.le.AdvertiseCallback;
+import android.bluetooth.le.AdvertiseData;
+import android.bluetooth.le.AdvertiseSettings;
+import android.bluetooth.le.BluetoothLeAdvertiser;
+
+import androidx.appcompat.app.AppCompatActivity;
+
+import com.openvela.bluetoothtest.R;
+
+public class BlePeripheralActivity extends AppCompatActivity {
+ private final String TAG = BlePeripheralActivity.class.getSimpleName();
+ private final BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
+ private volatile BluetoothLeAdvertiser bluetoothAdvertiser;
+ private TextView tvAdvState;
+ private Button btnAdv;
+ private EditText etAdvName;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_ble_peripheral);
+ initView();
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ if (isAdvertising()) {
+ stopAdvertising();
+ }
+ }
+
+ private void initView() {
+ tvAdvState = findViewById(R.id.tv_adv_state);
+ btnAdv = findViewById(R.id.btn_adv);
+ etAdvName = findViewById(R.id.et_adv_name);
+
+ btnAdv.setOnClickListener(v -> {
+ if (isAdvertising()) {
+ stopAdvertising();
+ tvAdvState.setText("Advertise Stopped");
+ btnAdv.setText("START ADVERTISE");
+ } else {
+ startAdvertising(etAdvName.getText().toString().getBytes());
+ }
+ });
+ }
+
+ private boolean isAdvertising() {
+ return (bluetoothAdvertiser != null);
+ }
+
+ private void startAdvertising(final byte[] payload) {
+ AdvertiseSettings advertiseSettings = new AdvertiseSettings.Builder()
+ .setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_LOW_LATENCY)
+ .setConnectable(true)
+ .setTimeout(0)
+ .setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_HIGH)
+ .build();
+ AdvertiseData advertiseData = new AdvertiseData.Builder()
+ .addManufacturerData(0xFF00, payload)
+ .setIncludeDeviceName(true)
+ .build();
+
+ bluetoothAdvertiser = bluetoothAdapter.getBluetoothLeAdvertiser();
+ bluetoothAdvertiser.startAdvertising(advertiseSettings, advertiseData, advertiseCallback);
+ }
+
+ public void stopAdvertising() {
+ bluetoothAdvertiser.stopAdvertising(advertiseCallback);
+ bluetoothAdvertiser = null;
+ }
+
+ private final AdvertiseCallback advertiseCallback = new AdvertiseCallback() {
+ @Override
+ public void onStartSuccess(AdvertiseSettings settingsInEffect) {
+ tvAdvState.setText("Advertising...");
+ btnAdv.setText("STOP ADVERTISE");
+ }
+
+ @Override
+ public void onStartFailure(int errorCode) {
+ tvAdvState.setText("Advertise Failed: " + errorCode);
+ Log.e(TAG, "onAdvStartFailure: " + errorCode);
+ }
+ };
+}
\ No newline at end of file
diff --git a/tools/test_suite/android/app/src/main/java/com/openvela/bluetoothtest/ble/BleScanActivity.java b/tools/test_suite/android/app/src/main/java/com/openvela/bluetoothtest/ble/BleScanActivity.java
new file mode 100755
index 0000000..30d82c6
--- /dev/null
+++ b/tools/test_suite/android/app/src/main/java/com/openvela/bluetoothtest/ble/BleScanActivity.java
@@ -0,0 +1,106 @@
+/****************************************************************************
+ * Copyright (C) 2024 Xiaomi Corporation
+ *
+ * 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 com.openvela.bluetoothtest.ble;
+
+import android.os.Bundle;
+import android.util.Log;
+import android.widget.EditText;
+import android.widget.TextView;
+import android.widget.Button;
+
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.recyclerview.widget.DividerItemDecoration;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.openvela.bluetooth.BtDevice;
+import com.openvela.bluetooth.callback.BluetoothDiscoveryCallback;
+import com.openvela.bluetoothtest.R;
+
+public class BleScanActivity extends AppCompatActivity {
+ private final String TAG = BleScanActivity.class.getSimpleName();
+ private static final long BLE_SCAN_PERIOD_MS = 12 * 1000;
+ private TextView tvScanState;
+ private Button btnScan;
+ private EditText etFilter;
+ private BleScanAdapter bleScanAdapter;
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_ble_scan);
+ bleScanAdapter = new BleScanAdapter(this);
+ initView();
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ if (bleScanAdapter.isScanning()) {
+ bleScanAdapter.stopScan();
+ }
+ }
+
+ private void initView() {
+ tvScanState = findViewById(R.id.tv_scan_state);
+ btnScan = findViewById(R.id.btn_scan);
+ etFilter = findViewById(R.id.et_filter);
+ RecyclerView recyclerView = findViewById(R.id.recyclerView);
+
+ btnScan.setOnClickListener(v -> {
+ if (bleScanAdapter.isScanning()) {
+ bleScanAdapter.stopScan();
+ } else {
+ bleScanAdapter.startScan(new String[]{etFilter.getText().toString()}, BLE_SCAN_PERIOD_MS, discoveryCallback);
+ }
+ });
+
+ recyclerView.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false));
+ recyclerView.addItemDecoration(new DividerItemDecoration(this, DividerItemDecoration.VERTICAL));
+ recyclerView.getItemAnimator().setChangeDuration(300);
+ recyclerView.getItemAnimator().setMoveDuration(300);
+ recyclerView.setAdapter(bleScanAdapter);
+ }
+
+ private final BluetoothDiscoveryCallback discoveryCallback = new BluetoothDiscoveryCallback() {
+ @Override
+ public void onDiscoveryResult(final BtDevice device) {}
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ tvScanState.setText("Scanning...");
+ btnScan.setText("STOP SCAN");
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+ tvScanState.setText("Scan Stopped");
+ btnScan.setText("START SCAN");
+ }
+
+ @Override
+ public void onDiscoveryFailed(int errorCode) {
+ super.onDiscoveryFailed(errorCode);
+ tvScanState.setText("Scan Failed: " + errorCode);
+ Log.e(TAG, "onDiscoveryFailed: " + errorCode);
+ }
+ };
+
+}
diff --git a/tools/test_suite/android/app/src/main/java/com/openvela/bluetoothtest/ble/BleScanAdapter.java b/tools/test_suite/android/app/src/main/java/com/openvela/bluetoothtest/ble/BleScanAdapter.java
new file mode 100755
index 0000000..0c7420d
--- /dev/null
+++ b/tools/test_suite/android/app/src/main/java/com/openvela/bluetoothtest/ble/BleScanAdapter.java
@@ -0,0 +1,264 @@
+/****************************************************************************
+ * Copyright (C) 2024 Xiaomi Corporation
+ *
+ * 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 com.openvela.bluetoothtest.ble;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.ParcelUuid;
+import android.text.TextUtils;
+import android.view.View;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.le.BluetoothLeScanner;
+import android.bluetooth.le.ScanCallback;
+import android.bluetooth.le.ScanRecord;
+import android.bluetooth.le.ScanResult;
+import android.bluetooth.le.ScanSettings;
+
+import androidx.core.os.HandlerCompat;
+
+import com.openvela.bluetooth.adapter.RecyclerAdapter;
+import com.openvela.bluetooth.adapter.RecyclerViewHolder;
+import com.openvela.bluetooth.BtDevice;
+import com.openvela.bluetooth.callback.BluetoothDiscoveryCallback;
+import com.openvela.bluetoothtest.R;
+
+public class BleScanAdapter extends RecyclerAdapter {
+ private final String TAG = BleScanAdapter.class.getSimpleName();
+ private static final String SCAN_TIMEOUT_TOKEN = "scan_timeout_token";
+ private static final int RSSI_UPDATE_INTERVAL_MS = 2 * 1000;
+ private final Handler handler = new Handler(Looper.myLooper());
+ private final BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
+ private volatile BluetoothLeScanner bluetoothScanner;
+ private ScanSettings scanSettings;
+ private String[] scanFilters;
+ private BluetoothDiscoveryCallback bleDiscoveryCallback;
+ private int view_position = -1;
+
+ public BleScanAdapter(Context context) {
+ super(context, R.layout.item_scan_result, new ArrayList<>());
+ }
+
+ @SuppressLint("DefaultLocale")
+ @Override
+ public void onBindViewHolderItem(RecyclerViewHolder viewHolder, BtDevice device) {
+ viewHolder.setText(R.id.tv_address, device.getAddress());
+ viewHolder.setText(R.id.tv_rssi, String.format("%ddBm", device.getRssi()));
+ if (device.getName() == null){
+ viewHolder.setText(R.id.tv_name, "Unknown");
+ } else {
+ viewHolder.setText(R.id.tv_name, device.getName());
+ }
+
+ if (viewHolder.getBindingAdapterPosition() == view_position){
+ viewHolder.setVisibility(R.id.ll_detail, View.VISIBLE);
+
+ ScanRecord scanRecord = device.getScanRecord();
+ if (scanRecord != null) {
+ // Flags
+ if (scanRecord.getAdvertiseFlags() >= 0) {
+ viewHolder.setVisibility(R.id.tv_flags, View.VISIBLE);
+ viewHolder.setText(R.id.tv_flags, "Flags: 0x" + String.format("%02x", scanRecord.getAdvertiseFlags()));
+ }
+ // Local Name
+ if (scanRecord.getDeviceName() != null) {
+ viewHolder.setVisibility(R.id.tv_local_name, View.VISIBLE);
+ viewHolder.setText(R.id.tv_local_name, "Local Name: "+ scanRecord.getDeviceName());
+ }
+ // Service UUIDs
+ List serviceUuids = scanRecord.getServiceUuids();
+ if (serviceUuids != null && !serviceUuids.isEmpty()){
+ viewHolder.setVisibility(R.id.tv_uuid, View.VISIBLE);
+ viewHolder.setText(R.id.tv_uuid, String.format("Service Uuids: %s", TextUtils.join(", ", serviceUuids)));
+ }
+ // Raw Data
+ byte[] rawData = scanRecord.getBytes();
+ int totalLength = 0;
+ do {
+ totalLength += rawData[totalLength] + 1;
+ } while (rawData[totalLength] != 0);
+
+ StringBuilder builder = new StringBuilder();
+ builder.append("RAW: 0x");
+ for (int i = 0; i < totalLength; i++) {
+ builder.append(String.format("%02x", rawData[i]));
+ }
+ viewHolder.setText(R.id.tv_raw_data, builder.toString());
+ }
+
+ } else {
+ viewHolder.setVisibility(R.id.ll_detail, View.GONE);
+ }
+
+ if (device.isConnectable()) {
+ viewHolder.setVisibility(R.id.tv_connect, View.VISIBLE);
+ viewHolder.setOnClickListener(R.id.tv_connect, v -> {
+ if (isScanning()) {
+ stopScan();
+ }
+ mContext.startActivity(new Intent(mContext, BleCentralActivity.class)
+ .putExtra(BleCentralActivity.EXTRA_TAG, device));
+ });
+ } else {
+ viewHolder.setVisibility(R.id.tv_connect, View.GONE);
+ }
+
+ viewHolder.setOnClickListener(v -> {
+ if(viewHolder.getBindingAdapterPosition() == view_position) {
+ notifyItemChanged(view_position);
+ view_position = -1;
+ } else {
+ notifyItemChanged(view_position);
+ view_position = viewHolder.getBindingAdapterPosition();
+ notifyItemChanged(view_position);
+ }
+ });
+ }
+
+ public boolean isScanning() {
+ return (bluetoothScanner != null);
+ }
+
+ @SuppressLint("NotifyDataSetChanged")
+ public void startScan(final String[] scanFilters, long scanPeriod, BluetoothDiscoveryCallback callback) {
+ bleDiscoveryCallback = callback;
+
+ if (!bluetoothAdapter.isEnabled()) {
+ if (bleDiscoveryCallback != null) {
+ bleDiscoveryCallback.onDiscoveryFailed(BluetoothDiscoveryCallback.FAILED_INTERNAL_ERROR);
+ return;
+ }
+ }
+
+ if (isScanning()) {
+ if (bleDiscoveryCallback != null){
+ bleDiscoveryCallback.onDiscoveryFailed(BluetoothDiscoveryCallback.FAILED_ALREADY_STARTED);
+ }
+ return;
+ }
+
+ bluetoothScanner = bluetoothAdapter.getBluetoothLeScanner();
+ if (bluetoothScanner != null) {
+ super.mItems.clear();
+ super.notifyDataSetChanged();
+ startScanTimer(scanPeriod);
+
+ this.scanFilters = scanFilters;
+ scanSettings = new ScanSettings.Builder()
+ .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
+ .setReportDelay(0L)
+ .build();
+ bluetoothScanner.startScan(null, scanSettings, scanCallback);
+ bleDiscoveryCallback.onStart();
+ } else {
+ bleDiscoveryCallback.onDiscoveryFailed(BluetoothDiscoveryCallback.FAILED_INTERNAL_ERROR);
+ }
+ }
+
+ public void stopScan() {
+ if (!isScanning()) {
+ return;
+ }
+
+ cancelScanTimer();
+ bluetoothScanner.stopScan(scanCallback);
+ bleDiscoveryCallback.onStop();
+ bluetoothScanner = null;
+ }
+
+ private void cancelScanTimer() {
+ handler.removeCallbacksAndMessages(SCAN_TIMEOUT_TOKEN);
+ }
+
+ private void startScanTimer(long scanPeriod) {
+ cancelScanTimer();
+
+ if (scanPeriod >= 0){
+ HandlerCompat.postDelayed(handler, () -> {
+ if (isScanning()) {
+ stopScan();
+ }
+ }, SCAN_TIMEOUT_TOKEN, scanPeriod);
+ }
+ }
+
+ private final ScanCallback scanCallback = new ScanCallback() {
+ @SuppressLint("NotifyDataSetChanged")
+ @Override
+ public void onScanResult(final int callbackType, final ScanResult result) {
+ synchronized (this) {
+ final String address = result.getDevice().getAddress();
+ final String name = result.getDevice().getName();
+ boolean found = true;
+
+ if (scanFilters != null) {
+ found = false;
+ for (String filter : scanFilters) {
+ if ((address != null && address.toLowerCase().contains(filter.toLowerCase())) ||
+ (name != null && name.toLowerCase().contains(filter.toLowerCase()))) {
+ found = true;
+ break;
+ }
+ }
+ }
+ if (!found) {
+ return;
+ }
+
+ for (int i = 0; i < getItemCount(); i++) {
+ BtDevice device = mItems.get(i);
+ if (TextUtils.equals(device.getAddress(), address)) {
+ if (device.getRssi() != result.getRssi() && System.currentTimeMillis() - device.getRssiUpdateTime() > RSSI_UPDATE_INTERVAL_MS) {
+ device.setRssi(result.getRssi());
+ device.setRssiUpdateTime(System.currentTimeMillis());
+ mItems.set(i, device);
+ notifyItemChanged(i);
+ }
+ return;
+ }
+ }
+
+ BtDevice newDevice = new BtDevice(address, name);
+ newDevice.setConnectable(result.isConnectable());
+ newDevice.setScanRecord(result.getScanRecord());
+ newDevice.setRssi(result.getRssi());
+ newDevice.setRssiUpdateTime(System.currentTimeMillis());
+ mItems.add(newDevice);
+ notifyDataSetChanged();
+
+ if (bleDiscoveryCallback != null) {
+ bleDiscoveryCallback.onDiscoveryResult(newDevice);
+ }
+ }
+ }
+
+ @Override
+ public void onScanFailed(final int errorCode) {
+ stopScan();
+ if (bleDiscoveryCallback != null) {
+ bleDiscoveryCallback.onDiscoveryFailed(errorCode);
+ }
+ }
+ };
+
+}
diff --git a/tools/test_suite/android/app/src/main/java/com/openvela/bluetoothtest/ble/GattClientAdapter.java b/tools/test_suite/android/app/src/main/java/com/openvela/bluetoothtest/ble/GattClientAdapter.java
new file mode 100755
index 0000000..26caa75
--- /dev/null
+++ b/tools/test_suite/android/app/src/main/java/com/openvela/bluetoothtest/ble/GattClientAdapter.java
@@ -0,0 +1,226 @@
+/****************************************************************************
+ * Copyright (C) 2024 Xiaomi Corporation
+ *
+ * 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 com.openvela.bluetoothtest.ble;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Map;
+import android.view.View;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Log;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothGatt;
+import android.bluetooth.BluetoothGattCallback;
+import android.bluetooth.BluetoothGattService;
+import android.bluetooth.BluetoothProfile;
+
+import androidx.core.os.HandlerCompat;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.openvela.bluetooth.adapter.RecyclerAdapter;
+import com.openvela.bluetooth.adapter.RecyclerViewHolder;
+import com.openvela.bluetooth.BtDevice;
+import com.openvela.bluetooth.callback.BleConnectCallback;
+import com.openvela.bluetoothtest.R;
+
+public class GattClientAdapter extends RecyclerAdapter {
+ private final static String TAG = GattClientAdapter.class.getSimpleName();
+ private static final String BASE_UUID_REGEX = "0000([0-9a-f][0-9a-f][0-9a-f][0-9a-f])-0000-1000-8000-00805f9b34fb";
+ private static final int GATT_CONNECT_TIMEOUT_MS = 10 * 1000;
+ private final Handler handler = new Handler(Looper.myLooper());
+ private final BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
+ private final Map gattHashMap = new HashMap<>();
+ private BleConnectCallback bleConnectCallback;
+ private int view_position = -1;
+
+ public GattClientAdapter(Context context) {
+ super(context, R.layout.item_gatt_service, new ArrayList<>());
+ }
+
+ @Override
+ public void onBindViewHolderItem(RecyclerViewHolder viewHolder, BluetoothGattService gattService) {
+ // Service UUID
+ String serviceUuid = gattService.getUuid().toString();
+ StringBuilder builder = new StringBuilder();
+ builder.append("UUID: 0x");
+ if (serviceUuid.toLowerCase().matches(BASE_UUID_REGEX)) {
+ builder.append(serviceUuid.substring(4, 8).toUpperCase());
+ } else {
+ builder.append(serviceUuid);
+ }
+ viewHolder.setText(R.id.tv_service_uuid, builder.toString());
+ // Service Type
+ if (gattService.getType() == BluetoothGattService.SERVICE_TYPE_PRIMARY) {
+ viewHolder.setText(R.id.tv_service_type, "PRIMARY SERVICE");
+ } else if (gattService.getType() == BluetoothGattService.SERVICE_TYPE_SECONDARY) {
+ viewHolder.setText(R.id.tv_service_type, "SECONDARY SERVICE");
+ } else {
+ viewHolder.setText(R.id.tv_service_type, "UNKNOWN SERVICE");
+ }
+
+ if (viewHolder.getBindingAdapterPosition() == view_position){
+ GattClientCharAdapter charAdapter = new GattClientCharAdapter(mContext, gattService.getCharacteristics());
+ RecyclerView recyclerView = viewHolder.getView(R.id.recyclerView);
+ recyclerView.setLayoutManager(new LinearLayoutManager(mContext, LinearLayoutManager.VERTICAL, false));
+ recyclerView.setAdapter(charAdapter);
+
+ viewHolder.setVisibility(R.id.recyclerView, View.VISIBLE);
+ } else {
+ viewHolder.setVisibility(R.id.recyclerView, View.GONE);
+ }
+
+ viewHolder.setOnClickListener(v -> {
+ if(viewHolder.getBindingAdapterPosition() == view_position) {
+ notifyItemChanged(view_position);
+ view_position = -1;
+ } else {
+ notifyItemChanged(view_position);
+ view_position = viewHolder.getBindingAdapterPosition();
+ notifyItemChanged(view_position);
+ }
+ });
+ }
+
+ public void connect(BtDevice device, BleConnectCallback callback) {
+ bleConnectCallback = callback;
+
+ String address = device.getAddress();
+ final BluetoothDevice bluetoothdevice = bluetoothAdapter.getRemoteDevice(address);
+ if (bluetoothdevice == null) {
+ bleConnectCallback.onConnectFailed(address, BleConnectCallback.FAILED_DEVICE_NOT_FOUND);
+ return;
+ }
+
+ BluetoothGatt bluetoothGatt;
+ bluetoothGatt = bluetoothdevice.connectGatt(mContext, false, gattCallback, BluetoothDevice.TRANSPORT_LE);
+ if (bluetoothGatt != null) {
+ startConnectTimer(address);
+ bleConnectCallback.onConnectionChanged(address, BluetoothProfile.STATE_CONNECTING);
+ gattHashMap.put(address, bluetoothGatt);
+ }
+ }
+
+ public void disconnect(BtDevice device) {
+ String address = device.getAddress();
+ BluetoothGatt bluetoothGatt = gattHashMap.get(address);
+
+ if (bluetoothGatt != null) {
+ cancelConnectTimer(address);
+ bluetoothGatt.disconnect();
+ bleConnectCallback.onConnectionChanged(address, BluetoothProfile.STATE_DISCONNECTING);
+ }
+ }
+
+ public void cancelConnect(BtDevice device) {
+ String address = device.getAddress();
+ BluetoothGatt bluetoothGatt = gattHashMap.get(address);
+
+ if (bluetoothGatt != null) {
+ cancelConnectTimer(address);
+ bluetoothGatt.disconnect();
+ bleConnectCallback.onConnectionChanged(address, BluetoothProfile.STATE_DISCONNECTED);
+ }
+ }
+
+ private void cancelConnectTimer(String address){
+ handler.removeCallbacksAndMessages(address);
+ }
+
+ private void startConnectTimer(String address) {
+ cancelConnectTimer(address);
+
+ HandlerCompat.postDelayed(handler, () -> {
+ bleConnectCallback.onConnectFailed(address, BleConnectCallback.FAILED_TIMEOUT);
+ close(address);
+ }, address, GATT_CONNECT_TIMEOUT_MS);
+ }
+
+ private void close(String address) {
+ BluetoothGatt bluetoothGatt = gattHashMap.get(address);
+ if (bluetoothGatt != null) {
+ bluetoothGatt.close();
+ gattHashMap.remove(address);
+ }
+ }
+
+ private final BluetoothGattCallback gattCallback = new BluetoothGattCallback() {
+ @Override
+ public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
+ BluetoothDevice device = gatt.getDevice();
+ if (device == null){
+ return;
+ }
+
+ String address = device.getAddress();
+ cancelConnectTimer(address);
+
+ if (status != BluetoothGatt.GATT_SUCCESS) {
+ if (bleConnectCallback != null){
+ bleConnectCallback.onConnectFailed(address, status);
+ }
+ close(address);
+ return;
+ }
+ if (newState == BluetoothProfile.STATE_CONNECTED) {
+ if (bleConnectCallback != null) {
+ bleConnectCallback.onConnectionChanged(address, BluetoothProfile.STATE_CONNECTED);
+ }
+ gatt.discoverServices();
+ } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
+ if (bleConnectCallback != null) {
+ bleConnectCallback.onConnectionChanged(address, BluetoothProfile.STATE_DISCONNECTED);
+ }
+ close(address);
+ }
+ }
+
+ @SuppressLint("NotifyDataSetChanged")
+ @Override
+ public void onServicesDiscovered(BluetoothGatt gatt, int status) {
+ BluetoothDevice device = gatt.getDevice();
+ if (device == null){
+ return;
+ }
+
+ String address = device.getAddress();
+ if (status == BluetoothGatt.GATT_SUCCESS) {
+ if (bleConnectCallback != null) {
+ bleConnectCallback.onServicesDiscovered(address);
+ }
+ handler.post(() -> {
+ mItems.clear();
+ mItems.addAll(gatt.getServices());
+ notifyDataSetChanged();
+ });
+ } else {
+ Log.e(TAG, "onServicesDiscovered failed: " + status);
+ }
+ }
+
+ public void onMtuChanged(BluetoothGatt gatt, int mtu, int status){
+
+ }
+ };
+
+}
diff --git a/tools/test_suite/android/app/src/main/java/com/openvela/bluetoothtest/ble/GattClientCharAdapter.java b/tools/test_suite/android/app/src/main/java/com/openvela/bluetoothtest/ble/GattClientCharAdapter.java
new file mode 100644
index 0000000..b1a71d8
--- /dev/null
+++ b/tools/test_suite/android/app/src/main/java/com/openvela/bluetoothtest/ble/GattClientCharAdapter.java
@@ -0,0 +1,85 @@
+/****************************************************************************
+ * Copyright (C) 2024 Xiaomi Corporation
+ *
+ * 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 com.openvela.bluetoothtest.ble;
+
+import java.util.List;
+
+import android.content.Context;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Log;
+import android.view.View;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothGatt;
+import android.bluetooth.BluetoothGattCharacteristic;
+
+import com.openvela.bluetooth.adapter.RecyclerAdapter;
+import com.openvela.bluetooth.adapter.RecyclerViewHolder;
+import com.openvela.bluetoothtest.R;
+
+public class GattClientCharAdapter extends RecyclerAdapter {
+ private final static String TAG = GattClientCharAdapter.class.getSimpleName();
+ private static final String BASE_UUID_REGEX = "0000([0-9a-f][0-9a-f][0-9a-f][0-9a-f])-0000-1000-8000-00805f9b34fb";
+ private final Handler handler = new Handler(Looper.myLooper());
+ private final BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
+
+ public GattClientCharAdapter(Context context, List data) {
+ super(context, R.layout.item_gatt_element, data);
+ }
+
+ @Override
+ public void onBindViewHolderItem(RecyclerViewHolder viewHolder, BluetoothGattCharacteristic gattChar) {
+ // Char UUID
+ String charUuid = gattChar.getUuid().toString();
+ StringBuilder builder = new StringBuilder();
+ builder.append("UUID: 0x");
+ if (charUuid.toLowerCase().matches(BASE_UUID_REGEX)) {
+ builder.append(charUuid.substring(4, 8).toUpperCase());
+ } else {
+ builder.append(charUuid);
+ }
+ viewHolder.setText(R.id.tv_char_uuid, builder.toString());
+
+ // Char Properties
+ int charProp = gattChar.getProperties();
+ builder.setLength(0);
+ if ((charProp & BluetoothGattCharacteristic.PROPERTY_READ) != 0) {
+ builder.append("READ,");
+ }
+ if ((charProp & BluetoothGattCharacteristic.PROPERTY_WRITE) != 0) {
+ builder.append("WRITE,");
+ }
+ if ((charProp & BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE) != 0) {
+ builder.append("WRITE_NO_RESPONSE,");
+ }
+ if ((charProp & BluetoothGattCharacteristic.PROPERTY_NOTIFY) != 0) {
+ builder.append("NOTIFY,");
+ }
+ if ((charProp & BluetoothGattCharacteristic.PROPERTY_INDICATE) != 0) {
+ builder.append("INDICATE,");
+ }
+ viewHolder.setText(R.id.tv_char_prop, String.format("Properties: %s", builder.toString()));
+
+ if ((charProp & BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE) != 0) {
+ viewHolder.setVisibility(R.id.tv_write_tput, View.VISIBLE);
+ viewHolder.setOnClickListener(R.id.tv_write_tput, v -> {
+ Log.i(TAG, "Write Tput start!");
+ });
+ }
+ }
+}
diff --git a/tools/test_suite/android/app/src/main/java/com/openvela/bluetoothtest/bredr/BredrInquiryActivity.java b/tools/test_suite/android/app/src/main/java/com/openvela/bluetoothtest/bredr/BredrInquiryActivity.java
new file mode 100755
index 0000000..ab27b30
--- /dev/null
+++ b/tools/test_suite/android/app/src/main/java/com/openvela/bluetoothtest/bredr/BredrInquiryActivity.java
@@ -0,0 +1,105 @@
+/****************************************************************************
+ * Copyright (C) 2024 Xiaomi Corporation
+ *
+ * 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 com.openvela.bluetoothtest.bredr;
+
+import android.os.Bundle;
+import android.util.Log;
+import android.widget.EditText;
+import android.widget.TextView;
+import android.widget.Button;
+
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.recyclerview.widget.DividerItemDecoration;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.openvela.bluetooth.BtDevice;
+import com.openvela.bluetooth.callback.BluetoothDiscoveryCallback;
+import com.openvela.bluetoothtest.R;
+
+public class BredrInquiryActivity extends AppCompatActivity {
+ private final String TAG = BredrInquiryActivity.class.getSimpleName();
+ private static final long BREDR_INQUIRY_PERIOD_MS = 12 * 1000;
+ private TextView tvInquiryState;
+ private Button btnInquiry;
+ private EditText etFilter;
+ private BredrInquiryAdapter bredrInquiryAdapter;
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_bredr_inquiry);
+ bredrInquiryAdapter = new BredrInquiryAdapter(this);
+ initView();
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ if (bredrInquiryAdapter.isDiscovering()) {
+ bredrInquiryAdapter.stopDiscovery();
+ }
+ }
+
+ private void initView() {
+ tvInquiryState = findViewById(R.id.tv_inquiry_state);
+ btnInquiry = findViewById(R.id.btn_inquiry);
+ etFilter = findViewById(R.id.et_filter);
+ RecyclerView recyclerView = findViewById(R.id.recyclerView);
+
+ btnInquiry.setOnClickListener(v -> {
+ if (bredrInquiryAdapter.isDiscovering()) {
+ bredrInquiryAdapter.stopDiscovery();
+ } else {
+ bredrInquiryAdapter.startDiscovery(new String[]{etFilter.getText().toString()}, BREDR_INQUIRY_PERIOD_MS, discoveryCallback);
+ }
+ });
+
+ recyclerView.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false));
+ recyclerView.addItemDecoration(new DividerItemDecoration(this, DividerItemDecoration.VERTICAL));
+ recyclerView.getItemAnimator().setChangeDuration(300);
+ recyclerView.getItemAnimator().setMoveDuration(300);
+ recyclerView.setAdapter(bredrInquiryAdapter);
+ }
+
+ private final BluetoothDiscoveryCallback discoveryCallback = new BluetoothDiscoveryCallback() {
+ @Override
+ public void onDiscoveryResult(final BtDevice device) {}
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ tvInquiryState.setText("Inquiring...");
+ btnInquiry.setText("STOP INQUIRY");
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+ tvInquiryState.setText("Inquiry Stopped");
+ btnInquiry.setText("START SCAN");
+ }
+
+ @Override
+ public void onDiscoveryFailed(int errorCode) {
+ super.onDiscoveryFailed(errorCode);
+ tvInquiryState.setText("Inquiry Failed: " + errorCode);
+ Log.e(TAG, "onDiscoveryFailed: " + errorCode);
+ }
+ };
+}
diff --git a/tools/test_suite/android/app/src/main/java/com/openvela/bluetoothtest/bredr/BredrInquiryAdapter.java b/tools/test_suite/android/app/src/main/java/com/openvela/bluetoothtest/bredr/BredrInquiryAdapter.java
new file mode 100755
index 0000000..b89fdad
--- /dev/null
+++ b/tools/test_suite/android/app/src/main/java/com/openvela/bluetoothtest/bredr/BredrInquiryAdapter.java
@@ -0,0 +1,187 @@
+/****************************************************************************
+ * Copyright (C) 2024 Xiaomi Corporation
+ *
+ * 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 com.openvela.bluetoothtest.bredr;
+
+import java.util.ArrayList;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.os.Handler;
+import android.os.Looper;
+import android.text.TextUtils;
+import android.view.View;
+
+import android.bluetooth.BluetoothAdapter;
+
+import androidx.core.os.HandlerCompat;
+
+import com.openvela.bluetooth.adapter.RecyclerAdapter;
+import com.openvela.bluetooth.adapter.RecyclerViewHolder;
+import com.openvela.bluetooth.BluetoothDiscoveryObserver;
+import com.openvela.bluetooth.BtDevice;
+import com.openvela.bluetooth.callback.BluetoothDiscoveryCallback;
+
+import com.openvela.bluetoothtest.R;
+
+public class BredrInquiryAdapter extends RecyclerAdapter {
+ private final String TAG = BredrInquiryAdapter.class.getSimpleName();
+ private static final String DISCOVERY_TIMEOUT_TOKEN = "discovery_timeout_token";
+ private static final int RSSI_UPDATE_INTERVAL_MS = 2 * 1000;
+ private final Handler handler = new Handler(Looper.myLooper());
+ private final BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
+ private volatile BluetoothDiscoveryObserver bluetoothDiscoveryObserver;
+ private String[] discoveryFilters;
+ private BluetoothDiscoveryCallback bluetoothDiscoveryCallback;
+ private int view_position = -1;
+
+ public BredrInquiryAdapter(Context context) {
+ super(context, R.layout.item_inquiry_result, new ArrayList<>());
+ bluetoothDiscoveryObserver = new BluetoothDiscoveryObserver(context);
+ bluetoothDiscoveryObserver.registerReceiver(discoveryCallback);
+ }
+
+ @SuppressLint("DefaultLocale")
+ @Override
+ public void onBindViewHolderItem(RecyclerViewHolder viewHolder, BtDevice device) {
+ viewHolder.setText(R.id.tv_address, device.getAddress());
+ viewHolder.setText(R.id.tv_rssi, String.format("%ddBm", device.getRssi()));
+ if (device.getName() == null){
+ viewHolder.setText(R.id.tv_name, "Unknown");
+ } else {
+ viewHolder.setText(R.id.tv_name, device.getName());
+ }
+
+ if (viewHolder.getBindingAdapterPosition() == view_position){
+
+ } else {
+ viewHolder.setVisibility(R.id.ll_detail, View.GONE);
+ }
+ }
+
+ public boolean isDiscovering() {
+ return bluetoothDiscoveryObserver.isDiscovering();
+ }
+
+ public void startDiscovery(final String[] discoveryFilters, long discoveryPeriod, BluetoothDiscoveryCallback callback) {
+ bluetoothDiscoveryCallback = callback;
+
+ if (!bluetoothAdapter.isEnabled()) {
+ if (bluetoothDiscoveryCallback != null) {
+ bluetoothDiscoveryCallback.onDiscoveryFailed(BluetoothDiscoveryCallback.FAILED_INTERNAL_ERROR);
+ return;
+ }
+ }
+
+ if (isDiscovering()) {
+ if (bluetoothDiscoveryCallback != null){
+ bluetoothDiscoveryCallback.onDiscoveryFailed(BluetoothDiscoveryCallback.FAILED_ALREADY_STARTED);
+ }
+ return;
+ }
+
+ super.mItems.clear();
+ startDiscoveryTimer(discoveryPeriod);
+
+ this.discoveryFilters = discoveryFilters;
+ bluetoothDiscoveryObserver.startDiscovery();
+ }
+
+ public void stopDiscovery() {
+ if (!isDiscovering()) {
+ return;
+ }
+
+ cancelDiscoveryTimer();
+ bluetoothDiscoveryObserver.cancelDiscovery();
+ }
+
+ private void cancelDiscoveryTimer() {
+ handler.removeCallbacksAndMessages(DISCOVERY_TIMEOUT_TOKEN);
+ }
+
+ private void startDiscoveryTimer(long discoveryPeriod) {
+ cancelDiscoveryTimer();
+
+ if (discoveryPeriod >= 0){
+ HandlerCompat.postDelayed(handler, () -> {
+ if (isDiscovering()) {
+ stopDiscovery();
+ }
+ }, DISCOVERY_TIMEOUT_TOKEN, discoveryPeriod);
+ }
+ }
+
+ private final BluetoothDiscoveryCallback discoveryCallback = new BluetoothDiscoveryCallback() {
+ @Override
+ public void onDiscoveryResult(final BtDevice foundDevice) {
+ final String address = foundDevice.getAddress();
+ final String name = foundDevice.getName();
+ boolean found = true;
+
+ if (discoveryFilters != null) {
+ found = false;
+ for (String filter : discoveryFilters) {
+ if ((address != null && address.toLowerCase().contains(filter.toLowerCase())) ||
+ (name != null && name.toLowerCase().contains(filter.toLowerCase()))) {
+ found = true;
+ break;
+ }
+ }
+ }
+ if (!found) {
+ return;
+ }
+
+ for (int i = 0; i < getItemCount(); i++) {
+ BtDevice device = mItems.get(i);
+ if (TextUtils.equals(device.getAddress(), address)) {
+ if (device.getRssi() != foundDevice.getRssi() && System.currentTimeMillis() - device.getRssiUpdateTime() > RSSI_UPDATE_INTERVAL_MS) {
+ device.setRssi(foundDevice.getRssi());
+ device.setRssiUpdateTime(System.currentTimeMillis());
+ mItems.set(i, device);
+ notifyItemChanged(i);
+ }
+ return;
+ }
+ }
+
+ BtDevice newDevice = new BtDevice(address, name);
+ newDevice.setRssi(foundDevice.getRssi());
+ newDevice.setRssiUpdateTime(System.currentTimeMillis());
+ mItems.add(newDevice);
+ notifyDataSetChanged();
+
+ if (bluetoothDiscoveryCallback != null) {
+ bluetoothDiscoveryCallback.onDiscoveryResult(newDevice);
+ }
+ }
+
+ @Override
+ public void onStart() {
+ if (bluetoothDiscoveryCallback != null) {
+ bluetoothDiscoveryCallback.onStart();
+ }
+ }
+
+ @Override
+ public void onStop() {
+ if (bluetoothDiscoveryCallback != null) {
+ bluetoothDiscoveryCallback.onStop();
+ }
+ }
+ };
+}
diff --git a/tools/test_suite/android/app/src/main/res/drawable/ic_launcher_background.xml b/tools/test_suite/android/app/src/main/res/drawable/ic_launcher_background.xml
new file mode 100755
index 0000000..9ae8512
--- /dev/null
+++ b/tools/test_suite/android/app/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,74 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tools/test_suite/android/app/src/main/res/drawable/ic_launcher_foreground.xml b/tools/test_suite/android/app/src/main/res/drawable/ic_launcher_foreground.xml
new file mode 100755
index 0000000..2b068d1
--- /dev/null
+++ b/tools/test_suite/android/app/src/main/res/drawable/ic_launcher_foreground.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tools/test_suite/android/app/src/main/res/layout/activity_ble_central.xml b/tools/test_suite/android/app/src/main/res/layout/activity_ble_central.xml
new file mode 100755
index 0000000..d3b9098
--- /dev/null
+++ b/tools/test_suite/android/app/src/main/res/layout/activity_ble_central.xml
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tools/test_suite/android/app/src/main/res/layout/activity_ble_peripheral.xml b/tools/test_suite/android/app/src/main/res/layout/activity_ble_peripheral.xml
new file mode 100755
index 0000000..19322fc
--- /dev/null
+++ b/tools/test_suite/android/app/src/main/res/layout/activity_ble_peripheral.xml
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tools/test_suite/android/app/src/main/res/layout/activity_ble_scan.xml b/tools/test_suite/android/app/src/main/res/layout/activity_ble_scan.xml
new file mode 100755
index 0000000..70f711b
--- /dev/null
+++ b/tools/test_suite/android/app/src/main/res/layout/activity_ble_scan.xml
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tools/test_suite/android/app/src/main/res/layout/activity_bredr_inquiry.xml b/tools/test_suite/android/app/src/main/res/layout/activity_bredr_inquiry.xml
new file mode 100755
index 0000000..684149e
--- /dev/null
+++ b/tools/test_suite/android/app/src/main/res/layout/activity_bredr_inquiry.xml
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tools/test_suite/android/app/src/main/res/layout/activity_main.xml b/tools/test_suite/android/app/src/main/res/layout/activity_main.xml
new file mode 100755
index 0000000..022a78b
--- /dev/null
+++ b/tools/test_suite/android/app/src/main/res/layout/activity_main.xml
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tools/test_suite/android/app/src/main/res/layout/item_gatt_element.xml b/tools/test_suite/android/app/src/main/res/layout/item_gatt_element.xml
new file mode 100644
index 0000000..4895b02
--- /dev/null
+++ b/tools/test_suite/android/app/src/main/res/layout/item_gatt_element.xml
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tools/test_suite/android/app/src/main/res/layout/item_gatt_service.xml b/tools/test_suite/android/app/src/main/res/layout/item_gatt_service.xml
new file mode 100755
index 0000000..fd4d168
--- /dev/null
+++ b/tools/test_suite/android/app/src/main/res/layout/item_gatt_service.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tools/test_suite/android/app/src/main/res/layout/item_inquiry_result.xml b/tools/test_suite/android/app/src/main/res/layout/item_inquiry_result.xml
new file mode 100755
index 0000000..d0f7386
--- /dev/null
+++ b/tools/test_suite/android/app/src/main/res/layout/item_inquiry_result.xml
@@ -0,0 +1,69 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tools/test_suite/android/app/src/main/res/layout/item_scan_result.xml b/tools/test_suite/android/app/src/main/res/layout/item_scan_result.xml
new file mode 100755
index 0000000..d0f7386
--- /dev/null
+++ b/tools/test_suite/android/app/src/main/res/layout/item_scan_result.xml
@@ -0,0 +1,69 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tools/test_suite/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/tools/test_suite/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100755
index 0000000..67820c5
--- /dev/null
+++ b/tools/test_suite/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/tools/test_suite/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/tools/test_suite/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100755
index 0000000..67820c5
--- /dev/null
+++ b/tools/test_suite/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/tools/test_suite/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/tools/test_suite/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp
new file mode 100755
index 0000000..93e024c
Binary files /dev/null and b/tools/test_suite/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ
diff --git a/tools/test_suite/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp b/tools/test_suite/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp
new file mode 100755
index 0000000..0054dab
Binary files /dev/null and b/tools/test_suite/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp differ
diff --git a/tools/test_suite/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/tools/test_suite/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
new file mode 100755
index 0000000..df7da53
Binary files /dev/null and b/tools/test_suite/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ
diff --git a/tools/test_suite/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/tools/test_suite/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp
new file mode 100755
index 0000000..a378217
Binary files /dev/null and b/tools/test_suite/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ
diff --git a/tools/test_suite/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp b/tools/test_suite/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp
new file mode 100755
index 0000000..1e57a8d
Binary files /dev/null and b/tools/test_suite/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp differ
diff --git a/tools/test_suite/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/tools/test_suite/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
new file mode 100755
index 0000000..4179ef9
Binary files /dev/null and b/tools/test_suite/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ
diff --git a/tools/test_suite/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/tools/test_suite/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
new file mode 100755
index 0000000..14b8fd9
Binary files /dev/null and b/tools/test_suite/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ
diff --git a/tools/test_suite/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp b/tools/test_suite/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp
new file mode 100755
index 0000000..1efcd0d
Binary files /dev/null and b/tools/test_suite/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp differ
diff --git a/tools/test_suite/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/tools/test_suite/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
new file mode 100755
index 0000000..4c9080c
Binary files /dev/null and b/tools/test_suite/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ
diff --git a/tools/test_suite/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/tools/test_suite/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
new file mode 100755
index 0000000..e0a2cd8
Binary files /dev/null and b/tools/test_suite/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ
diff --git a/tools/test_suite/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp b/tools/test_suite/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp
new file mode 100755
index 0000000..0a34969
Binary files /dev/null and b/tools/test_suite/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp differ
diff --git a/tools/test_suite/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/tools/test_suite/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
new file mode 100755
index 0000000..c3df462
Binary files /dev/null and b/tools/test_suite/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ
diff --git a/tools/test_suite/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/tools/test_suite/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
new file mode 100755
index 0000000..20f6568
Binary files /dev/null and b/tools/test_suite/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ
diff --git a/tools/test_suite/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp b/tools/test_suite/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp
new file mode 100755
index 0000000..62f745e
Binary files /dev/null and b/tools/test_suite/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp differ
diff --git a/tools/test_suite/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/tools/test_suite/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
new file mode 100755
index 0000000..c930d99
Binary files /dev/null and b/tools/test_suite/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ
diff --git a/tools/test_suite/android/app/src/main/res/values-night/themes.xml b/tools/test_suite/android/app/src/main/res/values-night/themes.xml
new file mode 100755
index 0000000..4fea9bd
--- /dev/null
+++ b/tools/test_suite/android/app/src/main/res/values-night/themes.xml
@@ -0,0 +1,7 @@
+
+
+
+
\ No newline at end of file
diff --git a/tools/test_suite/android/app/src/main/res/values/colors.xml b/tools/test_suite/android/app/src/main/res/values/colors.xml
new file mode 100755
index 0000000..bc29b00
--- /dev/null
+++ b/tools/test_suite/android/app/src/main/res/values/colors.xml
@@ -0,0 +1,12 @@
+
+
+ #FF000000
+ #FFFFFFFF
+ #0074FF
+ #001EFF
+ #D81B60
+ #B22222
+ #909090
+ #303030
+ #5c5c5c
+
diff --git a/tools/test_suite/android/app/src/main/res/values/ic_launcher_background.xml b/tools/test_suite/android/app/src/main/res/values/ic_launcher_background.xml
new file mode 100755
index 0000000..2824a44
--- /dev/null
+++ b/tools/test_suite/android/app/src/main/res/values/ic_launcher_background.xml
@@ -0,0 +1,4 @@
+
+
+ #0074FF
+
\ No newline at end of file
diff --git a/tools/test_suite/android/app/src/main/res/values/strings.xml b/tools/test_suite/android/app/src/main/res/values/strings.xml
new file mode 100755
index 0000000..fd423aa
--- /dev/null
+++ b/tools/test_suite/android/app/src/main/res/values/strings.xml
@@ -0,0 +1,11 @@
+
+ Bluetooth Test Suite
+ BLE Scan
+ BLE Central
+ BLE Peripheral
+ BREDR Inquiry
+
+ ENABLE
+ Bluetooth adapter is disabled
+
+
diff --git a/tools/test_suite/android/app/src/main/res/values/themes.xml b/tools/test_suite/android/app/src/main/res/values/themes.xml
new file mode 100755
index 0000000..399630f
--- /dev/null
+++ b/tools/test_suite/android/app/src/main/res/values/themes.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
diff --git a/tools/test_suite/android/app/src/main/res/xml/backup_rules.xml b/tools/test_suite/android/app/src/main/res/xml/backup_rules.xml
new file mode 100755
index 0000000..fa0f996
--- /dev/null
+++ b/tools/test_suite/android/app/src/main/res/xml/backup_rules.xml
@@ -0,0 +1,13 @@
+
+
+
+
\ No newline at end of file
diff --git a/tools/test_suite/android/app/src/main/res/xml/data_extraction_rules.xml b/tools/test_suite/android/app/src/main/res/xml/data_extraction_rules.xml
new file mode 100755
index 0000000..9ee9997
--- /dev/null
+++ b/tools/test_suite/android/app/src/main/res/xml/data_extraction_rules.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tools/test_suite/android/app/src/test/java/com/xiaomi/velabluetooth/ExampleUnitTest.java b/tools/test_suite/android/app/src/test/java/com/xiaomi/velabluetooth/ExampleUnitTest.java
new file mode 100755
index 0000000..963e4c9
--- /dev/null
+++ b/tools/test_suite/android/app/src/test/java/com/xiaomi/velabluetooth/ExampleUnitTest.java
@@ -0,0 +1,17 @@
+package com.openvela.bluetoothtest;
+
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * @see Testing documentation
+ */
+public class ExampleUnitTest {
+ @Test
+ public void addition_isCorrect() throws Exception {
+ assertEquals(4, 2 + 2);
+ }
+}
\ No newline at end of file
diff --git a/tools/test_suite/android/build.gradle b/tools/test_suite/android/build.gradle
new file mode 100755
index 0000000..71bb1bd
--- /dev/null
+++ b/tools/test_suite/android/build.gradle
@@ -0,0 +1,9 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+plugins {
+ id 'com.android.application' version '8.7.2' apply false
+ id 'com.android.library' version '8.7.2' apply false
+}
+
+tasks.register('clean', Delete) {
+ delete rootProject.getLayout().getBuildDirectory()
+}
\ No newline at end of file
diff --git a/tools/test_suite/android/core/.gitignore b/tools/test_suite/android/core/.gitignore
new file mode 100755
index 0000000..796b96d
--- /dev/null
+++ b/tools/test_suite/android/core/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/tools/test_suite/android/core/build.gradle b/tools/test_suite/android/core/build.gradle
new file mode 100755
index 0000000..409905f
--- /dev/null
+++ b/tools/test_suite/android/core/build.gradle
@@ -0,0 +1,33 @@
+plugins {
+ id 'com.android.library'
+}
+
+android {
+ namespace 'com.openvela.bluetoothtest'
+ compileSdk 34
+
+ defaultConfig {
+ minSdk 29
+ targetSdk 34
+
+ testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+}
+
+dependencies {
+ implementation fileTree(dir: 'libs', include: ['*.jar'])
+ implementation 'androidx.appcompat:appcompat:1.6.1'
+ implementation 'androidx.recyclerview:recyclerview:1.3.2'
+}
diff --git a/tools/test_suite/android/core/proguard-rules.pro b/tools/test_suite/android/core/proguard-rules.pro
new file mode 100755
index 0000000..481bb43
--- /dev/null
+++ b/tools/test_suite/android/core/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/tools/test_suite/android/core/src/main/AndroidManifest.xml b/tools/test_suite/android/core/src/main/AndroidManifest.xml
new file mode 100755
index 0000000..19e046c
--- /dev/null
+++ b/tools/test_suite/android/core/src/main/AndroidManifest.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/tools/test_suite/android/core/src/main/java/com/openvela/bluetooth/BluetoothDiscoveryObserver.java b/tools/test_suite/android/core/src/main/java/com/openvela/bluetooth/BluetoothDiscoveryObserver.java
new file mode 100755
index 0000000..bd85487
--- /dev/null
+++ b/tools/test_suite/android/core/src/main/java/com/openvela/bluetooth/BluetoothDiscoveryObserver.java
@@ -0,0 +1,94 @@
+/****************************************************************************
+ * Copyright (C) 2024 Xiaomi Corporation
+ *
+ * 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 com.openvela.bluetooth;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+
+import com.openvela.bluetooth.callback.BluetoothDiscoveryCallback;
+
+public class BluetoothDiscoveryObserver extends BroadcastReceiver {
+ private final Context context;
+ private final BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
+ private BluetoothDiscoveryCallback bluetoothDiscoveryCallback;
+
+ public BluetoothDiscoveryObserver(Context context){
+ this.context = context;
+ }
+
+ public boolean isDiscovering() {
+ return bluetoothAdapter.isDiscovering();
+ }
+
+ public void startDiscovery() {
+ bluetoothAdapter.startDiscovery();
+ }
+
+ public void cancelDiscovery() {
+ bluetoothAdapter.cancelDiscovery();
+ }
+
+ public void registerReceiver(BluetoothDiscoveryCallback callback) {
+ final IntentFilter filter = new IntentFilter();
+ filter.addAction(BluetoothAdapter.ACTION_DISCOVERY_STARTED);
+ filter.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED);
+ filter.addAction(BluetoothDevice.ACTION_FOUND);
+ context.registerReceiver(this, filter);
+ this.bluetoothDiscoveryCallback = callback;
+ }
+
+ public void unregisterReceiver() {
+ try {
+ context.unregisterReceiver(this);
+ this.bluetoothDiscoveryCallback = null;
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String action = intent.getAction();
+ if (action == null)
+ return;
+
+ switch(action) {
+ case BluetoothAdapter.ACTION_DISCOVERY_STARTED:
+ if (bluetoothDiscoveryCallback != null)
+ bluetoothDiscoveryCallback.onStart();
+ break;
+ case BluetoothAdapter.ACTION_DISCOVERY_FINISHED:
+ if (bluetoothDiscoveryCallback != null)
+ bluetoothDiscoveryCallback.onStop();
+ break;
+ case BluetoothDevice.ACTION_FOUND:
+ BluetoothDevice bluetoothDevice = intent.getParcelableExtra(android.bluetooth.BluetoothDevice.EXTRA_DEVICE);
+ int rssi = intent.getShortExtra(android.bluetooth.BluetoothDevice.EXTRA_RSSI, Short.MIN_VALUE);
+ BtDevice btDevice = new BtDevice(bluetoothDevice.getAddress(), bluetoothDevice.getName());
+ btDevice.setRssi(rssi);
+ if (bluetoothDiscoveryCallback != null)
+ bluetoothDiscoveryCallback.onDiscoveryResult(btDevice);
+ break;
+ default:
+ break;
+ }
+ }
+}
diff --git a/tools/test_suite/android/core/src/main/java/com/openvela/bluetooth/BluetoothStateObserver.java b/tools/test_suite/android/core/src/main/java/com/openvela/bluetooth/BluetoothStateObserver.java
new file mode 100755
index 0000000..2a1e77c
--- /dev/null
+++ b/tools/test_suite/android/core/src/main/java/com/openvela/bluetooth/BluetoothStateObserver.java
@@ -0,0 +1,70 @@
+/****************************************************************************
+ * Copyright (C) 2024 Xiaomi Corporation
+ *
+ * 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 com.openvela.bluetooth;
+
+import android.bluetooth.BluetoothAdapter;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+
+import com.openvela.bluetooth.callback.BluetoothStateCallback;
+
+public class BluetoothStateObserver extends BroadcastReceiver {
+ private final Context context;
+ private BluetoothStateCallback bluetoothStateCallback;
+
+ public BluetoothStateObserver(Context context){
+ this.context = context;
+ }
+
+ public void registerReceiver(BluetoothStateCallback callback) {
+ final IntentFilter filter = new IntentFilter();
+ filter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED);
+ context.registerReceiver(this, filter);
+ this.bluetoothStateCallback = callback;
+ }
+
+ public void unregisterReceiver() {
+ try {
+ context.unregisterReceiver(this);
+ this.bluetoothStateCallback = null;
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String action = intent.getAction();
+ if (action == null)
+ return;
+
+ if (action.equals(BluetoothAdapter.ACTION_STATE_CHANGED)) {
+ int status = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR);
+ if (status == BluetoothAdapter.STATE_ON) {
+ if (bluetoothStateCallback != null) {
+ bluetoothStateCallback.onEnabled();
+ }
+ } else if (status == BluetoothAdapter.STATE_OFF) {
+ if (bluetoothStateCallback != null) {
+ bluetoothStateCallback.onDisabled();
+ }
+ }
+ }
+ }
+}
diff --git a/tools/test_suite/android/core/src/main/java/com/openvela/bluetooth/BtDevice.java b/tools/test_suite/android/core/src/main/java/com/openvela/bluetooth/BtDevice.java
new file mode 100755
index 0000000..61167b7
--- /dev/null
+++ b/tools/test_suite/android/core/src/main/java/com/openvela/bluetooth/BtDevice.java
@@ -0,0 +1,129 @@
+/****************************************************************************
+ * Copyright (C) 2024 Xiaomi Corporation
+ *
+ * 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 com.openvela.bluetooth;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import android.bluetooth.BluetoothProfile;
+import android.bluetooth.le.ScanRecord;
+
+import androidx.annotation.RestrictTo;
+
+public class BtDevice implements Parcelable {
+ private int connectionState = BluetoothProfile.STATE_CONNECTED;
+ private String address;
+ private String name;
+ private int rssi;
+ private boolean connectable;
+ private ScanRecord scanRecord;
+ private long rssiUpdateTime;
+
+ public BtDevice(String address, String name) {
+ this.address = address;
+ this.name = name;
+ }
+
+ protected BtDevice(Parcel in) {
+ this.connectionState = in.readInt();
+ this.address = in.readString();
+ this.name = in.readString();
+ this.connectable = in.readBoolean();
+ }
+
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public static final Creator CREATOR = new Creator() {
+ @Override
+ public BtDevice createFromParcel(Parcel in) {
+ return new BtDevice(in);
+ }
+
+ @Override
+ public BtDevice[] newArray(int size) {
+ return new BtDevice[size];
+ }
+ };
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(this.connectionState);
+ dest.writeString(this.address);
+ dest.writeString(this.name);
+ dest.writeBoolean(this.connectable);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ public int getConnectionState() {
+ return this.connectionState;
+ }
+
+ public void setConnectionState(int state) {
+ this.connectionState = state;
+ }
+
+ public String getAddress() {
+ return this.address;
+ }
+
+ public void setAddress(String address) {
+ this.address = address;
+ }
+
+ public String getName() {
+ return this.name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public int getRssi() {
+ return this.rssi;
+ }
+
+ public void setRssi(int rssi) {
+ this.rssi = rssi;
+ }
+
+ public boolean isConnectable() {
+ return this.connectable;
+ }
+
+ public void setConnectable(boolean Connectable) {
+ this.connectable = Connectable;
+ }
+
+ public ScanRecord getScanRecord() {
+ return this.scanRecord;
+ }
+
+ public void setScanRecord(ScanRecord scanRecord) {
+ this.scanRecord = scanRecord;
+ }
+
+ public long getRssiUpdateTime() {
+ return this.rssiUpdateTime;
+ }
+
+ public void setRssiUpdateTime(long rssiUpdateTime) {
+ this.rssiUpdateTime = rssiUpdateTime;
+ }
+}
diff --git a/tools/test_suite/android/core/src/main/java/com/openvela/bluetooth/adapter/RecyclerAdapter.java b/tools/test_suite/android/core/src/main/java/com/openvela/bluetooth/adapter/RecyclerAdapter.java
new file mode 100755
index 0000000..279aa00
--- /dev/null
+++ b/tools/test_suite/android/core/src/main/java/com/openvela/bluetooth/adapter/RecyclerAdapter.java
@@ -0,0 +1,54 @@
+/****************************************************************************
+ * Copyright (C) 2024 Xiaomi Corporation
+ *
+ * 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 com.openvela.bluetooth.adapter;
+
+import java.util.List;
+import android.content.Context;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView;
+
+public abstract class RecyclerAdapter extends RecyclerView.Adapter {
+ protected Context mContext;
+ protected int mLayoutId;
+ protected List mItems;
+
+ public RecyclerAdapter(Context context, int layoutId, List items) {
+ mContext = context;
+ mLayoutId = layoutId;
+ mItems = items;
+ }
+
+ @NonNull
+ @Override
+ public RecyclerViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, int viewType) {
+ return RecyclerViewHolder.get(mContext, parent, mLayoutId);
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull RecyclerViewHolder holder, int position) {
+ onBindViewHolderItem(holder, mItems.get(position));
+ }
+
+ @Override
+ public int getItemCount() {
+ return mItems.size();
+ }
+
+ public abstract void onBindViewHolderItem(RecyclerViewHolder holder, T t);
+}
\ No newline at end of file
diff --git a/tools/test_suite/android/core/src/main/java/com/openvela/bluetooth/adapter/RecyclerViewHolder.java b/tools/test_suite/android/core/src/main/java/com/openvela/bluetooth/adapter/RecyclerViewHolder.java
new file mode 100755
index 0000000..412a88d
--- /dev/null
+++ b/tools/test_suite/android/core/src/main/java/com/openvela/bluetooth/adapter/RecyclerViewHolder.java
@@ -0,0 +1,74 @@
+/****************************************************************************
+ * Copyright (C) 2024 Xiaomi Corporation
+ *
+ * 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 com.openvela.bluetooth.adapter;
+
+import android.content.Context;
+import android.util.SparseArray;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+import androidx.recyclerview.widget.RecyclerView;
+
+public class RecyclerViewHolder extends RecyclerView.ViewHolder {
+ private final SparseArray mViews;
+ private final View mView;
+
+ public RecyclerViewHolder(View view) {
+ super(view);
+ mView = view;
+ mViews = new SparseArray<>();
+ }
+
+ public static RecyclerViewHolder get(Context context, ViewGroup parent, int layoutId) {
+ View itemView = LayoutInflater.from(context).inflate(layoutId, parent, false);
+ return new RecyclerViewHolder(itemView);
+ }
+
+ public View getView() {
+ return mView;
+ }
+
+ public T getView(int viewId) {
+ View view = mViews.get(viewId);
+ if (view == null) {
+ view = mView.findViewById(viewId);
+ mViews.put(viewId, view);
+ }
+ return (T) view;
+ }
+
+ public void setVisibility(int viewId, int visibility) {
+ View tv = getView(viewId);
+ tv.setVisibility(visibility);
+ }
+
+ public void setText(int viewId, String text) {
+ TextView tv = getView(viewId);
+ tv.setText(text);
+ }
+
+ public void setOnClickListener(int viewId, View.OnClickListener listener) {
+ View view = getView(viewId);
+ view.setOnClickListener(listener);
+ }
+
+ public void setOnClickListener(View.OnClickListener listener) {
+ View view = getView();
+ view.setOnClickListener(listener);
+ }
+}
diff --git a/tools/test_suite/android/core/src/main/java/com/openvela/bluetooth/callback/BleConnectCallback.java b/tools/test_suite/android/core/src/main/java/com/openvela/bluetooth/callback/BleConnectCallback.java
new file mode 100755
index 0000000..628421f
--- /dev/null
+++ b/tools/test_suite/android/core/src/main/java/com/openvela/bluetooth/callback/BleConnectCallback.java
@@ -0,0 +1,13 @@
+package com.openvela.bluetooth.callback;
+
+public abstract class BleConnectCallback {
+ public static final int FAILED_DEVICE_NOT_FOUND = 1000;
+ public static final int FAILED_TIMEOUT = 1001;
+
+ public void onConnectionChanged(String address, int newState) {}
+
+ public void onConnectFailed(String address, int errorCode) {}
+
+ public void onServicesDiscovered(String address) {}
+
+}
diff --git a/tools/test_suite/android/core/src/main/java/com/openvela/bluetooth/callback/BluetoothDiscoveryCallback.java b/tools/test_suite/android/core/src/main/java/com/openvela/bluetooth/callback/BluetoothDiscoveryCallback.java
new file mode 100755
index 0000000..e1759ab
--- /dev/null
+++ b/tools/test_suite/android/core/src/main/java/com/openvela/bluetooth/callback/BluetoothDiscoveryCallback.java
@@ -0,0 +1,18 @@
+package com.openvela.bluetooth.callback;
+
+public abstract class BluetoothDiscoveryCallback {
+ public static final int FAILED_ALREADY_STARTED = 1;
+ public static final int FAILED_APPLICATION_REGISTRATION_FAILED = 2;
+ public static final int FAILED_FEATURE_UNSUPPORTED = 4;
+ public static final int FAILED_INTERNAL_ERROR = 3;
+ public static final int FAILED_OUT_OF_HARDWARE_RESOURCES = 5;
+ public static final int FAILED_SCANNING_TOO_FREQUENTLY = 6;
+
+ public void onStart() {}
+
+ public void onStop() {}
+
+ public abstract void onDiscoveryResult(T device);
+
+ public void onDiscoveryFailed(int errorCode) {}
+}
diff --git a/tools/test_suite/android/core/src/main/java/com/openvela/bluetooth/callback/BluetoothStateCallback.java b/tools/test_suite/android/core/src/main/java/com/openvela/bluetooth/callback/BluetoothStateCallback.java
new file mode 100755
index 0000000..340723b
--- /dev/null
+++ b/tools/test_suite/android/core/src/main/java/com/openvela/bluetooth/callback/BluetoothStateCallback.java
@@ -0,0 +1,7 @@
+package com.openvela.bluetooth.callback;
+
+public interface BluetoothStateCallback {
+ void onEnabled();
+
+ void onDisabled();
+}
diff --git a/tools/test_suite/android/core/src/main/res/values/strings.xml b/tools/test_suite/android/core/src/main/res/values/strings.xml
new file mode 100755
index 0000000..1ebd5b4
--- /dev/null
+++ b/tools/test_suite/android/core/src/main/res/values/strings.xml
@@ -0,0 +1,3 @@
+
+ bluetoothtestlib
+
diff --git a/tools/test_suite/android/gradle.properties b/tools/test_suite/android/gradle.properties
new file mode 100755
index 0000000..3a131ca
--- /dev/null
+++ b/tools/test_suite/android/gradle.properties
@@ -0,0 +1,23 @@
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
+# AndroidX package structure to make it clearer which packages are bundled with the
+# Android operating system, and which are packaged with your app's APK
+# https://developer.android.com/topic/libraries/support-library/androidx-rn
+android.useAndroidX=true
+# Enables namespacing of each library's R class so that its R class includes only the
+# resources declared in the library itself and none from the library's dependencies,
+# thereby reducing the size of the R class for that library
+android.nonTransitiveRClass=true
+android.defaults.buildfeatures.buildconfig=true
+android.nonFinalResIds=false
\ No newline at end of file
diff --git a/tools/test_suite/android/gradle/wrapper/gradle-wrapper.jar b/tools/test_suite/android/gradle/wrapper/gradle-wrapper.jar
new file mode 100755
index 0000000..d64cd49
Binary files /dev/null and b/tools/test_suite/android/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/tools/test_suite/android/gradle/wrapper/gradle-wrapper.properties b/tools/test_suite/android/gradle/wrapper/gradle-wrapper.properties
new file mode 100755
index 0000000..09523c0
--- /dev/null
+++ b/tools/test_suite/android/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,7 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
+networkTimeout=10000
+validateDistributionUrl=true
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/tools/test_suite/android/gradlew b/tools/test_suite/android/gradlew
new file mode 100755
index 0000000..6e18c4d
--- /dev/null
+++ b/tools/test_suite/android/gradlew
@@ -0,0 +1,249 @@
+#!/bin/sh
+
+#
+# Copyright (C) 2024 Xiaomi Corporation.
+#
+# 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
+#
+# https://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.
+#
+
+##############################################################################
+#
+# Gradle start up script for POSIX generated by Gradle.
+#
+# Important for running:
+#
+# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+# noncompliant, but you have some other compliant shell such as ksh or
+# bash, then to run this script, type that shell name before the whole
+# command line, like:
+#
+# ksh Gradle
+#
+# Busybox and similar reduced shells will NOT work, because this script
+# requires all of these POSIX shell features:
+# * functions;
+# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+# * compound commands having a testable exit status, especially «case»;
+# * various built-in commands including «command», «set», and «ulimit».
+#
+# Important for patching:
+#
+# (2) This script targets any POSIX shell, so it avoids extensions provided
+# by Bash, Ksh, etc; in particular arrays are avoided.
+#
+# The "traditional" practice of packing multiple parameters into a
+# space-separated string is a well documented source of bugs and security
+# problems, so this is (mostly) avoided, by progressively accumulating
+# options in "$@", and eventually passing that to Java.
+#
+# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+# see the in-line comments for details.
+#
+# There are tweaks for specific operating systems such as AIX, CygWin,
+# Darwin, MinGW, and NonStop.
+#
+# (3) This script is generated from the Groovy template
+# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# within the Gradle project.
+#
+# You can find Gradle at https://github.com/gradle/gradle/.
+#
+##############################################################################
+
+# Attempt to set APP_HOME
+
+# Resolve links: $0 may be a link
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+ APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
+ [ -h "$app_path" ]
+do
+ ls=$( ls -ld "$app_path" )
+ link=${ls#*' -> '}
+ case $link in #(
+ /*) app_path=$link ;; #(
+ *) app_path=$APP_HOME$link ;;
+ esac
+done
+
+# This is normally unused
+# shellcheck disable=SC2034
+APP_BASE_NAME=${0##*/}
+# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
+APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD=maximum
+
+warn () {
+ echo "$*"
+} >&2
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+} >&2
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "$( uname )" in #(
+ CYGWIN* ) cygwin=true ;; #(
+ Darwin* ) darwin=true ;; #(
+ MSYS* | MINGW* ) msys=true ;; #(
+ NONSTOP* ) nonstop=true ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD=$JAVA_HOME/jre/sh/java
+ else
+ JAVACMD=$JAVA_HOME/bin/java
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD=java
+ if ! command -v java >/dev/null 2>&1
+ then
+ die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+fi
+
+# Increase the maximum file descriptors if we can.
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+ case $MAX_FD in #(
+ max*)
+ # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ MAX_FD=$( ulimit -H -n ) ||
+ warn "Could not query maximum file descriptor limit"
+ esac
+ case $MAX_FD in #(
+ '' | soft) :;; #(
+ *)
+ # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ ulimit -n "$MAX_FD" ||
+ warn "Could not set maximum file descriptor limit to $MAX_FD"
+ esac
+fi
+
+# Collect all arguments for the java command, stacking in reverse order:
+# * args from the command line
+# * the main class name
+# * -classpath
+# * -D...appname settings
+# * --module-path (only if needed)
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if "$cygwin" || "$msys" ; then
+ APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+ CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
+
+ JAVACMD=$( cygpath --unix "$JAVACMD" )
+
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ for arg do
+ if
+ case $arg in #(
+ -*) false ;; # don't mess with options #(
+ /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
+ [ -e "$t" ] ;; #(
+ *) false ;;
+ esac
+ then
+ arg=$( cygpath --path --ignore --mixed "$arg" )
+ fi
+ # Roll the args list around exactly as many times as the number of
+ # args, so each arg winds up back in the position where it started, but
+ # possibly modified.
+ #
+ # NB: a `for` loop captures its iteration list before it begins, so
+ # changing the positional parameters here affects neither the number of
+ # iterations, nor the values presented in `arg`.
+ shift # remove old arg
+ set -- "$@" "$arg" # push replacement arg
+ done
+fi
+
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Collect all arguments for the java command:
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
+# and any embedded shellness will be escaped.
+# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
+# treated as '${Hostname}' itself on the command line.
+
+set -- \
+ "-Dorg.gradle.appname=$APP_BASE_NAME" \
+ -classpath "$CLASSPATH" \
+ org.gradle.wrapper.GradleWrapperMain \
+ "$@"
+
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1
+then
+ die "xargs is not available"
+fi
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+# set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+ printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+ xargs -n1 |
+ sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+ tr '\n' ' '
+ )" '"$@"'
+
+exec "$JAVACMD" "$@"
diff --git a/tools/test_suite/android/gradlew.bat b/tools/test_suite/android/gradlew.bat
new file mode 100755
index 0000000..d7510e3
--- /dev/null
+++ b/tools/test_suite/android/gradlew.bat
@@ -0,0 +1,92 @@
+@rem
+@rem Copyright (C) 2024 Xiaomi Corporation.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%"=="" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%"=="" set DIRNAME=.
+@rem This is normally unused
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if %ERRORLEVEL% equ 0 goto execute
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if %ERRORLEVEL% equ 0 goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+set EXIT_CODE=%ERRORLEVEL%
+if %EXIT_CODE% equ 0 set EXIT_CODE=1
+if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
+exit /b %EXIT_CODE%
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/tools/test_suite/android/settings.gradle b/tools/test_suite/android/settings.gradle
new file mode 100755
index 0000000..31f7353
--- /dev/null
+++ b/tools/test_suite/android/settings.gradle
@@ -0,0 +1,19 @@
+pluginManagement {
+ repositories {
+ google()
+ mavenCentral()
+ gradlePluginPortal()
+ maven { url 'https://jitpack.io' }
+ }
+}
+dependencyResolutionManagement {
+ repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
+ repositories {
+ google()
+ mavenCentral()
+ maven { url 'https://jitpack.io' }
+ }
+}
+rootProject.name = "Bluetooth Test Suite"
+include ':app'
+include ':core'
\ No newline at end of file