Android Ble Bluetooth App (4) UI optimization and descriptor

foreword

  In the previous article, we learned about the characteristics and attributes, and at the same time displayed the characteristics and attributes under the Bluetooth service of the device. In this article, we need to use these characteristics and attributes to complete some functions.

Table of contents

text

  Before developing specific functions, UI optimization is also essential. Taking the main page as an example, it is rather strange at present. If you are using this app for the first time, you may be very confused. use? Based on this point, we add a layout to optimize the following prompts on the main page.

1. UI optimization

① Main page prompt optimization

  First create a new one under layout lay_empty_ble.xml, the code inside is as follows:

<?xml version="1.0" encoding="utf-8"?>
<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="match_parent"
    android:gravity="center"
    android:orientation="vertical"
    tools:ignore="UseCompoundDrawables">

    <ImageView
        android:id="@+id/imageView"
        android:layout_width="100dp"
        android:layout_height="100dp"
        app:srcCompat="@drawable/ic_empty_ble" />

    <TextView
        android:id="@+id/textView2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:padding="24dp"
        android:text="暂无蓝牙设备,请点击页面左上角蓝牙图标按钮,进入扫描页面,选择设备连接后进行操作"
        android:textColor="@color/gray"
        android:textSize="16sp" />
</LinearLayout>

An icon is used here, ic_empty_ble.xml, and the code is as follows:

<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="100dp"
    android:height="100dp"
    android:viewportWidth="1024"
    android:viewportHeight="1024">
    <path
        android:fillColor="#e6e6e6"
        android:pathData="M997.9,516.6a488.4,488.4 0,0 1,-489.5 488.2,488.4 488.4,0 0,1 -489.5,-490.8C19.9,242.3 240.1,23.8 511,25.8a488.4,488.4 0,0 1,486.9 490.8zM534.7,726l99.2,-74.9 -99.2,-78.7v153.7zM633.9,379.4l-98.9,-73.2v151.8L633.9,379.4zM561.3,515.2l130.3,-103.6c29.2,-23.3 29.8,-42.8 0.8,-64.7 -54.2,-40.9 -108.3,-81.8 -163.8,-120.9 -10.2,-7.3 -29.4,-12.1 -39.1,-7.3 -9.6,4.9 -17.1,23.1 -17.5,35.7 -1.8,54.8 -0.8,109.7 -0.9,164.6 0,6.7 -1,13.3 -1.9,23.9L339.3,341l-37.4,46.4 160.3,127.8c-40.6,32.3 -79.9,62.5 -118,94.1 -45.2,37.4 -52.4,22.8 -6.9,77.9 0.9,1 2.9,1.1 4.4,1.6l129.5,-101.2c0,69 -0.9,131.3 0.9,193.4 0.3,11.8 8.7,27.9 18.4,33.6 7.8,4.6 25.6,0.3 34.7,-6.2a7693.4,7693.4 0,0 0,167.1 -124.8c29.3,-22.3 28.8,-41.2 -0.7,-64.8 -42.6,-34.2 -85.4,-68.1 -130.2,-103.7z" />
</vector>

Then use it in activity_main.xml, modify the code as follows:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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="match_parent"
    tools:context=".MainActivity">

    <com.google.android.material.appbar.MaterialToolbar
        android:id="@+id/toolbar"
        android:layout_width="match_parent"
        android:layout_height="?attr/actionBarSize"
        android:background="@color/orange"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:navigationIcon="@drawable/ic_scan_ble"
        app:title="GoodBle"
        app:titleCentered="true"
        app:titleTextColor="@color/white">

        <TextView
            android:id="@+id/tv_disconnect"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="end"
            android:layout_marginEnd="8dp"
            android:padding="8dp"
            android:text="断开连接"
            android:textColor="@color/white"
            android:visibility="gone" />
    </com.google.android.material.appbar.MaterialToolbar>

    <TextView
        android:id="@+id/tv_device_info"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:padding="16dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/toolbar" />

    <include
        android:id="@+id/empty_ble_lay"
        layout="@layout/lay_empty_ble"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_below="@id/toolbar"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/toolbar" />

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/rv_service"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/tv_device_info" />

</androidx.constraintlayout.widget.ConstraintLayout>

The preview effect is shown in the figure below:
insert image description here
  the service list is blank when there is no data, so let’s modify the code in MainActivity, because it is displayed by default, so you can hide this layout after connecting the Bluetooth device to discover the service, modify as follows As shown in the figure:

insert image description here

Then, when disconnecting the callback, display an empty content layout, clear the service list, refresh the adapter, and modify the code as follows:

    override fun onConnectionStateChange(state: Boolean) {
    
    
        runOnUiThread {
    
    
            if (state) binding.tvDisconnect.visibility = View.VISIBLE
            else {
    
    
                binding.emptyBleLay.root.visibility = View.VISIBLE
                mServiceList.clear()
                mServiceAdapter?.notifyDataSetChanged()
            }
        }
    }

Run it below:

insert image description here

② UUID display optimization

  In this way, it seems that the main page will not appear monotonous when there is no device information. Then there is another small detail. When the Bluetooth service and characteristics of the device are not defined by SIG but customized by the manufacturer, we'd better display the completed one. UUID, for the convenience of use, create a new BleConstant class under the ble package, the code is as follows:

object BleConstant {
    
    

    const val APP_NAME = "GoodBle"

    const val UNKNOWN_DEVICE = "Unknown device"

    const val UNKNOWN_SERVICE = "Unknown Service"

    const val UNKNOWN_CHARACTERISTICS = "Unknown Characteristics"

    const val UNKNOWN_DESCRIPTOR = "Unknown Descriptor"

    const val BROADCAST = "Broadcast"

    const val READ = "Read"

    const val WRITE_NO_RESPONSE = "Write No Response"

    const val WRITE = "Write"

    const val NOTIFY = "Notify"

    const val INDICATE = "Indicate"

    const val AUTHENTICATED_SIGNED_WRITES = "Authenticated Signed Writes"

    const val EXTENDED_PROPERTIES = "Extended Properties"
}

Some constants are defined here, including unknown services, unknown characteristics, and some other attributes. In this way, it is enough to modify a constant when modifying. Next, let's modify the value of the else in BleUtils and the value of the function's constant in the constant name, and the rest can be modified in the service adapter and feature adapter, first of all, the service adapter, getServiceName()modifygetCharacteristicsName()

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
    
    
        val serviceName = BleUtils.getServiceName(services[position].uuid)
        holder.binding.tvServiceName.text = serviceName
        holder.binding.tvServiceUuid.text = if (serviceName != UNKNOWN_SERVICE) BleUtils.getShortUUID(services[position].uuid) else services[position].uuid.toString()
        ...
    }

services[position].uuid.toString()It is the complete uuid of the service, which is lowercase by default, and you can also change it to uppercase.

Then change the same feature adapter:

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
    
    
        val characteristicName = BleUtils.getCharacteristicsName(characteristics[position].uuid)
        holder.binding.tvCharacterName.text = characteristicName
        holder.binding.tvCharacterUuid.text = if (characteristicName != UNKNOWN_CHARACTERISTICS) BleUtils.getShortUUID(characteristics[position].uuid) else characteristics[position].uuid.toString()
        ...
    }

Run it again, and the UUID for unknown device services and characteristics will display the completed values.

③ Equipment information optimization

  After the device is currently connected, we can see the service information of the device, but we don’t know which device it is. There is no place to display the device information. The most important thing is of course the device name and Mac address. Here we can optimize it. First, modify it. The content of the control activity_main.xmlin the following MaterialToolbar, because I want to use the device name as the title, and the device Mac address as the subtitle, so add two subtitle attribute values:

    <com.google.android.material.appbar.MaterialToolbar
        ...
        app:subtitleCentered="true"
        app:subtitleTextColor="@color/white">

Here we set the display position and text color of the subtitle respectively. The next thing to do is to get the device and add a function in BleCore. The code is as follows:

	fun getDevice() = mGatt?.device

Then back to MainActivity, you only need to modify the code in the callback function of the connection state, as shown below:

    @SuppressLint("MissingPermission")
    override fun onConnectionStateChange(state: Boolean) {
    
    
        runOnUiThread {
    
    
            if (state) {
    
    
                binding.tvDisconnect.visibility = View.VISIBLE
                bleCore.getDevice()?.apply {
    
    
                    binding.toolbar.title = name ?: UNKNOWN_DEVICE
                    binding.toolbar.subtitle = address
                }
            }
            else {
    
    
                binding.toolbar.title = APP_NAME
                binding.toolbar.subtitle = ""
                binding.tvDeviceInfo.text = ""
                binding.emptyBleLay.root.visibility = View.VISIBLE
                mServiceList.clear()
                mServiceAdapter?.notifyDataSetChanged()
            }
        }
    }

Let's run it and see:
insert image description here

2. Description

  Features and attributes were mentioned in the previous article. What functions of features are determined by attributes, so what does the description do?

① concept

In Bluetooth Low Energy (BLE), a Descriptor is a data structure used to provide additional information about a characteristic value. Descriptor provides a more detailed description and configuration options for a particular trait. Descriptor is a sub-item of Characteristics (Characteristics), which is used to describe specific attributes or behaviors of characteristics. Each feature can have one or more Descriptors.

Here are some common BLE Descriptor types and their meanings:

  1. Declaration Descriptor: This Descriptor is used to describe the declaration information of the characteristic, including the unique identifier of the characteristic, permission, value format and other signs. It provides basic information about characteristics for other devices to understand.

  2. User Description (User Description) Descriptor: used to provide a human-readable description of the feature. This description can be the feature's name, label, or other descriptive text about the feature.

  3. Configuration Descriptor: Configuration options used to describe characteristics. This Descriptor can contain optional settings for the characteristic, such as sampling rate, measurement unit or threshold, etc.

  4. Notification Descriptor: Used to configure whether the feature supports the notification function. This Descriptor can be used to enable the device to receive notifications of characteristic value changes.

  5. Linear Interval Descriptor: Used to describe the linear relationship of feature values, such as numerical range and step size.

  6. Client configuration Descriptor: It is used to allow remote devices (such as central devices) to subscribe to the change notification of characteristic values, which is very important.
    These are just some examples of common BLE Descriptor types and their meanings, it is actually possible to define custom Descriptors according to application requirements.

    Descriptor provides a more detailed description and configuration of characteristics, which can be transmitted and accessed through the Bluetooth protocol. In BLE applications, Descriptor plays an important role in configuration and metadata information, helping devices exchange and understand data accurately.

So now you have understood the role of descriptors, and we have no descriptors under the current features. Note that not every feature has a descriptor. Let's write the descriptors below. First, we add a described list control in item_characteristic.xml, the code is as follows:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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:foreground="?attr/selectableItemBackground"
    android:paddingStart="16dp">

    <TextView
        android:id="@+id/tv_character_name"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="8dp"
        android:text="特性"
        android:textColor="@color/black"
        android:textSize="16sp"
        android:textStyle="bold"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/tv_uuid_title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="UUID:"
        app:layout_constraintStart_toStartOf="@+id/tv_character_name"
        app:layout_constraintTop_toBottomOf="@+id/tv_character_name" />

    <TextView
        android:id="@+id/tv_character_uuid"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="UUID"
        android:textColor="@color/black"
        app:layout_constraintBottom_toBottomOf="@+id/tv_uuid_title"
        app:layout_constraintStart_toEndOf="@+id/tv_uuid_title"
        app:layout_constraintTop_toTopOf="@+id/tv_uuid_title" />

    <TextView
        android:id="@+id/tv_property_title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="8dp"
        android:text="Properties:"
        app:layout_constraintStart_toStartOf="@+id/tv_character_name"
        app:layout_constraintTop_toBottomOf="@+id/tv_uuid_title" />

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/rv_property"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toBottomOf="@+id/tv_property_title"
        app:layout_constraintStart_toEndOf="@+id/tv_property_title"
        app:layout_constraintTop_toTopOf="@+id/tv_property_title" />

    <LinearLayout
        android:id="@+id/lay_descriptors"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginBottom="8dp"
        android:orientation="vertical"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/tv_property_title"
        tools:layout_editor_absoluteX="16dp">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Descriptors:"
            android:textColor="@color/black"
            android:textStyle="bold" />

        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/rv_descriptor"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />
    </LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

According to the preview picture, it is actually this piece of content.

insert image description here

Now we can formally write the descriptor adapter.

② Describe the adapter

  First add one under layout item_descriptor.xml, the code is as follows:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <TextView
        android:id="@+id/tv_descriptor_name"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="描述"
        android:textColor="@color/black"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/tv_uuid_title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="UUID:"
        app:layout_constraintStart_toStartOf="@+id/tv_descriptor_name"
        app:layout_constraintTop_toBottomOf="@+id/tv_descriptor_name" />

    <TextView
        android:id="@+id/tv_descriptor_uuid"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="UUID"
        android:textColor="@color/black"
        app:layout_constraintBottom_toBottomOf="@+id/tv_uuid_title"
        app:layout_constraintStart_toEndOf="@+id/tv_uuid_title"
        app:layout_constraintTop_toTopOf="@+id/tv_uuid_title" />
</androidx.constraintlayout.widget.ConstraintLayout>

Then about the name of the descriptor, we can write a function in BleUtils, the code is as follows:

    fun getDescriptorName(uuid: UUID) =
        when ("0x${
      
      uuid.toString().substring(4, 8).uppercase(Locale.getDefault())}") {
    
    
            "0x2900" -> "Characteristic Extended Properties"
            "0x2901" -> "Characteristic User Description"
            "0x2902" -> "Client Characteristic Configuration"
            "0x2903" -> "Server Characteristic Configuration"
            "0x2904" -> "Characteristic Presentation Format"
            "0x2905" -> "Characteristic Aggregate Format"
            "0x2906" -> "Valid Range"
            "0x2907" -> "External Report Reference"
            "0x2908" -> "Report Reference"
            "0x2909" -> "Number of Digitals"
            "0x290A" -> "Value Trigger Setting"
            "0x290B" -> "Environmental Sensing Configuration"
            "0x290C" -> "Environmental Sensing Measurement"
            "0x290D" -> "Environmental Sensing Trigger Setting"
            "0x290E" -> "Time Trigger Setting"
            "0x290F" -> "Complete BR-EDR Transport Block Data"
            "0x2910" -> "Observation Schedule"
            "0x2911" -> "Valid Range and Accuracy"
            else -> BleConstant.UNKNOWN_DESCRIPTOR
        }

Next, we write the descriptor adapter and create a new class under the adapter package DescriptorAdapter. The code is as follows:

class DescriptorAdapter(
    private val descriptors: List<BluetoothGattDescriptor>
) : RecyclerView.Adapter<DescriptorAdapter.ViewHolder>() {
    
    

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
    
    
        return ViewHolder(ItemDescriptorBinding.inflate(LayoutInflater.from(parent.context), parent, false))
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
    
    
        val descriptorName = BleUtils.getDescriptorName(descriptors[position].uuid)
        holder.binding.tvDescriptorName.text = descriptorName
        holder.binding.tvDescriptorUuid.text = if (descriptorName != BleConstant.UNKNOWN_DESCRIPTOR) BleUtils.getShortUUID(descriptors[position].uuid) else descriptors[position].uuid.toString()
    }

    override fun getItemCount() = descriptors.size

    class ViewHolder(itemView: ItemDescriptorBinding) : RecyclerView.ViewHolder(itemView.root) {
    
    
        var binding: ItemDescriptorBinding

        init {
    
    
            binding = itemView
        }
    }
}

The code here also displays full data for custom UUIDs and short UUIDs for SIGs.

③ Display descriptor

The next step is to load the display descriptor data in the characteristic adapter, modify the onBindViewHolder() function in the CharacteristicAdapter, and add the following code in it:

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
    
    
        ...
        //加载特性下的描述
        if (characteristics[position].descriptors.isEmpty()) {
    
    
            holder.binding.layDescriptors.visibility = View.GONE
            return
        }
        holder.binding.rvDescriptor.apply {
    
    
            layoutManager = LinearLayoutManager(context)
            adapter = DescriptorAdapter(characteristics[position].descriptors)
        }
    }

The function here isEmpty()is very important, because not every feature has a descriptor, which has already been said before, if there is no one, we will directly hide the corresponding descriptor layout, otherwise, load the descriptor data, and run it below to see.

insert image description here

Through this figure, you can clearly see the descriptors under the characteristics, and this article is here.

3. Source code

If it is helpful to you, you may wish to Star or Fork , the mountain is high and the water is long, there will be a period later~

Source address: GoodBle

Guess you like

Origin blog.csdn.net/qq_38436214/article/details/132183853