diff --git a/bluetoothle/src/main/java/com/sample/android/bluetoothle/kotlin/BluetoothLeService.kt b/bluetoothle/src/main/java/com/sample/android/bluetoothle/kotlin/BluetoothLeService.kt new file mode 100644 index 000000000..b8d281d7b --- /dev/null +++ b/bluetoothle/src/main/java/com/sample/android/bluetoothle/kotlin/BluetoothLeService.kt @@ -0,0 +1,331 @@ +/* + * Copyright 2026 The Android Open Source Project + * + * 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. + */ + +package com.sample.android.bluetoothle.kotlin + +import android.Manifest +import android.annotation.SuppressLint +import android.app.Service +import android.bluetooth.BluetoothAdapter +import android.bluetooth.BluetoothDevice +import android.bluetooth.BluetoothGatt +import android.bluetooth.BluetoothGattCallback +import android.bluetooth.BluetoothProfile +import android.content.Intent +import android.os.Binder +import android.os.IBinder +import android.util.Log +import androidx.annotation.RequiresPermission + +private const val TAG = "BluetoothLeService" + +@SuppressLint("MissingPermission") +// [START android_bluetooth_service_all] +// [START android_bluetooth_binder] +class BluetoothLeService : Service() { + + private val binder = LocalBinder() + // [START_EXCLUDE silent] + + private var bluetoothAdapter: BluetoothAdapter? = null + private var bluetoothGatt: BluetoothGatt? = null + private var connectionState = STATE_DISCONNECTED + + companion object { + const val ACTION_GATT_CONNECTED = + "com.example.bluetooth.le.ACTION_GATT_CONNECTED" + const val ACTION_GATT_DISCONNECTED = + "com.example.bluetooth.le.ACTION_GATT_DISCONNECTED" + + private const val STATE_DISCONNECTED = 0 + private const val STATE_CONNECTED = 2 + } + + private val bluetoothGattCallback = object : BluetoothGattCallback() { + override fun onConnectionStateChange(gatt: BluetoothGatt?, status: Int, newState: Int) { + if (newState == BluetoothProfile.STATE_CONNECTED) { + // successfully connected to the GATT Server + connectionState = STATE_CONNECTED + broadcastUpdate(ACTION_GATT_CONNECTED) + } else if (newState == BluetoothProfile.STATE_DISCONNECTED) { + // disconnected from the GATT Server + connectionState = STATE_DISCONNECTED + broadcastUpdate(ACTION_GATT_DISCONNECTED) + } + } + } + // [END_EXCLUDE] + + override fun onBind(intent: Intent): IBinder? { + return binder + } + + inner class LocalBinder : Binder() { + fun getService(): BluetoothLeService { + return this@BluetoothLeService + } + } + // [START_EXCLUDE silent] + + // In BluetoothLeService + fun initialize(): Boolean { + bluetoothAdapter = BluetoothAdapter.getDefaultAdapter() + if (bluetoothAdapter == null) { + Log.e(TAG, "Unable to obtain a BluetoothAdapter.") + return false + } + return true + } + + @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) + fun connect(address: String): Boolean { + bluetoothAdapter?.let { adapter -> + try { + val device = adapter.getRemoteDevice(address) + // connect to the GATT server on the device + bluetoothGatt = device.connectGatt(this, false, bluetoothGattCallback) + return true + } catch (exception: IllegalArgumentException) { + Log.w(TAG, "Device not found with provided address. Unable to connect.") + return false + } + } ?: run { + Log.w(TAG, "BluetoothAdapter not initialized") + return false + } + } + + // [START android_bluetooth_broadcast] + private fun broadcastUpdate(action: String) { + val intent = Intent(action) + sendBroadcast(intent) + } + // [END android_bluetooth_broadcast] + + @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) + override fun onUnbind(intent: Intent?): Boolean { + close() + return super.onUnbind(intent) + } + + private fun close() { + bluetoothGatt?.let { gatt -> + gatt.close() + bluetoothGatt = null + } + } + // [END_EXCLUDE] +} +// [END android_bluetooth_binder] + +// [END android_bluetooth_service_all] + +/** + * Namespaces for simplified versions of BluetoothLeService to match documentation. + */ +private object ConnectSimpleNamespace { + class BluetoothLeService { + private var bluetoothAdapter: BluetoothAdapter? = null + private val TAG = "BluetoothLeService" + + // [START android_bluetooth_connect_simple] + fun connect(address: String): Boolean { + bluetoothAdapter?.let { adapter -> + try { + val device = adapter.getRemoteDevice(address) + } catch (exception: IllegalArgumentException) { + Log.w(TAG, "Device not found with provided address.") + return false + } + // connect to the GATT server on the device + return true + } ?: run { + Log.w(TAG, "BluetoothAdapter not initialized") + return false + } + } + // [END android_bluetooth_connect_simple] + } +} + +private object CallbackSimpleNamespace { + class BluetoothLeService { + // [START android_bluetooth_callback_simple] + private val bluetoothGattCallback = object : BluetoothGattCallback() { + override fun onConnectionStateChange(gatt: BluetoothGatt?, status: Int, newState: Int) { + if (newState == BluetoothProfile.STATE_CONNECTED) { + // successfully connected to the GATT Server + } else if (newState == BluetoothProfile.STATE_DISCONNECTED) { + // disconnected from the GATT Server + } + } + } + // [END android_bluetooth_callback_simple] + } +} + +private object InitializeNamespace { + // [START android_bluetooth_initialize] + private const val TAG = "BluetoothLeService" + + class BluetoothLeService : Service() { + + private var bluetoothAdapter: BluetoothAdapter? = null + + fun initialize(): Boolean { + bluetoothAdapter = BluetoothAdapter.getDefaultAdapter() + if (bluetoothAdapter == null) { + Log.e(TAG, "Unable to obtain a BluetoothAdapter.") + return false + } + return true + } + + // [START_EXCLUDE] + override fun onBind(intent: Intent): IBinder? { + return null + } + // [END_EXCLUDE] + } + // [END android_bluetooth_initialize] +} + +@SuppressLint("MissingPermission") +private object ConnectGattNamespace { + class BluetoothLeService : Service() { + // [START android_bluetooth_connect_gatt] + // [START_EXCLUDE silent] + fun dummy(device: BluetoothDevice, bluetoothGattCallback: BluetoothGattCallback) { + // [END_EXCLUDE] + var bluetoothGatt: BluetoothGatt? = null + // ... + bluetoothGatt = device.connectGatt(this, false, bluetoothGattCallback) + // [START_EXCLUDE silent] + } + // [END_EXCLUDE] + // [END android_bluetooth_connect_gatt] + override fun onBind(intent: Intent): IBinder? = null + } +} + +@SuppressLint("MissingPermission") +private object ConnectNamespace { + // [START android_bluetooth_connect] + class BluetoothLeService : Service() { + + // [START_EXCLUDE silent] + private var bluetoothAdapter: BluetoothAdapter? = null + private val bluetoothGattCallback = object : BluetoothGattCallback() {} + private val TAG = "BluetoothLeService" + // [END_EXCLUDE] + // [START_EXCLUDE] + override fun onBind(intent: Intent): IBinder? = null + fun dummy() {} + // [END_EXCLUDE] + private var bluetoothGatt: BluetoothGatt? = null + + // [START_EXCLUDE] + fun dummy2() {} + // [END_EXCLUDE] + fun connect(address: String): Boolean { + bluetoothAdapter?.let { adapter -> + try { + val device = adapter.getRemoteDevice(address) + // connect to the GATT server on the device + bluetoothGatt = device.connectGatt(this, false, bluetoothGattCallback) + return true + } catch (exception: IllegalArgumentException) { + Log.w(TAG, "Device not found with provided address. Unable to connect.") + return false + } + } ?: run { + Log.w(TAG, "BluetoothAdapter not initialized") + return false + } + } + } + // [END android_bluetooth_connect] +} + +@SuppressLint("MissingPermission") +private object CallbackNamespace { + // [START android_bluetooth_callback] + class BluetoothLeService : Service() { + + // [START_EXCLUDE silent] + private val binder = LocalBinder() + private var bluetoothAdapter: BluetoothAdapter? = null + private var bluetoothGatt: BluetoothGatt? = null + private fun broadcastUpdate(action: String) {} + override fun onBind(intent: Intent): IBinder? = null + inner class LocalBinder : Binder() + // [END_EXCLUDE] + private var connectionState = STATE_DISCONNECTED + + private val bluetoothGattCallback = object : BluetoothGattCallback() { + override fun onConnectionStateChange(gatt: BluetoothGatt?, status: Int, newState: Int) { + if (newState == BluetoothProfile.STATE_CONNECTED) { + // successfully connected to the GATT Server + connectionState = STATE_CONNECTED + broadcastUpdate(ACTION_GATT_CONNECTED) + } else if (newState == BluetoothProfile.STATE_DISCONNECTED) { + // disconnected from the GATT Server + connectionState = STATE_DISCONNECTED + broadcastUpdate(ACTION_GATT_DISCONNECTED) + } + } + } + + // [START_EXCLUDE] + fun dummy() {} + // [END_EXCLUDE] + companion object { + const val ACTION_GATT_CONNECTED = + "com.example.bluetooth.le.ACTION_GATT_CONNECTED" + const val ACTION_GATT_DISCONNECTED = + "com.example.bluetooth.le.ACTION_GATT_DISCONNECTED" + + private const val STATE_DISCONNECTED = 0 + private const val STATE_CONNECTED = 2 + } + } + // [END android_bluetooth_callback] +} + +@SuppressLint("MissingPermission") +private object CloseNamespace { + // [START android_bluetooth_close] + class BluetoothLeService : Service() { + + // [START_EXCLUDE] + private var bluetoothGatt: BluetoothGatt? = null + override fun onBind(intent: Intent): IBinder? = null + // [END_EXCLUDE] + override fun onUnbind(intent: Intent?): Boolean { + close() + return super.onUnbind(intent) + } + + private fun close() { + bluetoothGatt?.let { gatt -> + gatt.close() + bluetoothGatt = null + } + } + } + // [END android_bluetooth_close] +} + diff --git a/bluetoothle/src/main/java/com/sample/android/bluetoothle/kotlin/DeviceControlActivity.kt b/bluetoothle/src/main/java/com/sample/android/bluetoothle/kotlin/DeviceControlActivity.kt new file mode 100644 index 000000000..7c50d38fb --- /dev/null +++ b/bluetoothle/src/main/java/com/sample/android/bluetoothle/kotlin/DeviceControlActivity.kt @@ -0,0 +1,301 @@ +/* + * Copyright 2026 The Android Open Source Project + * + * 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. + */ + +package com.sample.android.bluetoothle.kotlin + +import android.Manifest +import android.annotation.SuppressLint +import android.content.BroadcastReceiver +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.content.ServiceConnection +import android.os.Bundle +import android.os.IBinder +import android.util.Log +import androidx.annotation.RequiresPermission +import androidx.appcompat.app.AppCompatActivity +import com.sample.android.bluetoothle.R +import com.sample.android.bluetoothle.kotlin.BluetoothLeService.LocalBinder + +private const val TAG = "DeviceControlActivity" + +// [START android_bluetooth_activity_all] +// [START android_bluetooth_bind_service] +class DeviceControlActivity : AppCompatActivity() { + + private var bluetoothService: BluetoothLeService? = null + // [START_EXCLUDE silent] + private var deviceAddress: String? = null + private var connected = false + // [END_EXCLUDE] + // [START android_bluetooth_service_connection] + // Code to manage Service lifecycle. + private val serviceConnection: ServiceConnection = object : ServiceConnection { + // [START_EXCLUDE silent] + @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) + // [END_EXCLUDE] + override fun onServiceConnected( + componentName: ComponentName, + service: IBinder + ) { + bluetoothService = (service as LocalBinder).getService() + bluetoothService?.let { bluetooth -> + // call functions on service to check connection and connect to devices + // [START_EXCLUDE silent] + if (!bluetooth.initialize()) { + Log.e(TAG, "Unable to initialize Bluetooth") + finish() + return@let + } + // perform device connection + deviceAddress?.let { bluetooth.connect(it) } + // [END_EXCLUDE] + } + } + + override fun onServiceDisconnected(componentName: ComponentName) { + bluetoothService = null + } + } + // [END android_bluetooth_service_connection] + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.gatt_services_characteristics) + // [START_EXCLUDE silent] + + deviceAddress = intent.getStringExtra("EXTRAS_DEVICE_ADDRESS") + // [END_EXCLUDE] + + val gattServiceIntent = Intent(this, BluetoothLeService::class.java) + bindService(gattServiceIntent, serviceConnection, Context.BIND_AUTO_CREATE) + } + // [START_EXCLUDE silent] + + private val gattUpdateReceiver: BroadcastReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + when (intent.action) { + BluetoothLeService.ACTION_GATT_CONNECTED -> { + connected = true + updateConnectionState(R.string.connected) + } + BluetoothLeService.ACTION_GATT_DISCONNECTED -> { + connected = false + updateConnectionState(R.string.disconnected) + } + } + } + } + + @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) + override fun onResume() { + super.onResume() + registerReceiver(gattUpdateReceiver, makeGattUpdateIntentFilter()) + bluetoothService?.let { service -> + deviceAddress?.let { address -> + val result = service.connect(address) + Log.d(TAG, "Connect request result=$result") + } + } + } + + override fun onPause() { + super.onPause() + unregisterReceiver(gattUpdateReceiver) + } + private fun makeGattUpdateIntentFilter(): IntentFilter { + return IntentFilter().apply { + addAction(BluetoothLeService.ACTION_GATT_CONNECTED) + addAction(BluetoothLeService.ACTION_GATT_DISCONNECTED) + } + } + + private fun updateConnectionState(resourceId: Int) { + // Placeholder implementation + } + // [END_EXCLUDE] +} +// [END android_bluetooth_bind_service] +// [END android_bluetooth_activity_all] + +/** + * Namespaces for progressive versions of ServiceConnection to match documentation. + */ +private object ServiceConnectionSimpleNamespace { + class DeviceControlActivity { + private var bluetoothService: BluetoothLeService? = null + + // [START android_bluetooth_service_connection_simple] + // Code to manage Service lifecycle. + val serviceConnection: ServiceConnection = object : ServiceConnection { + override fun onServiceConnected( + componentName: ComponentName, + service: IBinder + ) { + bluetoothService = (service as LocalBinder).getService() + bluetoothService?.let { bluetooth -> + // call functions on service to check connection and connect to devices + } + } + + override fun onServiceDisconnected(componentName: ComponentName) { + bluetoothService = null + } + } + // [END android_bluetooth_service_connection_simple] + } +} + +private object ServiceConnectionInitializeNamespace { + // [START android_bluetooth_service_connection_initialize] + class DeviceControlActivity : AppCompatActivity() { + + // [START_EXCLUDE silent] + private var bluetoothService: BluetoothLeService? = null + private val TAG = "DeviceControlActivity" + // [END_EXCLUDE] + // Code to manage Service lifecycle. + private val serviceConnection: ServiceConnection = object : ServiceConnection { + override fun onServiceConnected( + componentName: ComponentName, + service: IBinder + ) { + bluetoothService = (service as LocalBinder).getService() + bluetoothService?.let { bluetooth -> + if (!bluetooth.initialize()) { + Log.e(TAG, "Unable to initialize Bluetooth") + finish() + } + // perform device connection + } + } + + override fun onServiceDisconnected(componentName: ComponentName) { + bluetoothService = null + } + } + + // [START_EXCLUDE] + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + } + // [END_EXCLUDE] + } + // [END android_bluetooth_service_connection_initialize] +} + +@SuppressLint("MissingPermission") +private object ServiceConnectionConnectNamespace { + class DeviceControlActivity : AppCompatActivity() { + + private var bluetoothService: BluetoothLeService? = null + private var deviceAddress: String? = null + private val TAG = "DeviceControlActivity" + + // [START android_bluetooth_service_connection_connect] + // Code to manage Service lifecycle. + private val serviceConnection: ServiceConnection = object : ServiceConnection { + // [START_EXCLUDE silent] + @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) + // [END_EXCLUDE] + override fun onServiceConnected( + componentName: ComponentName, + service: IBinder + ) { + bluetoothService = (service as LocalBinder).getService() + bluetoothService?.let { bluetooth -> + if (!bluetooth.initialize()) { + Log.e(TAG, "Unable to initialize Bluetooth") + finish() + } + // perform device connection + deviceAddress?.let { bluetooth.connect(it) } + } + } + + override fun onServiceDisconnected(componentName: ComponentName) { + bluetoothService = null + } + } + // [END android_bluetooth_service_connection_connect] + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + } + } +} + +@SuppressLint("MissingPermission") +private object UpdateReceiverNamespace { + // [START android_bluetooth_update_receiver] + class DeviceControlActivity : AppCompatActivity() { + + // [START_EXCLUDE silent] + private var bluetoothService: BluetoothLeService? = null + private var deviceAddress: String? = null + private var connected = false + private val TAG = "DeviceControlActivity" + private fun updateConnectionState(resourceId: Int) {} + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + } + // [END_EXCLUDE] + private val gattUpdateReceiver: BroadcastReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + when (intent.action) { + BluetoothLeService.ACTION_GATT_CONNECTED -> { + connected = true + updateConnectionState(R.string.connected) + } + BluetoothLeService.ACTION_GATT_DISCONNECTED -> { + connected = false + updateConnectionState(R.string.disconnected) + } + } + } + } + + // [START_EXCLUDE silent] + @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) + // [END_EXCLUDE] + override fun onResume() { + super.onResume() + registerReceiver(gattUpdateReceiver, makeGattUpdateIntentFilter()) + bluetoothService?.let { service -> + deviceAddress?.let { address -> + val result = service.connect(address) + Log.d(TAG, "Connect request result=$result") + } + } + } + + override fun onPause() { + super.onPause() + unregisterReceiver(gattUpdateReceiver) + } + + private fun makeGattUpdateIntentFilter(): IntentFilter { + return IntentFilter().apply { + addAction(BluetoothLeService.ACTION_GATT_CONNECTED) + addAction(BluetoothLeService.ACTION_GATT_DISCONNECTED) + } + } + } + // [END android_bluetooth_update_receiver] +} + diff --git a/bluetoothle/src/main/res/layout/gatt_services_characteristics.xml b/bluetoothle/src/main/res/layout/gatt_services_characteristics.xml new file mode 100644 index 000000000..14140aada --- /dev/null +++ b/bluetoothle/src/main/res/layout/gatt_services_characteristics.xml @@ -0,0 +1,21 @@ + + + + diff --git a/bluetoothle/src/main/res/values/strings.xml b/bluetoothle/src/main/res/values/strings.xml index fa7796f20..1ebd1bd3b 100644 --- a/bluetoothle/src/main/res/values/strings.xml +++ b/bluetoothle/src/main/res/values/strings.xml @@ -14,4 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. --> - + + Connected + Disconnected +