mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge.git
synced 2026-07-31 07:44:24 +02:00
Shokz: Multipoint pairing
This commit is contained in:
@@ -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" />
|
||||
|
||||
+1
@@ -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";
|
||||
|
||||
+11
@@ -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 -> {
|
||||
|
||||
+11
@@ -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
|
||||
+73
@@ -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
|
||||
}
|
||||
+233
@@ -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"
|
||||
}
|
||||
}
|
||||
+2
@@ -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,
|
||||
|
||||
+244
-1
@@ -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>
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user