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 @@ + + + + + + + +