Allow user to add multiple services & characteristics to GATT Server
Bug: 269390098
Test: build
Change-Id: I8da75986d0cc07399cf08207cdfd40378a835096
diff --git a/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/advertiser/AdvertiserFragment.kt b/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/advertiser/AdvertiserFragment.kt
index bef5649..248d2a9 100644
--- a/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/advertiser/AdvertiserFragment.kt
+++ b/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/advertiser/AdvertiserFragment.kt
@@ -17,6 +17,7 @@
package androidx.bluetooth.integration.testapp.ui.advertiser
// TODO(ofy) Migrate to androidx.bluetooth.BluetoothLe once Gatt Server API is in place
+// TODO(ofy) Migrate to androidx.bluetooth.GattService
import android.Manifest
import android.annotation.SuppressLint
import android.bluetooth.BluetoothGattCharacteristic
@@ -44,6 +45,8 @@
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
+import androidx.recyclerview.widget.DividerItemDecoration
+import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.tabs.TabLayout
import java.util.UUID
import kotlinx.coroutines.CoroutineScope
@@ -61,7 +64,7 @@
private lateinit var bluetoothLe: BluetoothLe
- // TODO(ofy) Migrate to androidx.bluetooth.BluetoothLe once scan API is in place
+ // TODO(ofy) Migrate to androidx.bluetooth.BluetoothLe once openGattServer API is in place
private lateinit var bluetoothLeExperimental: BluetoothLeExperimental
private var advertiseDataAdapter: AdvertiseDataAdapter? = null
@@ -91,6 +94,8 @@
_binding?.viewRecyclerViewOverlay?.isVisible = value
}
+ private var gattServerServicesAdapter: GattServerServicesAdapter? = null
+
private var isGattServerOpen: Boolean = false
set(value) {
field = value
@@ -182,6 +187,20 @@
}
}
+ binding.buttonAddService.setOnClickListener {
+ addGattService()
+ }
+
+ gattServerServicesAdapter =
+ GattServerServicesAdapter(
+ viewModel.gattServerServices,
+ ::addGattCharacteristic
+ )
+ binding.recyclerViewGattServerServices.adapter = gattServerServicesAdapter
+ binding.recyclerViewGattServerServices.addItemDecoration(
+ DividerItemDecoration(context, LinearLayoutManager.VERTICAL)
+ )
+
binding.buttonGattServer.setOnClickListener {
if (gattServerJob?.isActive == true) {
isGattServerOpen = false
@@ -340,26 +359,48 @@
}
}
- private fun openGattServer() {
- Log.d(TAG, "openGattServer() called")
+ private fun addGattService() {
+ // TODO(ofy) Show dialog for Service customization and replace sampleService
val sampleService =
BluetoothGattService(UUID.randomUUID(), BluetoothGattService.SERVICE_TYPE_PRIMARY)
+
+ viewModel.gattServerAddService(sampleService)
+
+ gattServerServicesAdapter?.notifyItemInserted(viewModel.gattServerServices.size - 1)
+ }
+
+ private fun addGattCharacteristic(bluetoothGattService: BluetoothGattService) {
+ // TODO(ofy) Show dialog for Characteristic customization and replace sampleCharacteristic
+
val sampleCharacteristic = BluetoothGattCharacteristic(
UUID.randomUUID(),
BluetoothGattCharacteristic.PROPERTY_READ,
BluetoothGattCharacteristic.PERMISSION_READ
)
- sampleService.addCharacteristic(sampleCharacteristic)
- val services = listOf(sampleService)
+ bluetoothGattService.addCharacteristic(sampleCharacteristic)
+
+ gattServerServicesAdapter?.notifyItemChanged(
+ viewModel.gattServerServices.indexOf(
+ bluetoothGattService
+ )
+ )
+ }
+
+ private fun openGattServer() {
+ Log.d(TAG, "openGattServer() called")
gattServerJob = gattServerScope.launch {
isGattServerOpen = true
- bluetoothLeExperimental.openGattServer(services).collect { gattServerCallback ->
- Log.d(TAG, "openGattServer() called with: gattServerCallback = $gattServerCallback")
- }
+ bluetoothLeExperimental.openGattServer(viewModel.gattServerServices)
+ .collect { gattServerCallback ->
+ Log.d(
+ TAG,
+ "openGattServer() called with: gattServerCallback = $gattServerCallback"
+ )
+ }
}
}
}
diff --git a/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/advertiser/AdvertiserViewModel.kt b/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/advertiser/AdvertiserViewModel.kt
index f0e4cdd..2109813 100644
--- a/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/advertiser/AdvertiserViewModel.kt
+++ b/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/advertiser/AdvertiserViewModel.kt
@@ -16,6 +16,8 @@
package androidx.bluetooth.integration.testapp.ui.advertiser
+// TODO(ofy) Migrate to androidx.bluetooth.GattService
+import android.bluetooth.BluetoothGattService
import androidx.bluetooth.AdvertiseParams
import androidx.lifecycle.ViewModel
import java.util.UUID
@@ -60,6 +62,8 @@
serviceUuids
)
+ val gattServerServices = mutableListOf<BluetoothGattService>()
+
fun removeAdvertiseDataAtIndex(index: Int) {
val manufacturerDataSize = manufacturerDatas.size
val serviceDataSize = serviceDatas.size
@@ -72,4 +76,8 @@
serviceUuids.removeAt(index - manufacturerDataSize - serviceDataSize)
}
}
+
+ fun gattServerAddService(bluetoothGattService: BluetoothGattService) {
+ gattServerServices.add(bluetoothGattService)
+ }
}
diff --git a/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/advertiser/GattServerCharacteristicsAdapter.kt b/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/advertiser/GattServerCharacteristicsAdapter.kt
new file mode 100644
index 0000000..57608e1
--- /dev/null
+++ b/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/advertiser/GattServerCharacteristicsAdapter.kt
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2023 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
+ *
+ * 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 androidx.bluetooth.integration.testapp.ui.advertiser
+
+import android.bluetooth.BluetoothGattCharacteristic
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.TextView
+import androidx.bluetooth.GattCharacteristic
+import androidx.bluetooth.integration.testapp.R
+import androidx.recyclerview.widget.RecyclerView
+
+class GattServerCharacteristicsAdapter(
+ private val characteristics: List<BluetoothGattCharacteristic>
+) : RecyclerView.Adapter<GattServerCharacteristicsAdapter.ViewHolder>() {
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
+ val view = LayoutInflater.from(parent.context)
+ .inflate(R.layout.item_gatt_server_characteristic, parent, false)
+ return ViewHolder(view)
+ }
+
+ override fun onBindViewHolder(holder: ViewHolder, position: Int) {
+ holder.bind(characteristics[position])
+ }
+
+ override fun getItemCount(): Int {
+ return characteristics.size
+ }
+
+ inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
+
+ private val textViewUuid: TextView = itemView.findViewById(R.id.text_view_uuid)
+ private val textViewProperties: TextView = itemView.findViewById(R.id.text_view_properties)
+
+ fun bind(characteristic: BluetoothGattCharacteristic) {
+ textViewUuid.text = characteristic.uuid.toString()
+
+ val properties = characteristic.properties
+ val context = itemView.context
+
+ val propertiesList = mutableListOf<String>()
+ // TODO(ofy) Update these with GattCharacteristic.isReadable, isWriteable, ...
+ if (properties.and(GattCharacteristic.PROPERTY_INDICATE) != 0) {
+ propertiesList.add(context.getString(R.string.indicate))
+ }
+ if (properties.and(GattCharacteristic.PROPERTY_NOTIFY) != 0) {
+ propertiesList.add(context.getString(R.string.notify))
+ }
+ val isReadable = properties.and(GattCharacteristic.PROPERTY_READ) != 0
+ if (isReadable) {
+ propertiesList.add(context.getString(R.string.read))
+ }
+ val isWriteable = (properties.and(GattCharacteristic.PROPERTY_WRITE) != 0 ||
+ properties.and(GattCharacteristic.PROPERTY_WRITE_NO_RESPONSE) != 0 ||
+ properties.and(GattCharacteristic.PROPERTY_SIGNED_WRITE) != 0)
+ if (isWriteable) {
+ propertiesList.add(context.getString(R.string.write))
+ }
+ textViewProperties.text = propertiesList.joinToString()
+ }
+ }
+}
diff --git a/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/advertiser/GattServerServicesAdapter.kt b/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/advertiser/GattServerServicesAdapter.kt
new file mode 100644
index 0000000..d2fd6c2
--- /dev/null
+++ b/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/advertiser/GattServerServicesAdapter.kt
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2023 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
+ *
+ * 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 androidx.bluetooth.integration.testapp.ui.advertiser
+
+import android.bluetooth.BluetoothGattService
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.Button
+import android.widget.TextView
+import androidx.bluetooth.integration.testapp.R
+import androidx.recyclerview.widget.RecyclerView
+
+class GattServerServicesAdapter(
+ private val services: List<BluetoothGattService>,
+ private val onClickAddCharacteristic: (BluetoothGattService) -> Unit
+) : RecyclerView.Adapter<GattServerServicesAdapter.ViewHolder>() {
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
+ val view = LayoutInflater.from(parent.context)
+ .inflate(R.layout.item_gatt_server_service, parent, false)
+ return ViewHolder(view)
+ }
+
+ override fun onBindViewHolder(holder: ViewHolder, position: Int) {
+ holder.bind(services[position])
+ }
+
+ override fun getItemCount(): Int {
+ return services.size
+ }
+
+ inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
+
+ private val textViewUuid: TextView = itemView.findViewById(R.id.text_view_uuid)
+ private val buttonAddCharacteristic: Button =
+ itemView.findViewById(R.id.button_add_characteristic)
+
+ private val recyclerViewServiceCharacteristic: RecyclerView =
+ itemView.findViewById(R.id.recycler_view_service_characteristic)
+
+ private var currentBluetoothGattService: BluetoothGattService? = null
+
+ init {
+ buttonAddCharacteristic.setOnClickListener {
+ currentBluetoothGattService?.let(onClickAddCharacteristic)
+ }
+ }
+
+ fun bind(bluetoothGattService: BluetoothGattService) {
+ currentBluetoothGattService = bluetoothGattService
+
+ textViewUuid.text = bluetoothGattService.uuid.toString()
+
+ recyclerViewServiceCharacteristic.adapter = GattServerCharacteristicsAdapter(
+ bluetoothGattService.characteristics
+ )
+ }
+ }
+}
diff --git a/bluetooth/integration-tests/testapp/src/main/res/layout/fragment_advertiser.xml b/bluetooth/integration-tests/testapp/src/main/res/layout/fragment_advertiser.xml
index 024d732..4181f87 100644
--- a/bluetooth/integration-tests/testapp/src/main/res/layout/fragment_advertiser.xml
+++ b/bluetooth/integration-tests/testapp/src/main/res/layout/fragment_advertiser.xml
@@ -164,6 +164,37 @@
app:layout_constraintTop_toBottomOf="@+id/tab_layout"
tools:visibility="visible">
+ <androidx.core.widget.NestedScrollView
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ app:layout_constraintTop_toBottomOf="parent"
+ app:layout_constraintTop_toTopOf="parent">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <androidx.recyclerview.widget.RecyclerView
+ android:id="@+id/recycler_view_gatt_server_services"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ app:layoutManager="LinearLayoutManager"
+ tools:itemCount="1"
+ tools:listitem="@layout/item_gatt_server_service" />
+
+ <Button
+ android:id="@+id/button_add_service"
+ style="@style/Widget.MaterialComponents.Button.TextButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_margin="16dp"
+ android:text="@string/add_service" />
+
+ </LinearLayout>
+
+ </androidx.core.widget.NestedScrollView>
+
<Button
android:id="@+id/button_gatt_server"
android:layout_width="wrap_content"
diff --git a/bluetooth/integration-tests/testapp/src/main/res/layout/item_gatt_server_characteristic.xml b/bluetooth/integration-tests/testapp/src/main/res/layout/item_gatt_server_characteristic.xml
new file mode 100644
index 0000000..64bb680
--- /dev/null
+++ b/bluetooth/integration-tests/testapp/src/main/res/layout/item_gatt_server_characteristic.xml
@@ -0,0 +1,76 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2023 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
+
+ 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.
+ -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:padding="8dp">
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/uuid"
+ android:textAllCaps="true" />
+
+ <TextView
+ android:id="@+id/text_view_uuid"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="4dp"
+ android:textColor="@color/black"
+ tools:text="0x1800" />
+
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/properties" />
+
+ <TextView
+ android:id="@+id/text_view_properties"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="4dp"
+ android:textAllCaps="true"
+ android:textColor="@color/black"
+ tools:text="@string/read" />
+
+ </LinearLayout>
+
+ <!-- TODO(ofy) Add Descriptor -->
+
+ <!-- <Button-->
+ <!-- android:id="@+id/button_add_descriptor"-->
+ <!-- style="@style/Widget.MaterialComponents.Button.TextButton"-->
+ <!-- android:layout_width="wrap_content"-->
+ <!-- android:layout_height="wrap_content"-->
+ <!-- android:layout_marginStart="16dp"-->
+ <!-- android:text="@string/add_descriptor" />-->
+
+</LinearLayout>
diff --git a/bluetooth/integration-tests/testapp/src/main/res/layout/item_gatt_server_service.xml b/bluetooth/integration-tests/testapp/src/main/res/layout/item_gatt_server_service.xml
new file mode 100644
index 0000000..bbb6557
--- /dev/null
+++ b/bluetooth/integration-tests/testapp/src/main/res/layout/item_gatt_server_service.xml
@@ -0,0 +1,76 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2023 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
+
+ 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.
+ -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:padding="8dp">
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/generic_attribute"
+ android:textColor="@color/black"
+ android:textStyle="bold" />
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/uuid"
+ android:textAllCaps="true" />
+
+ <TextView
+ android:id="@+id/text_view_uuid"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="4dp"
+ android:textColor="@color/black"
+ tools:text="0x1800" />
+
+ </LinearLayout>
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/primary_service"
+ android:textAllCaps="true" />
+
+ <androidx.recyclerview.widget.RecyclerView
+ android:id="@+id/recycler_view_service_characteristic"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="16dp"
+ app:layoutManager="LinearLayoutManager"
+ tools:itemCount="3"
+ tools:listitem="@layout/item_gatt_server_characteristic" />
+
+ <Button
+ android:id="@+id/button_add_characteristic"
+ style="@style/Widget.MaterialComponents.Button.TextButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="16dp"
+ android:text="@string/add_characteristic" />
+
+</LinearLayout>
diff --git a/bluetooth/integration-tests/testapp/src/main/res/values/donottranslate-strings.xml b/bluetooth/integration-tests/testapp/src/main/res/values/donottranslate-strings.xml
index 06dffe7..fa1ad57 100644
--- a/bluetooth/integration-tests/testapp/src/main/res/values/donottranslate-strings.xml
+++ b/bluetooth/integration-tests/testapp/src/main/res/values/donottranslate-strings.xml
@@ -67,6 +67,9 @@
<!-- GATT Server -->
<string name="gatt_server">Gatt Server</string>
+ <string name="add_service">Add Service</string>
+ <string name="add_characteristic">Add Characteristic</string>
+ <string name="add_descriptor">Add Descriptor</string>
<string name="open_gatt_server">Open Gatt Server</string>
<string name="stop_gatt_server">Stop Gatt Server</string>
</resources>