android: clean up main service and remove minimum API on head gestures

This commit is contained in:
Kavish Devar
2025-09-10 11:30:22 +05:30
parent 0e9aadd672
commit aecbb066b5
3 changed files with 137 additions and 100 deletions

View File

@@ -17,6 +17,7 @@
*/
@file:OptIn(ExperimentalEncodingApi::class)
@file:Suppress("DEPRECATION")
package me.kavishdevar.librepods.services
@@ -29,6 +30,7 @@ import android.app.PendingIntent
import android.app.Service
import android.appwidget.AppWidgetManager
import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothHeadset
import android.bluetooth.BluetoothManager
import android.bluetooth.BluetoothProfile
import android.bluetooth.BluetoothSocket
@@ -41,6 +43,7 @@ import android.content.IntentFilter
import android.content.SharedPreferences
import android.content.pm.PackageManager
import android.content.res.Resources
import android.graphics.Color
import android.media.AudioManager
import android.net.Uri
import android.os.BatteryManager
@@ -315,6 +318,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
}
}
@SuppressLint("MissingPermission", "UnspecifiedRegisterReceiverFlag")
override fun onCreate() {
super.onCreate()
@@ -337,7 +341,14 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
ServiceManager.setService(this)
startForegroundNotification()
initGestureDetector()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
initGestureDetector()
} else {
gestureDetector = null
config.headGestures = false
sharedPreferences.edit { putBoolean("head_gestures", false) }
Log.d("AirPodsService", "Head gestures disabled as device is running Android 9 or below")
}
bleManager = BLEManager(this)
bleManager.setAirPodsStatusListener(bleStatusListener)
@@ -345,63 +356,111 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
sharedPreferences = getSharedPreferences("settings", MODE_PRIVATE)
with(sharedPreferences) {
val editor = edit()
edit {
if (!contains("conversational_awareness_pause_music")) putBoolean(
"conversational_awareness_pause_music",
false
)
if (!contains("personalized_volume")) putBoolean("personalized_volume", false)
if (!contains("automatic_ear_detection")) putBoolean(
"automatic_ear_detection",
true
)
if (!contains("long_press_nc")) putBoolean("long_press_nc", true)
if (!contains("off_listening_mode")) putBoolean("off_listening_mode", false)
if (!contains("show_phone_battery_in_widget")) putBoolean(
"show_phone_battery_in_widget",
true
)
if (!contains("single_anc")) putBoolean("single_anc", true)
if (!contains("long_press_transparency")) putBoolean(
"long_press_transparency",
true
)
if (!contains("conversational_awareness")) putBoolean(
"conversational_awareness",
true
)
if (!contains("relative_conversational_awareness_volume")) putBoolean(
"relative_conversational_awareness_volume",
true
)
if (!contains("long_press_adaptive")) putBoolean("long_press_adaptive", true)
if (!contains("loud_sound_reduction")) putBoolean("loud_sound_reduction", true)
if (!contains("long_press_off")) putBoolean("long_press_off", false)
if (!contains("volume_control")) putBoolean("volume_control", true)
if (!contains("head_gestures")) putBoolean("head_gestures", true)
if (!contains("disconnect_when_not_wearing")) putBoolean(
"disconnect_when_not_wearing",
false
)
if (!contains("conversational_awareness_pause_music")) editor.putBoolean("conversational_awareness_pause_music", false)
if (!contains("personalized_volume")) editor.putBoolean("personalized_volume", false)
if (!contains("automatic_ear_detection")) editor.putBoolean("automatic_ear_detection", true)
if (!contains("long_press_nc")) editor.putBoolean("long_press_nc", true)
if (!contains("off_listening_mode")) editor.putBoolean("off_listening_mode", false)
if (!contains("show_phone_battery_in_widget")) editor.putBoolean("show_phone_battery_in_widget", true)
if (!contains("single_anc")) editor.putBoolean("single_anc", true)
if (!contains("long_press_transparency")) editor.putBoolean("long_press_transparency", true)
if (!contains("conversational_awareness")) editor.putBoolean("conversational_awareness", true)
if (!contains("relative_conversational_awareness_volume")) editor.putBoolean("relative_conversational_awareness_volume", true)
if (!contains("long_press_adaptive")) editor.putBoolean("long_press_adaptive", true)
if (!contains("loud_sound_reduction")) editor.putBoolean("loud_sound_reduction", true)
if (!contains("long_press_off")) editor.putBoolean("long_press_off", false)
if (!contains("volume_control")) editor.putBoolean("volume_control", true)
if (!contains("head_gestures")) editor.putBoolean("head_gestures", true)
if (!contains("disconnect_when_not_wearing")) editor.putBoolean("disconnect_when_not_wearing", false)
// AirPods state-based takeover
if (!contains("takeover_when_disconnected")) putBoolean(
"takeover_when_disconnected",
true
)
if (!contains("takeover_when_idle")) putBoolean("takeover_when_idle", true)
if (!contains("takeover_when_music")) putBoolean("takeover_when_music", false)
if (!contains("takeover_when_call")) putBoolean("takeover_when_call", true)
// AirPods state-based takeover
if (!contains("takeover_when_disconnected")) editor.putBoolean("takeover_when_disconnected", true)
if (!contains("takeover_when_idle")) editor.putBoolean("takeover_when_idle", true)
if (!contains("takeover_when_music")) editor.putBoolean("takeover_when_music", false)
if (!contains("takeover_when_call")) editor.putBoolean("takeover_when_call", true)
// Phone state-based takeover
if (!contains("takeover_when_ringing_call")) putBoolean(
"takeover_when_ringing_call",
true
)
if (!contains("takeover_when_media_start")) putBoolean(
"takeover_when_media_start",
true
)
// Phone state-based takeover
if (!contains("takeover_when_ringing_call")) editor.putBoolean("takeover_when_ringing_call", true)
if (!contains("takeover_when_media_start")) editor.putBoolean("takeover_when_media_start", true)
if (!contains("adaptive_strength")) putInt("adaptive_strength", 51)
if (!contains("tone_volume")) putInt("tone_volume", 75)
if (!contains("conversational_awareness_volume")) putInt(
"conversational_awareness_volume",
43
)
if (!contains("adaptive_strength")) editor.putInt("adaptive_strength", 51)
if (!contains("tone_volume")) editor.putInt("tone_volume", 75)
if (!contains("conversational_awareness_volume")) editor.putInt("conversational_awareness_volume", 43)
if (!contains("textColor")) putLong("textColor", -1L)
if (!contains("textColor")) editor.putLong("textColor", -1L)
if (!contains("qs_click_behavior")) putString("qs_click_behavior", "cycle")
if (!contains("name")) putString("name", "AirPods")
if (!contains("ble_only_mode")) putBoolean("ble_only_mode", false)
if (!contains("qs_click_behavior")) editor.putString("qs_click_behavior", "cycle")
if (!contains("name")) editor.putString("name", "AirPods")
if (!contains("ble_only_mode")) editor.putBoolean("ble_only_mode", false)
if (!contains("left_single_press_action")) putString(
"left_single_press_action",
StemAction.defaultActions[StemPressType.SINGLE_PRESS]!!.name
)
if (!contains("right_single_press_action")) putString(
"right_single_press_action",
StemAction.defaultActions[StemPressType.SINGLE_PRESS]!!.name
)
if (!contains("left_double_press_action")) putString(
"left_double_press_action",
StemAction.defaultActions[StemPressType.DOUBLE_PRESS]!!.name
)
if (!contains("right_double_press_action")) putString(
"right_double_press_action",
StemAction.defaultActions[StemPressType.DOUBLE_PRESS]!!.name
)
if (!contains("left_triple_press_action")) putString(
"left_triple_press_action",
StemAction.defaultActions[StemPressType.TRIPLE_PRESS]!!.name
)
if (!contains("right_triple_press_action")) putString(
"right_triple_press_action",
StemAction.defaultActions[StemPressType.TRIPLE_PRESS]!!.name
)
if (!contains("left_long_press_action")) putString(
"left_long_press_action",
StemAction.defaultActions[StemPressType.LONG_PRESS]!!.name
)
if (!contains("right_long_press_action")) putString(
"right_long_press_action",
StemAction.defaultActions[StemPressType.LONG_PRESS]!!.name
)
if (!contains("left_single_press_action")) editor.putString("left_single_press_action",
StemAction.defaultActions[StemPressType.SINGLE_PRESS]!!.name)
if (!contains("right_single_press_action")) editor.putString("right_single_press_action",
StemAction.defaultActions[StemPressType.SINGLE_PRESS]!!.name)
if (!contains("left_double_press_action")) editor.putString("left_double_press_action",
StemAction.defaultActions[StemPressType.DOUBLE_PRESS]!!.name)
if (!contains("right_double_press_action")) editor.putString("right_double_press_action",
StemAction.defaultActions[StemPressType.DOUBLE_PRESS]!!.name)
if (!contains("left_triple_press_action")) editor.putString("left_triple_press_action",
StemAction.defaultActions[StemPressType.TRIPLE_PRESS]!!.name)
if (!contains("right_triple_press_action")) editor.putString("right_triple_press_action",
StemAction.defaultActions[StemPressType.TRIPLE_PRESS]!!.name)
if (!contains("left_long_press_action")) editor.putString("left_long_press_action",
StemAction.defaultActions[StemPressType.LONG_PRESS]!!.name)
if (!contains("right_long_press_action")) editor.putString("right_long_press_action",
StemAction.defaultActions[StemPressType.LONG_PRESS]!!.name)
editor.apply()
}
}
initializeConfig()
@@ -453,6 +512,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
registerReceiver(ancModeReceiver, ancModeFilter, RECEIVER_EXPORTED)
} else {
@Suppress("UnspecifiedRegisterReceiverFlag")
registerReceiver(ancModeReceiver, ancModeFilter)
}
val audioManager =
@@ -518,6 +578,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
RECEIVER_EXPORTED
)
} else {
@Suppress("UnspecifiedRegisterReceiverFlag")
registerReceiver(BatteryChangedIntentReceiver, batteryChangedIntentFilter)
}
}
@@ -598,6 +659,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
registerReceiver(showIslandReceiver, showIslandIntentFilter, RECEIVER_EXPORTED)
} else {
@Suppress("UnspecifiedRegisterReceiverFlag")
registerReceiver(showIslandReceiver, showIslandIntentFilter)
}
@@ -610,6 +672,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
registerReceiver(connectionReceiver, deviceIntentFilter, RECEIVER_EXPORTED)
registerReceiver(bluetoothReceiver, serviceIntentFilter, RECEIVER_EXPORTED)
} else {
@Suppress("UnspecifiedRegisterReceiverFlag")
registerReceiver(connectionReceiver, deviceIntentFilter)
registerReceiver(bluetoothReceiver, serviceIntentFilter)
}
@@ -623,6 +686,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
bluetoothAdapter.getProfileProxy(
this,
object : BluetoothProfile.ServiceListener {
@SuppressLint("NewApi")
override fun onServiceConnected(profile: Int, proxy: BluetoothProfile) {
if (profile == BluetoothProfile.A2DP) {
val connectedDevices = proxy.connectedDevices
@@ -668,6 +732,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
}
}
@Suppress("unused")
fun cameraOpened() {
Log.d("AirPodsService", "Camera opened, gonna handle stem presses and take action if enabled")
val isCameraShutterUsed = listOf(
@@ -904,7 +969,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
Log.d("AirPodsParser", "Connected device: ${device.mac}, info1: ${device.info1}, info2: ${device.info2})")
}
val newDevices = connectedDevices.filter { newDevice ->
val notInOld = aacpManager.oldConnectedDevices?.none { oldDevice -> oldDevice.mac == newDevice.mac } ?: true
val notInOld = aacpManager.oldConnectedDevices.none { oldDevice -> oldDevice.mac == newDevice.mac }
val notLocal = newDevice.mac != localMac
notInOld && notLocal
}
@@ -958,8 +1023,8 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
}
private fun processEarDetectionChange(earDetection: ByteArray) {
var inEar = false
var inEarData = listOf(earDetectionNotification.status[0] == 0x00.toByte(), earDetectionNotification.status[1] == 0x00.toByte())
var inEar: Boolean
val inEarData = listOf(earDetectionNotification.status[0] == 0x00.toByte(), earDetectionNotification.status[1] == 0x00.toByte())
var justEnabledA2dp = false
earDetectionNotification.setStatus(earDetection)
if (config.earDetectionEnabled) {
@@ -1008,10 +1073,8 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
Log.d("AirPodsParser", "inEarData: ${inEarData.sorted()}, newInEarData: ${newInEarData.sorted()}")
if (newInEarData.sorted() != inEarData.sorted()) {
inEarData = newInEarData
if (inEar == true) {
if (inEar) {
if (!justEnabledA2dp) {
justEnabledA2dp = false
MediaController.sendPlay()
MediaController.iPausedTheMedia = false
}
@@ -1178,7 +1241,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
}
}
private fun logPacket(packet: ByteArray, source: String) {
private fun logPacket(packet: ByteArray, @Suppress("SameParameterValue") source: String) {
val packetHex = packet.joinToString(" ") { "%02X".format(it) }
val logEntry = "$source: $packetHex"
@@ -1207,10 +1270,6 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
}
}
fun getPacketLogs(): Set<String> {
return inMemoryLogs.toSet()
}
private fun clearPacketLogs() {
synchronized(inMemoryLogs) {
inMemoryLogs.clear()
@@ -1232,7 +1291,6 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
private var isInCall = false
private var callNumber: String? = null
@RequiresApi(Build.VERSION_CODES.Q)
private fun initGestureDetector() {
if (gestureDetector == null) {
gestureDetector = GestureDetector(this)
@@ -1296,11 +1354,6 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
}
}
fun setPhoneBatteryInWidget(enabled: Boolean) {
widgetMobileBatteryEnabled = enabled
updateBattery()
}
@OptIn(ExperimentalMaterial3Api::class)
fun startForegroundNotification() {
val disconnectedNotificationChannel = NotificationChannel(
@@ -1322,7 +1375,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
).apply {
description = "Notifications about problems connecting to AirPods protocol"
enableLights(true)
lightColor = android.graphics.Color.RED
lightColor = Color.RED
enableVibration(true)
}
@@ -1609,7 +1662,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
batteryList: List<Battery>? = null
) {
val notificationManager = getSystemService(NotificationManager::class.java)
var updatedNotification: Notification? = null
var updatedNotification: Notification?
val notificationIntent = Intent(this, MainActivity::class.java)
val pendingIntent = PendingIntent.getActivity(
@@ -1695,7 +1748,6 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
}
}
@RequiresApi(Build.VERSION_CODES.Q)
fun handleIncomingCall() {
if (isInCall) return
if (config.headGestures) {
@@ -1712,9 +1764,8 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
}
}
}
@OptIn(ExperimentalCoroutinesApi::class)
@RequiresApi(Build.VERSION_CODES.Q)
@OptIn(ExperimentalCoroutinesApi::class)
suspend fun testHeadGestures(): Boolean {
return suspendCancellableCoroutine { continuation ->
gestureDetector?.startDetection(doNotStop = true) { accepted ->
@@ -1801,7 +1852,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
.appendPath(applicationContext.resources.getResourceTypeName(resId))
.appendPath(applicationContext.resources.getResourceEntryName(resId))
.build()
} catch (e: Resources.NotFoundException) {
} catch (_: Resources.NotFoundException) {
null
}
}
@@ -1821,7 +1872,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
@Suppress("PrivatePropertyName")
private val ACTION_ASI_UPDATE_BLUETOOTH_DATA = "batterywidget.impl.action.update_bluetooth_data"
@Suppress("MissingPermission")
@Suppress("MissingPermission", "unused")
fun broadcastBatteryInformation() {
if (device == null) return
@@ -1848,13 +1899,13 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
)
// Broadcast vendor-specific event
val intent = Intent(android.bluetooth.BluetoothHeadset.ACTION_VENDOR_SPECIFIC_HEADSET_EVENT).apply {
putExtra(android.bluetooth.BluetoothHeadset.EXTRA_VENDOR_SPECIFIC_HEADSET_EVENT_CMD, VENDOR_SPECIFIC_HEADSET_EVENT_IPHONEACCEV)
putExtra(android.bluetooth.BluetoothHeadset.EXTRA_VENDOR_SPECIFIC_HEADSET_EVENT_CMD_TYPE, android.bluetooth.BluetoothHeadset.AT_CMD_TYPE_SET)
putExtra(android.bluetooth.BluetoothHeadset.EXTRA_VENDOR_SPECIFIC_HEADSET_EVENT_ARGS, arguments)
val intent = Intent(BluetoothHeadset.ACTION_VENDOR_SPECIFIC_HEADSET_EVENT).apply {
putExtra(BluetoothHeadset.EXTRA_VENDOR_SPECIFIC_HEADSET_EVENT_CMD, VENDOR_SPECIFIC_HEADSET_EVENT_IPHONEACCEV)
putExtra(BluetoothHeadset.EXTRA_VENDOR_SPECIFIC_HEADSET_EVENT_CMD_TYPE, BluetoothHeadset.AT_CMD_TYPE_SET)
putExtra(BluetoothHeadset.EXTRA_VENDOR_SPECIFIC_HEADSET_EVENT_ARGS, arguments)
putExtra(BluetoothDevice.EXTRA_DEVICE, device)
putExtra(BluetoothDevice.EXTRA_NAME, device?.name)
addCategory("${android.bluetooth.BluetoothHeadset.VENDOR_SPECIFIC_HEADSET_EVENT_COMPANY_ID_CATEGORY}.$APPLE")
addCategory("${BluetoothHeadset.VENDOR_SPECIFIC_HEADSET_EVENT_COMPANY_ID_CATEGORY}.$APPLE")
}
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
@@ -2211,13 +2262,12 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
throw lastException ?: IllegalStateException(errorMessage)
}
@RequiresApi(Build.VERSION_CODES.R)
@SuppressLint("MissingPermission", "UnspecifiedRegisterReceiverFlag")
fun connectToSocket(device: BluetoothDevice) {
Log.d("AirPodsService", "<LogCollector:Start> Connecting to socket")
HiddenApiBypass.addHiddenApiExemptions("Landroid/bluetooth/BluetoothSocket;")
val uuid: ParcelUuid = ParcelUuid.fromString("74ec2172-0bad-4d01-8f77-997b2be0722a")
if (isConnectedLocally != true && !CrossDevice.isAvailable) {
if (!isConnectedLocally && !CrossDevice.isAvailable) {
socket = try {
createBluetoothSocket(device, uuid)
} catch (e: Exception) {
@@ -2284,11 +2334,11 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
setupStemActions()
while (socket.isConnected == true) {
while (socket.isConnected) {
socket.let {
val buffer = ByteArray(1024)
val bytesRead = it.inputStream.read(buffer)
var data: ByteArray = byteArrayOf()
var data: ByteArray
if (bytesRead > 0) {
data = buffer.copyOfRange(0, bytesRead)
sendBroadcast(Intent(AirPodsNotifications.AIRPODS_DATA).apply {
@@ -2367,6 +2417,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
val conversationAwarenessNotification =
AirPodsNotifications.ConversationalAwarenessNotification()
@Suppress("unused")
fun setEarDetection(enabled: Boolean) {
if (config.earDetectionEnabled != enabled) {
config.earDetectionEnabled = enabled
@@ -2563,17 +2614,6 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
isHeadTrackingActive = false
}
fun shouldTakeOverBasedOnAirPodsState(connectionState: String): Boolean {
if (CrossDevice.isAvailable) return true
return when (connectionState) {
"Disconnected" -> config.takeoverWhenDisconnected
"Idle" -> config.takeoverWhenIdle
"Music" -> config.takeoverWhenMusic
"Call", "Ringing", "Hanging Up" -> config.takeoverWhenCall
else -> false
}
}
}
private fun Int.dpToPx(): Int {

View File

@@ -21,7 +21,6 @@ import kotlin.math.max
import kotlin.math.min
import kotlin.math.pow
@RequiresApi(Build.VERSION_CODES.Q)
class GestureDetector(
private val airPodsService: AirPodsService
) {

View File

@@ -12,8 +12,7 @@ import androidx.annotation.RequiresApi
import me.kavishdevar.librepods.R
import java.util.concurrent.atomic.AtomicBoolean
@RequiresApi(Build.VERSION_CODES.Q)
class GestureFeedback(private val context: Context) {
class GestureFeedback(context: Context) {
private val TAG = "GestureFeedback"
@@ -25,8 +24,7 @@ class GestureFeedback(private val context: Context) {
AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_ASSISTANCE_ACCESSIBILITY)
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
.setFlags(AudioAttributes.FLAG_LOW_LATENCY or
AudioAttributes.FLAG_AUDIBILITY_ENFORCED)
.setFlags(AudioAttributes.FLAG_AUDIBILITY_ENFORCED)
.build()
)
.build()