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:launchMode="singleTop"
|
||||||
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|layoutDirection|uiMode"
|
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|layoutDirection|uiMode"
|
||||||
android:parentActivityName=".activities.ControlCenterv2" />
|
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
|
<activity
|
||||||
android:name=".devices.miband.MiBandPairingActivity"
|
android:name=".devices.miband.MiBandPairingActivity"
|
||||||
android:label="@string/title_activity_mi_band_pairing" />
|
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_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_DEVICE_CHARTS_TABS = "charts_tabs";
|
||||||
public static final String PREFS_PER_APP_NOTIFICATION_SETTINGS = "pref_per_app_notification_settings";
|
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_NOTIFICATION = "um25_current_threshold_notify";
|
||||||
public static final String PREF_UM25_SHOW_THRESHOLD = "um25_current_threshold";
|
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.audiorecordings.AudioRecordingsActivity;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.activities.loyaltycards.LoyaltyCardsSettingsActivity;
|
import nodomain.freeyourgadget.gadgetbridge.activities.loyaltycards.LoyaltyCardsSettingsActivity;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.activities.loyaltycards.LoyaltyCardsSettingsConst;
|
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.musicmanager.MusicManagerActivity;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.activities.widgets.WidgetScreensListActivity;
|
import nodomain.freeyourgadget.gadgetbridge.activities.widgets.WidgetScreensListActivity;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.capabilities.HeartRateCapability;
|
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");
|
final Preference audioRecordings = findPreference("pref_key_audio_recordings");
|
||||||
if (audioRecordings != null) {
|
if (audioRecordings != null) {
|
||||||
audioRecordings.setOnPreferenceClickListener(preference -> {
|
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 {
|
override fun getDeviceSpecificSettings(device: GBDevice): DeviceSpecificSettings {
|
||||||
val settings = DeviceSpecificSettings()
|
val settings = DeviceSpecificSettings()
|
||||||
|
|
||||||
|
settings.addRootScreen(R.xml.devicesettings_multipoint)
|
||||||
settings.addRootScreen(R.xml.devicesettings_media_source)
|
settings.addRootScreen(R.xml.devicesettings_media_source)
|
||||||
settings.addRootScreen(R.xml.devicesettings_shokz_equalizer)
|
settings.addRootScreen(R.xml.devicesettings_shokz_equalizer)
|
||||||
settings.addRootScreen(R.xml.devicesettings_playback_mode)
|
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.addSubScreen(DeviceSpecificSettingsScreen.CALLS_AND_NOTIFICATIONS, R.xml.devicesettings_headphones)
|
||||||
|
|
||||||
settings.addConnectedPreferences(
|
settings.addConnectedPreferences(
|
||||||
|
DeviceSettingsPreferenceConst.PREF_MULTIPOINT,
|
||||||
DeviceSettingsPreferenceConst.PREF_LANGUAGE,
|
DeviceSettingsPreferenceConst.PREF_LANGUAGE,
|
||||||
DeviceSettingsPreferenceConst.PREF_MEDIA_SOURCE,
|
DeviceSettingsPreferenceConst.PREF_MEDIA_SOURCE,
|
||||||
DeviceSettingsPreferenceConst.PREF_SHOKZ_EQUALIZER_BLUETOOTH,
|
DeviceSettingsPreferenceConst.PREF_SHOKZ_EQUALIZER_BLUETOOTH,
|
||||||
|
|||||||
+244
-1
@@ -1,7 +1,16 @@
|
|||||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.shokz
|
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.os.Handler
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||||
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst
|
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.GBDeviceEventBatteryInfo
|
||||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventUpdatePreferences
|
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventUpdatePreferences
|
||||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventVersionInfo
|
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.AbstractHeadphoneBTBRDeviceSupport
|
||||||
import nodomain.freeyourgadget.gadgetbridge.service.btbr.TransactionBuilder
|
import nodomain.freeyourgadget.gadgetbridge.service.btbr.TransactionBuilder
|
||||||
import nodomain.freeyourgadget.gadgetbridge.util.CheckSums
|
import nodomain.freeyourgadget.gadgetbridge.util.CheckSums
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.util.GB
|
||||||
import nodomain.freeyourgadget.gadgetbridge.util.kotlin.stringUntilNullTerminator
|
import nodomain.freeyourgadget.gadgetbridge.util.kotlin.stringUntilNullTerminator
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
@@ -34,6 +44,11 @@ class ShokzSupport : AbstractHeadphoneBTBRDeviceSupport(LOG, MAX_MTU) {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun setContext(gbDevice: GBDevice, btAdapter: BluetoothAdapter, context: Context) {
|
||||||
|
super.setContext(gbDevice, btAdapter, context)
|
||||||
|
setupMultipointBroadcastReceiver()
|
||||||
|
}
|
||||||
|
|
||||||
override fun initializeDevice(builder: TransactionBuilder): TransactionBuilder {
|
override fun initializeDevice(builder: TransactionBuilder): TransactionBuilder {
|
||||||
packetBuffer.clear()
|
packetBuffer.clear()
|
||||||
timeoutHandler.removeCallbacksAndMessages(null)
|
timeoutHandler.removeCallbacksAndMessages(null)
|
||||||
@@ -63,6 +78,7 @@ class ShokzSupport : AbstractHeadphoneBTBRDeviceSupport(LOG, MAX_MTU) {
|
|||||||
override fun dispose() {
|
override fun dispose() {
|
||||||
synchronized(ConnectionMonitor) {
|
synchronized(ConnectionMonitor) {
|
||||||
timeoutHandler.removeCallbacksAndMessages(null)
|
timeoutHandler.removeCallbacksAndMessages(null)
|
||||||
|
LocalBroadcastManager.getInstance(context).unregisterReceiver(multipointBroadcastReceiver)
|
||||||
super.dispose()
|
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())
|
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.write(*encodeCommand(message.command, message.args))
|
||||||
builder.queue()
|
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 {
|
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_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="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="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>
|
</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