Shokz: Multipoint pairing

This commit is contained in:
José Rebelo
2025-08-10 00:22:24 +01:00
parent a85c895adf
commit 12b9b8a1c0
13 changed files with 770 additions and 1 deletions
+6
View File
@@ -677,6 +677,12 @@
android:launchMode="singleTop"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|layoutDirection|uiMode"
android:parentActivityName=".activities.ControlCenterv2" />
<activity
android:name=".activities.multipoint.MultipointPairingActivity"
android:label="@string/bluetooth_multipoint_pairing"
android:launchMode="singleTop"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|layoutDirection|uiMode"
android:parentActivityName=".activities.ControlCenterv2" />
<activity
android:name=".devices.miband.MiBandPairingActivity"
android:label="@string/title_activity_mi_band_pairing" />
@@ -562,6 +562,7 @@ public class DeviceSettingsPreferenceConst {
public static final String PREFS_ACTIVITY_IN_DEVICE_CARD_DISTANCE = "prefs_activity_in_device_card_distance";
public static final String PREFS_DEVICE_CHARTS_TABS = "charts_tabs";
public static final String PREFS_PER_APP_NOTIFICATION_SETTINGS = "pref_per_app_notification_settings";
public static final String PREF_MULTIPOINT = "pref_multipoint";
public static final String PREF_UM25_SHOW_THRESHOLD_NOTIFICATION = "um25_current_threshold_notify";
public static final String PREF_UM25_SHOW_THRESHOLD = "um25_current_threshold";
@@ -81,6 +81,7 @@ import nodomain.freeyourgadget.gadgetbridge.activities.app_specific_notification
import nodomain.freeyourgadget.gadgetbridge.activities.audiorecordings.AudioRecordingsActivity;
import nodomain.freeyourgadget.gadgetbridge.activities.loyaltycards.LoyaltyCardsSettingsActivity;
import nodomain.freeyourgadget.gadgetbridge.activities.loyaltycards.LoyaltyCardsSettingsConst;
import nodomain.freeyourgadget.gadgetbridge.activities.multipoint.MultipointPairingActivity;
import nodomain.freeyourgadget.gadgetbridge.activities.musicmanager.MusicManagerActivity;
import nodomain.freeyourgadget.gadgetbridge.activities.widgets.WidgetScreensListActivity;
import nodomain.freeyourgadget.gadgetbridge.capabilities.HeartRateCapability;
@@ -1477,6 +1478,16 @@ public class DeviceSpecificSettingsFragment extends AbstractPreferenceFragment i
});
}
final Preference multipointPref = findPreference(PREF_MULTIPOINT);
if (multipointPref != null) {
multipointPref.setOnPreferenceClickListener(preference -> {
final Intent intent = new Intent(getContext(), MultipointPairingActivity.class);
intent.putExtra(GBDevice.EXTRA_DEVICE, getDevice());
startActivity(intent);
return true;
});
}
final Preference audioRecordings = findPreference("pref_key_audio_recordings");
if (audioRecordings != null) {
audioRecordings.setOnPreferenceClickListener(preference -> {
@@ -0,0 +1,11 @@
package nodomain.freeyourgadget.gadgetbridge.activities.multipoint
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
@Parcelize
data class MultipointDevice(
val address: String,
val name: String?,
val isConnected: Boolean = false
) : Parcelable
@@ -0,0 +1,73 @@
package nodomain.freeyourgadget.gadgetbridge.activities.multipoint
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import nodomain.freeyourgadget.gadgetbridge.R
class MultipointDeviceAdapter(
private val devices: List<MultipointDevice>,
private val onAction: (MultipointDevice, Action) -> Unit
) : RecyclerView.Adapter<MultipointDeviceAdapter.DeviceViewHolder>() {
var allowAction = false
enum class Action {
CONNECT,
DISCONNECT
}
class DeviceViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val deviceIcon: ImageView = itemView.findViewById(R.id.device_icon)
val deviceName: TextView = itemView.findViewById(R.id.device_name)
val deviceAddress: TextView = itemView.findViewById(R.id.device_address)
val connectionButton: Button = itemView.findViewById(R.id.connection_button)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DeviceViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_multipoint_device, parent, false)
return DeviceViewHolder(view)
}
override fun onBindViewHolder(holder: DeviceViewHolder, position: Int) {
val device = devices[position]
val context = holder.itemView.context
holder.deviceName.text = device.name ?: context.getString(R.string.unknown)
holder.deviceAddress.text = device.address
val (icon, buttonText, action) = if (device.isConnected) {
Triple(
R.drawable.ic_bluetooth_connected,
context.getString(R.string.controlcenter_disconnect),
Action.DISCONNECT
)
} else {
Triple(
R.drawable.ic_bluetooth_disabled,
context.getString(R.string.connect),
Action.CONNECT
)
}
val allowConnect = allowAction && devices.count { it.isConnected } < 2
val allowDisconnect = allowAction
holder.deviceIcon.setImageResource(icon)
holder.connectionButton.text = buttonText
holder.connectionButton.isEnabled = when (action) {
Action.CONNECT -> allowConnect
Action.DISCONNECT -> allowDisconnect
}
holder.connectionButton.setOnClickListener {
onAction(device, action)
}
}
override fun getItemCount(): Int = devices.size
}
@@ -0,0 +1,233 @@
package nodomain.freeyourgadget.gadgetbridge.activities.multipoint
import android.annotation.SuppressLint
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Bundle
import android.view.View
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import androidx.recyclerview.widget.LinearLayoutManager
import nodomain.freeyourgadget.gadgetbridge.R
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBActivity
import nodomain.freeyourgadget.gadgetbridge.databinding.ActivityMultipointPairingBinding
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice
import org.slf4j.LoggerFactory
class MultipointPairingActivity : AbstractGBActivity() {
private lateinit var gbDevice: GBDevice
private lateinit var binding: ActivityMultipointPairingBinding
private lateinit var deviceAdapter: MultipointDeviceAdapter
private var devices = mutableListOf<MultipointDevice>()
private var isMultipointEnabled = false
private var pairingNewDevice = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMultipointPairingBinding.inflate(layoutInflater)
setContentView(binding.root)
gbDevice = intent.getParcelableExtra(GBDevice.EXTRA_DEVICE)
?: throw IllegalArgumentException("GBDevice must not be null")
initViews()
setupRecyclerView()
updateUI()
registerBroadcastReceiver()
requestStatus()
requestDeviceList()
}
private fun initViews() {
binding.multipointEnabled.setOnCheckedChangeListener { _, isChecked ->
if (isChecked != isMultipointEnabled) {
toggleMultipoint(isChecked)
}
}
binding.buttonPairNewDevice.setOnClickListener {
setPairingMode(!pairingNewDevice)
}
}
private fun setupRecyclerView() {
deviceAdapter = MultipointDeviceAdapter(devices) { device, action ->
when (action) {
MultipointDeviceAdapter.Action.CONNECT -> connectToDevice(device.address)
MultipointDeviceAdapter.Action.DISCONNECT -> disconnectFromDevice(device.address)
}
}
binding.devicesRecyclerView.layoutManager = LinearLayoutManager(this)
binding.devicesRecyclerView.adapter = deviceAdapter
}
private fun registerBroadcastReceiver() {
val intentFilter = IntentFilter().apply {
addAction(ACTION_MULTIPOINT_DEVICE_LIST)
addAction(ACTION_MULTIPOINT_STATUS_UPDATE)
addAction(ACTION_MULTIPOINT_PAIRING_UPDATE)
addAction(GBDevice.ACTION_DEVICE_CHANGED)
}
LocalBroadcastManager.getInstance(this).registerReceiver(broadcastReceiver, intentFilter)
}
override fun onDestroy() {
super.onDestroy()
setPairingMode(false)
LocalBroadcastManager.getInstance(this).unregisterReceiver(broadcastReceiver)
}
private val broadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
if (intent == null || intent.action == null) {
LOG.warn("Got null intent or action")
return
}
val device = intent.getParcelableExtra<GBDevice>(GBDevice.EXTRA_DEVICE)
if (device?.address != gbDevice.address) {
LOG.warn("Got multipoint action {} for {}, but we're {}", intent.action, device, gbDevice)
return // not for this device
}
when (intent.action) {
ACTION_MULTIPOINT_DEVICE_LIST -> {
LOG.debug("Got multipoint device list")
val deviceList = intent.getParcelableArrayListExtra<MultipointDevice>(EXTRA_DEVICE_LIST)
if (deviceList != null) {
updateDeviceList(deviceList)
}
}
ACTION_MULTIPOINT_STATUS_UPDATE -> {
LOG.debug("Got multipoint status update")
isMultipointEnabled = intent.getBooleanExtra(EXTRA_MULTIPOINT_ENABLED, false)
updateUI()
}
ACTION_MULTIPOINT_PAIRING_UPDATE -> {
LOG.debug("Got multipoint pairing update")
pairingNewDevice = intent.getBooleanExtra(EXTRA_PAIRING_ENABLED, false)
updateUI()
}
GBDevice.ACTION_DEVICE_CHANGED -> {
LOG.debug("Got device update")
gbDevice = device
updateUI()
}
}
}
}
@SuppressLint("NotifyDataSetChanged")
private fun updateDeviceList(deviceList: List<MultipointDevice>) {
devices.clear()
devices.addAll(deviceList)
deviceAdapter.notifyDataSetChanged()
if (devices.isEmpty()) {
binding.devicesRecyclerView.visibility = View.GONE
binding.emptyStateLayout.visibility = View.VISIBLE
} else {
binding.devicesRecyclerView.visibility = View.VISIBLE
binding.emptyStateLayout.visibility = View.GONE
}
binding.buttonPairNewDevice.isEnabled =
gbDevice.isInitialized && isMultipointEnabled && devices.count { it.isConnected } < 2
}
@SuppressLint("NotifyDataSetChanged")
private fun updateUI() {
binding.multipointEnabled.setOnCheckedChangeListener(null)
binding.multipointEnabled.isChecked = isMultipointEnabled
binding.multipointEnabled.isEnabled = gbDevice.isInitialized
binding.multipointEnabled.setOnCheckedChangeListener { _, isChecked ->
if (isChecked != isMultipointEnabled) {
toggleMultipoint(isChecked)
}
}
binding.buttonPairNewDevice.isEnabled =
gbDevice.isInitialized && isMultipointEnabled && devices.count { it.isConnected } < 2
binding.buttonPairNewDevice.text = if (pairingNewDevice) {
getString(R.string.bluetooth_multipoint_pair_stop)
} else {
getString(R.string.bluetooth_multipoint_pair_new)
}
deviceAdapter.allowAction = gbDevice.isInitialized && isMultipointEnabled
deviceAdapter.notifyDataSetChanged()
}
private fun toggleMultipoint(enable: Boolean) {
val action = if (enable) ACTION_MULTIPOINT_ENABLE else ACTION_MULTIPOINT_DISABLE
sendDeviceIntent(Intent(action))
}
private fun requestStatus() {
sendDeviceIntent(Intent(ACTION_MULTIPOINT_GET_STATUS))
}
private fun requestDeviceList() {
sendDeviceIntent(Intent(ACTION_MULTIPOINT_GET_DEVICES))
}
private fun setPairingMode(enabled: Boolean) {
val intent = Intent(ACTION_MULTIPOINT_START_PAIRING)
intent.putExtra(EXTRA_PAIRING_ENABLED, enabled)
sendDeviceIntent(intent)
}
private fun connectToDevice(deviceAddress: String) {
val intent = Intent(ACTION_MULTIPOINT_CONNECT_DEVICE)
intent.putExtra(EXTRA_DEVICE_ADDRESS, deviceAddress)
sendDeviceIntent(intent)
}
private fun disconnectFromDevice(deviceAddress: String) {
val intent = Intent(ACTION_MULTIPOINT_DISCONNECT_DEVICE)
intent.putExtra(EXTRA_DEVICE_ADDRESS, deviceAddress)
sendDeviceIntent(intent)
}
private fun sendDeviceIntent(intent: Intent) {
intent.putExtra(GBDevice.EXTRA_DEVICE, gbDevice)
LocalBroadcastManager.getInstance(this).sendBroadcast(intent)
}
companion object {
private val LOG = LoggerFactory.getLogger(MultipointPairingActivity::class.java)
const val ACTION_MULTIPOINT_DEVICE_LIST =
"nodomain.freeyourgadget.gadgetbridge.ACTION_MULTIPOINT_DEVICE_LIST"
const val ACTION_MULTIPOINT_STATUS_UPDATE =
"nodomain.freeyourgadget.gadgetbridge.ACTION_MULTIPOINT_STATUS_UPDATE"
const val ACTION_MULTIPOINT_PAIRING_UPDATE =
"nodomain.freeyourgadget.gadgetbridge.ACTION_MULTIPOINT_PAIRING_UPDATE"
const val ACTION_MULTIPOINT_ENABLE =
"nodomain.freeyourgadget.gadgetbridge.ACTION_MULTIPOINT_ENABLE"
const val ACTION_MULTIPOINT_DISABLE =
"nodomain.freeyourgadget.gadgetbridge.ACTION_MULTIPOINT_DISABLE"
const val ACTION_MULTIPOINT_GET_STATUS =
"nodomain.freeyourgadget.gadgetbridge.ACTION_MULTIPOINT_GET_STATUS"
const val ACTION_MULTIPOINT_GET_DEVICES =
"nodomain.freeyourgadget.gadgetbridge.ACTION_MULTIPOINT_GET_DEVICES"
const val ACTION_MULTIPOINT_CONNECT_DEVICE =
"nodomain.freeyourgadget.gadgetbridge.ACTION_MULTIPOINT_CONNECT_DEVICE"
const val ACTION_MULTIPOINT_DISCONNECT_DEVICE =
"nodomain.freeyourgadget.gadgetbridge.ACTION_MULTIPOINT_DISCONNECT_DEVICE"
const val ACTION_MULTIPOINT_START_PAIRING =
"nodomain.freeyourgadget.gadgetbridge.ACTION_MULTIPOINT_START_PAIRING"
const val EXTRA_DEVICE_LIST = "device_list"
const val EXTRA_MULTIPOINT_ENABLED = "enabled"
const val EXTRA_PAIRING_ENABLED = "enabled"
const val EXTRA_DEVICE_ADDRESS = "device_address"
}
}
@@ -40,6 +40,7 @@ abstract class ShokzCoordinator : AbstractBLClassicDeviceCoordinator() {
override fun getDeviceSpecificSettings(device: GBDevice): DeviceSpecificSettings {
val settings = DeviceSpecificSettings()
settings.addRootScreen(R.xml.devicesettings_multipoint)
settings.addRootScreen(R.xml.devicesettings_media_source)
settings.addRootScreen(R.xml.devicesettings_shokz_equalizer)
settings.addRootScreen(R.xml.devicesettings_playback_mode)
@@ -51,6 +52,7 @@ abstract class ShokzCoordinator : AbstractBLClassicDeviceCoordinator() {
settings.addSubScreen(DeviceSpecificSettingsScreen.CALLS_AND_NOTIFICATIONS, R.xml.devicesettings_headphones)
settings.addConnectedPreferences(
DeviceSettingsPreferenceConst.PREF_MULTIPOINT,
DeviceSettingsPreferenceConst.PREF_LANGUAGE,
DeviceSettingsPreferenceConst.PREF_MEDIA_SOURCE,
DeviceSettingsPreferenceConst.PREF_SHOKZ_EQUALIZER_BLUETOOTH,
@@ -1,7 +1,16 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.shokz
import android.bluetooth.BluetoothAdapter
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Handler
import android.widget.Toast
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst
import nodomain.freeyourgadget.gadgetbridge.activities.multipoint.MultipointDevice
import nodomain.freeyourgadget.gadgetbridge.activities.multipoint.MultipointPairingActivity
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventUpdatePreferences
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventVersionInfo
@@ -9,6 +18,7 @@ import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice
import nodomain.freeyourgadget.gadgetbridge.service.AbstractHeadphoneBTBRDeviceSupport
import nodomain.freeyourgadget.gadgetbridge.service.btbr.TransactionBuilder
import nodomain.freeyourgadget.gadgetbridge.util.CheckSums
import nodomain.freeyourgadget.gadgetbridge.util.GB
import nodomain.freeyourgadget.gadgetbridge.util.kotlin.stringUntilNullTerminator
import org.slf4j.LoggerFactory
import java.nio.ByteBuffer
@@ -34,6 +44,11 @@ class ShokzSupport : AbstractHeadphoneBTBRDeviceSupport(LOG, MAX_MTU) {
return true
}
override fun setContext(gbDevice: GBDevice, btAdapter: BluetoothAdapter, context: Context) {
super.setContext(gbDevice, btAdapter, context)
setupMultipointBroadcastReceiver()
}
override fun initializeDevice(builder: TransactionBuilder): TransactionBuilder {
packetBuffer.clear()
timeoutHandler.removeCallbacksAndMessages(null)
@@ -63,6 +78,7 @@ class ShokzSupport : AbstractHeadphoneBTBRDeviceSupport(LOG, MAX_MTU) {
override fun dispose() {
synchronized(ConnectionMonitor) {
timeoutHandler.removeCallbacksAndMessages(null)
LocalBroadcastManager.getInstance(context).unregisterReceiver(multipointBroadcastReceiver)
super.dispose()
}
}
@@ -536,6 +552,98 @@ class ShokzSupport : AbstractHeadphoneBTBRDeviceSupport(LOG, MAX_MTU) {
)
}
ShokzCommand.MULTIPOINT_RET -> {
val status = buf.getInt()
val statusBool = status == 0x00010100
LOG.info("Multipoint status={} (0x{})", statusBool, status.toHexString())
broadcastMultipointStatus(statusBool)
}
ShokzCommand.MULTIPOINT_ON_ACK -> {
val enabled: Boolean
if (buf.remaining() >= 4) {
val status = buf.getInt()
LOG.info("Multipoint on ACK status={}", status)
enabled = status == 0
} else {
LOG.warn("Multipoint on ACK payload too short")
enabled = false
}
broadcastMultipointStatus(enabled)
}
ShokzCommand.MULTIPOINT_OFF_ACK -> {
val disabled: Boolean
if (buf.remaining() >= 4) {
val status = buf.getInt()
LOG.info("Multipoint off ACK status={}", status)
disabled = status == 0
} else {
LOG.warn("Multipoint off ACK payload too short")
disabled = false
}
broadcastMultipointStatus(!disabled)
}
ShokzCommand.MULTIPOINT_DEVICES_RET -> {
buf.get() // 0
val numDevices = buf.get().toInt() and 0xff
LOG.debug("Got {} multipoint devices", numDevices);
val devices = mutableListOf<MultipointDevice>()
repeat(numDevices) {
val idx = buf.get().toInt() and 0xff
val macAddress = ByteArray(6)
buf.get(macAddress)
macAddress.reverse()
val connected = buf.get()
val nameLength = buf.get().toInt() and 0xff
val nameBytes = ByteArray(nameLength)
buf.get(nameBytes)
val name = String(nameBytes, Charsets.UTF_8)
LOG.debug("Device {}: {} - {} ({})", idx, macAddress.toHexString(), name, connected)
devices.add(
MultipointDevice(
macAddress.joinToString(separator = ":") {
String.format("%02X", it)
},
name,
connected.toInt() == 1
)
)
}
broadcastMultipointList(devices)
}
ShokzCommand.MULTIPOINT_CONNECT_ACK -> {
val status = buf.getInt()
LOG.info("Multipoint connect ack, status = 0x{}", status.toHexString())
when (status) {
0x00000400 -> {
GB.toast("Too many connected devices", Toast.LENGTH_LONG, GB.WARN)
}
}
}
ShokzCommand.MULTIPOINT_DEVICE_CONNECTION_NOTIFY -> {
LOG.info("Got multipoint device connection, requesting list")
requestPairedDevices()
}
ShokzCommand.MULTIPOINT_DISCONNECT_ACK -> {
LOG.info("Multipoint disconnect ack, status = 0x{}", buf.getInt().toHexString())
}
ShokzCommand.MULTIPOINT_PAIR_SECOND_FINISH -> {
LOG.info("Multipoint pair second finish")
broadcastMultipointPairing(false)
}
ShokzCommand.MULTIPOINT_START_PAIR_ACK -> {
LOG.info("Multipoint start pair ack, status = 0x{}", buf.getInt().toHexString())
broadcastMultipointPairing(true)
}
else -> LOG.warn("Unhandled command {}, args={}", command, args.toHexString())
}
}
@@ -581,7 +689,142 @@ class ShokzSupport : AbstractHeadphoneBTBRDeviceSupport(LOG, MAX_MTU) {
builder.write(*encodeCommand(message.command, message.args))
builder.queue()
timeoutHandler.postDelayed({ onCommandTimeout() }, 2000L)
when (message.command) {
ShokzCommand.MULTIPOINT_PAIR_SECOND_FINISH,
ShokzCommand.MULTIPOINT_START_PAIR_REQ -> {
sendNextCommand()
}
else -> {
timeoutHandler.postDelayed({ onCommandTimeout() }, 2000L)
}
}
}
private fun setupMultipointBroadcastReceiver() {
val intentFilter = IntentFilter().apply {
addAction(MultipointPairingActivity.ACTION_MULTIPOINT_ENABLE)
addAction(MultipointPairingActivity.ACTION_MULTIPOINT_DISABLE)
addAction(MultipointPairingActivity.ACTION_MULTIPOINT_GET_DEVICES)
addAction(MultipointPairingActivity.ACTION_MULTIPOINT_GET_STATUS)
addAction(MultipointPairingActivity.ACTION_MULTIPOINT_CONNECT_DEVICE)
addAction(MultipointPairingActivity.ACTION_MULTIPOINT_DISCONNECT_DEVICE)
addAction(MultipointPairingActivity.ACTION_MULTIPOINT_START_PAIRING)
}
LocalBroadcastManager.getInstance(context).registerReceiver(
multipointBroadcastReceiver,
intentFilter
)
}
private val multipointBroadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
val device = intent?.getParcelableExtra<GBDevice>(GBDevice.EXTRA_DEVICE)
if (device?.address != gbDevice.address) {
return // not for this device
}
when (intent.action) {
MultipointPairingActivity.ACTION_MULTIPOINT_ENABLE -> {
LOG.info("Enabling multipoint")
queueCommand(ShokzCommand.MULTIPOINT_ON)
}
MultipointPairingActivity.ACTION_MULTIPOINT_DISABLE -> {
val macAddress = bluetoothAdapter.address
LOG.debug("Disabling multipoint from mac address {}", macAddress)
val macAddressBytes = macAddress.replace(":", "").hexToByteArray()
macAddressBytes.reverse()
queueCommand(ShokzCommand.MULTIPOINT_OFF, macAddressBytes)
}
MultipointPairingActivity.ACTION_MULTIPOINT_GET_DEVICES -> requestPairedDevices()
MultipointPairingActivity.ACTION_MULTIPOINT_GET_STATUS -> requestMultipointStatus()
MultipointPairingActivity.ACTION_MULTIPOINT_CONNECT_DEVICE -> {
val deviceAddress = intent.getStringExtra(MultipointPairingActivity.EXTRA_DEVICE_ADDRESS)
if (deviceAddress != null) {
connectToDevice(deviceAddress)
}
}
MultipointPairingActivity.ACTION_MULTIPOINT_DISCONNECT_DEVICE -> {
val deviceAddress = intent.getStringExtra(MultipointPairingActivity.EXTRA_DEVICE_ADDRESS)
if (deviceAddress != null) {
disconnectFromDevice(deviceAddress)
}
}
MultipointPairingActivity.ACTION_MULTIPOINT_START_PAIRING -> {
val enabled = intent.getBooleanExtra(MultipointPairingActivity.EXTRA_PAIRING_ENABLED, false)
startPairingMode(enabled)
}
else -> LOG.warn("Unknown action {}", intent.action)
}
}
}
private fun requestPairedDevices() {
LOG.info("Requesting paired devices")
queueCommand(ShokzCommand.MULTIPOINT_DEVICES_GET)
}
private fun requestMultipointStatus() {
LOG.info("Requesting multipoint status")
queueCommand(ShokzCommand.MULTIPOINT_GET)
}
private fun connectToDevice(deviceAddress: String) {
LOG.info("Connecting to {}", deviceAddress)
val macAddress = deviceAddress.replace(":", "").hexToByteArray()
macAddress.reverse()
queueCommand(ShokzCommand.MULTIPOINT_CONNECT_REQ, macAddress)
}
private fun disconnectFromDevice(deviceAddress: String) {
LOG.info("Disconnecting from {}", deviceAddress)
val macAddress = deviceAddress.replace(":", "").hexToByteArray()
macAddress.reverse()
queueCommand(ShokzCommand.MULTIPOINT_DISCONNECT_REQ, macAddress)
}
private fun startPairingMode(enabled: Boolean) {
LOG.info("Starting pairing mode enabled={}", enabled)
if (enabled) {
queueCommand(ShokzCommand.MULTIPOINT_START_PAIR_REQ)
} else {
queueCommand(ShokzCommand.MULTIPOINT_PAIR_SECOND_FINISH)
}
}
private fun broadcastMultipointStatus(enabled: Boolean) {
val intent = Intent(MultipointPairingActivity.ACTION_MULTIPOINT_STATUS_UPDATE).apply {
putExtra(GBDevice.EXTRA_DEVICE, device)
putExtra(MultipointPairingActivity.EXTRA_MULTIPOINT_ENABLED, enabled)
}
LocalBroadcastManager.getInstance(context).sendBroadcast(intent)
}
private fun broadcastMultipointPairing(enabled: Boolean) {
val intent = Intent(MultipointPairingActivity.ACTION_MULTIPOINT_PAIRING_UPDATE).apply {
putExtra(GBDevice.EXTRA_DEVICE, device)
putExtra(MultipointPairingActivity.EXTRA_PAIRING_ENABLED, enabled)
}
LocalBroadcastManager.getInstance(context).sendBroadcast(intent)
}
private fun broadcastMultipointList(devices: List<MultipointDevice>) {
val intent = Intent(MultipointPairingActivity.ACTION_MULTIPOINT_DEVICE_LIST).apply {
putExtra(GBDevice.EXTRA_DEVICE, device)
putParcelableArrayListExtra(
MultipointPairingActivity.EXTRA_DEVICE_LIST,
ArrayList(devices)
)
}
LocalBroadcastManager.getInstance(context).sendBroadcast(intent)
}
companion object {
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="@android:color/white"
android:pathData="M792,904 L624,736 480,880h-40v-304L256,760l-56,-56 196,-196L56,168l56,-56 736,736 -56,56ZM520,726l46,-46 -46,-46v92ZM564,452 L508,396 596,308 520,234v174l-80,-80v-248h40l228,228 -144,144Z" />
</vector>
@@ -0,0 +1,95 @@
<?xml version="1.0" encoding="utf-8"?>
<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="match_parent"
android:orientation="vertical"
android:padding="16dp">
<!-- Header with title and toggle -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/bluetooth_multipoint"
android:textSize="18sp"
android:textStyle="bold" />
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/multipoint_enabled"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:gravity="center"
android:orientation="horizontal">
<Button
android:id="@+id/button_pair_new_device"
style="@style/Widget.Material3.Button.OutlinedButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_weight="1"
android:text="@string/bluetooth_multipoint_pair_new" />
</LinearLayout>
<!-- Paired devices list -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:text="@string/bluetooth_paired_devices"
android:textSize="16sp"
android:textStyle="bold" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/devices_recycler_view"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:clipToPadding="false"
android:paddingBottom="8dp" />
<!-- Empty state -->
<LinearLayout
android:id="@+id/empty_state_layout"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:gravity="center"
android:orientation="vertical"
android:visibility="gone"
tools:ignore="UseCompoundDrawables">
<ImageView
android:layout_width="64dp"
android:layout_height="64dp"
android:layout_marginBottom="16dp"
android:alpha="0.6"
android:importantForAccessibility="no"
android:src="@drawable/ic_bluetooth" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:alpha="0.6"
android:gravity="center"
android:text="@string/bluetooth_multipoint_no_devices"
android:textSize="16sp" />
</LinearLayout>
</LinearLayout>
@@ -0,0 +1,67 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView 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:layout_margin="4dp"
app:cardCornerRadius="8dp"
app:cardElevation="2dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal"
android:padding="16dp">
<!-- Device icon -->
<ImageView
android:id="@+id/device_icon"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginEnd="16dp"
android:importantForAccessibility="no"
android:src="@drawable/ic_bluetooth" />
<!-- Device info -->
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:id="@+id/device_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/devicetype_unknown"
android:textColor="?android:attr/textColorPrimary"
android:textSize="16sp"
android:textStyle="bold" />
<TextView
android:id="@+id/device_address"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:text="00:00:00:00:00:00"
android:textColor="?android:attr/textColorSecondary"
android:textSize="12sp"
tools:ignore="HardcodedText" />
</LinearLayout>
<!-- Connection button -->
<Button
android:id="@+id/connection_button"
style="@style/Widget.Material3.Button.OutlinedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minWidth="72dp"
android:text="@string/connect"
android:textSize="12sp" />
</LinearLayout>
</androidx.cardview.widget.CardView>
+7
View File
@@ -4240,4 +4240,11 @@
<string name="shokz_controls_long_press_multi_function">Long-press the multi-function button</string>
<string name="shokz_controls_simultaneous_volume_up_down">Long-press volume up and down simultaneously</string>
<string name="track_name_required">Track name cannot be empty</string>
<string name="bluetooth_multipoint_pairing">Multipoint pairing</string>
<string name="bluetooth_multipoint">Multipoint</string>
<string name="bluetooth_multipoint_pair_new">Pair New Device</string>
<string name="bluetooth_multipoint_no_devices">No paired devices found\n\nTap "Pair New Device" to add a device</string>
<string name="bluetooth_multipoint_pair_stop">Stop pairing</string>
<string name="bluetooth_paired_devices">Paired Devices</string>
<string name="connect">Connect</string>
</resources>
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<Preference
android:icon="@drawable/ic_bluetooth_searching"
android:key="pref_multipoint"
android:persistent="false"
android:title="@string/bluetooth_multipoint_pairing" />
</androidx.preference.PreferenceScreen>