From fda343ca39724cd9eb0134419dcde1d1b1aa6914 Mon Sep 17 00:00:00 2001 From: Kavish Devar Date: Tue, 7 Jan 2025 23:22:16 +0530 Subject: [PATCH] fix for pre-tiramisu android versions --- .../kavishdevar/aln/CustomDeviceActivity.kt | 13 +++- .../java/me/kavishdevar/aln/MainActivity.kt | 11 +++- .../aln/composables/NoiseControlSettings.kt | 66 +++++++++++++++++-- .../aln/screens/AirPodsSettingsScreen.kt | 2 +- .../me/kavishdevar/aln/screens/DebugScreen.kt | 4 +- .../aln/services/AirPodsQSService.kt | 17 +++-- .../aln/services/AirPodsService.kt | 59 ++++++++++------- .../java/me/kavishdevar/aln/utils/Window.kt | 1 - 8 files changed, 135 insertions(+), 38 deletions(-) diff --git a/android/app/src/main/java/me/kavishdevar/aln/CustomDeviceActivity.kt b/android/app/src/main/java/me/kavishdevar/aln/CustomDeviceActivity.kt index 5f2fd5d83..e6f7780a6 100644 --- a/android/app/src/main/java/me/kavishdevar/aln/CustomDeviceActivity.kt +++ b/android/app/src/main/java/me/kavishdevar/aln/CustomDeviceActivity.kt @@ -18,6 +18,7 @@ package me.kavishdevar.aln +import android.Manifest import android.annotation.SuppressLint import android.bluetooth.BluetoothDevice import android.bluetooth.BluetoothDevice.TRANSPORT_LE @@ -25,11 +26,13 @@ import android.bluetooth.BluetoothGatt import android.bluetooth.BluetoothGattCallback import android.bluetooth.BluetoothGattCharacteristic import android.bluetooth.BluetoothManager +import android.os.Build import android.os.Bundle import android.util.Log import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge +import androidx.annotation.RequiresPermission import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize @@ -49,7 +52,7 @@ import org.lsposed.hiddenapibypass.HiddenApiBypass import java.util.UUID class CustomDevice : ComponentActivity() { - @SuppressLint("MissingPermission", "CoroutineCreationDuringComposition", "NewApi") + @SuppressLint("MissingPermission", "CoroutineCreationDuringComposition") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() @@ -152,7 +155,7 @@ class CustomDevice : ComponentActivity() { } } -@SuppressLint("MissingPermission", "NewApi") +@RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) fun sendWriteRequest( gatt: BluetoothGatt, characteristicUuid: String, @@ -177,6 +180,10 @@ fun sendWriteRequest( // Send the write request - val success = gatt.writeCharacteristic(characteristic, value, BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT) + val success = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + gatt.writeCharacteristic(characteristic, value, BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT) + } else { + gatt.writeCharacteristic(characteristic) + } Log.d("GATT", "Write request sent $success to UUID: $characteristicUuid") } \ No newline at end of file diff --git a/android/app/src/main/java/me/kavishdevar/aln/MainActivity.kt b/android/app/src/main/java/me/kavishdevar/aln/MainActivity.kt index bdc520ac4..f819939bc 100644 --- a/android/app/src/main/java/me/kavishdevar/aln/MainActivity.kt +++ b/android/app/src/main/java/me/kavishdevar/aln/MainActivity.kt @@ -22,9 +22,11 @@ import android.annotation.SuppressLint import android.content.BroadcastReceiver import android.content.ComponentName import android.content.Context +import android.content.Context.RECEIVER_EXPORTED import android.content.Intent import android.content.IntentFilter import android.content.ServiceConnection +import android.os.Build import android.os.Bundle import android.os.IBinder import android.util.Log @@ -110,7 +112,7 @@ class MainActivity : ComponentActivity() { } } -@SuppressLint("MissingPermission", "InlinedApi") +@SuppressLint("MissingPermission", "InlinedApi", "UnspecifiedRegisterReceiverFlag") @OptIn(ExperimentalPermissionsApi::class) @Composable fun Main() { @@ -146,7 +148,12 @@ fun Main() { addAction(AirPodsNotifications.AIRPODS_CONNECTION_DETECTED) } Log.d("MainActivity", "Registering Receiver") - context.registerReceiver(connectionStatusReceiver, filter, Context.RECEIVER_EXPORTED) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + context.registerReceiver(connectionStatusReceiver, filter, RECEIVER_EXPORTED) + } else { + context.registerReceiver(connectionStatusReceiver, filter) + } Log.d("MainActivity", "Registered Receiver") NavHost( diff --git a/android/app/src/main/java/me/kavishdevar/aln/composables/NoiseControlSettings.kt b/android/app/src/main/java/me/kavishdevar/aln/composables/NoiseControlSettings.kt index 4e1689e07..6515c374e 100644 --- a/android/app/src/main/java/me/kavishdevar/aln/composables/NoiseControlSettings.kt +++ b/android/app/src/main/java/me/kavishdevar/aln/composables/NoiseControlSettings.kt @@ -23,20 +23,29 @@ import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter +import android.content.SharedPreferences import android.os.Build +import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.gestures.draggable +import androidx.compose.foundation.gestures.rememberDraggableState import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.VerticalDivider import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -50,19 +59,36 @@ import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import me.kavishdevar.aln.R import me.kavishdevar.aln.services.AirPodsService import me.kavishdevar.aln.utils.AirPodsNotifications import me.kavishdevar.aln.utils.NoiseControlMode +import kotlin.math.roundToInt @SuppressLint("UnspecifiedRegisterReceiverFlag") @Composable fun NoiseControlSettings(service: AirPodsService) { val context = LocalContext.current val sharedPreferences = context.getSharedPreferences("settings", Context.MODE_PRIVATE) - val offListeningMode = sharedPreferences.getBoolean("off_listening_mode", true) + val offListeningMode = remember { mutableStateOf(sharedPreferences.getBoolean("off_listening_mode", true)) } + + val preferenceChangeListener = remember { + SharedPreferences.OnSharedPreferenceChangeListener { _, key -> + if (key == "off_listening_mode") { + offListeningMode.value = sharedPreferences.getBoolean("off_listening_mode", true) + } + } + } + + DisposableEffect(Unit) { + sharedPreferences.registerOnSharedPreferenceChangeListener(preferenceChangeListener) + onDispose { + sharedPreferences.unregisterOnSharedPreferenceChangeListener(preferenceChangeListener) + } + } val isDarkTheme = isSystemInDarkTheme() val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFE3E3E8) @@ -71,18 +97,28 @@ fun NoiseControlSettings(service: AirPodsService) { val selectedBackground = if (isDarkTheme) Color(0xFF5C5A5F) else Color(0xFFFFFFFF) val noiseControlMode = remember { mutableStateOf(NoiseControlMode.OFF) } + val selectedOffset = remember { mutableFloatStateOf(0f) } + val animatedOffset = animateFloatAsState(targetValue = selectedOffset.floatValue) val d1a = remember { mutableFloatStateOf(0f) } val d2a = remember { mutableFloatStateOf(0f) } val d3a = remember { mutableFloatStateOf(0f) } + val boxWidth = 200.dp.value + fun onModeSelected(mode: NoiseControlMode, received: Boolean = false) { - if (!received && !offListeningMode && mode == NoiseControlMode.OFF) { + if (!received && !offListeningMode.value && mode == NoiseControlMode.OFF) { noiseControlMode.value = NoiseControlMode.ADAPTIVE } else { noiseControlMode.value = mode } if (!received) service.setANCMode(mode.ordinal + 1) + selectedOffset.floatValue = when (noiseControlMode.value) { + NoiseControlMode.NOISE_CANCELLATION -> 3 * boxWidth + NoiseControlMode.OFF -> 0f + NoiseControlMode.ADAPTIVE -> 2 * boxWidth + NoiseControlMode.TRANSPARENCY -> 1 * boxWidth + } when (noiseControlMode.value) { NoiseControlMode.NOISE_CANCELLATION -> { d1a.floatValue = 1f @@ -107,6 +143,15 @@ fun NoiseControlSettings(service: AirPodsService) { } } + LaunchedEffect(noiseControlMode.value) { + selectedOffset.value = when (noiseControlMode.value) { + NoiseControlMode.NOISE_CANCELLATION -> 3 * boxWidth + NoiseControlMode.OFF -> 0f + NoiseControlMode.ADAPTIVE -> 2 * boxWidth + NoiseControlMode.TRANSPARENCY -> 1 * boxWidth + } + } + val noiseControlReceiver = remember { object : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { @@ -154,13 +199,26 @@ fun NoiseControlSettings(service: AirPodsService) { .fillMaxWidth() .height(75.dp) .padding(8.dp) + .draggable( + orientation = Orientation.Horizontal, + state = rememberDraggableState { delta -> + selectedOffset.floatValue = (selectedOffset.floatValue + delta).coerceIn(0f, 3 * boxWidth) + } + ) ) { Row( modifier = Modifier .fillMaxWidth() .background(backgroundColor, RoundedCornerShape(14.dp)) ) { - if (offListeningMode) { + Box( + modifier = Modifier + .offset { IntOffset(animatedOffset.value.roundToInt(), 0) } + .width(boxWidth.dp) + .height(75.dp) + .background(selectedBackground, RoundedCornerShape(14.dp)) + ) + if (offListeningMode.value) { NoiseControlButton( icon = ImageBitmap.imageResource(R.drawable.noise_cancellation), onClick = { onModeSelected(NoiseControlMode.OFF) }, @@ -219,7 +277,7 @@ fun NoiseControlSettings(service: AirPodsService) { .padding(horizontal = 8.dp) .padding(top = 1.dp) ) { - if (offListeningMode) { + if (offListeningMode.value) { Text( text = "Off", style = TextStyle(fontSize = 12.sp, color = textColor), diff --git a/android/app/src/main/java/me/kavishdevar/aln/screens/AirPodsSettingsScreen.kt b/android/app/src/main/java/me/kavishdevar/aln/screens/AirPodsSettingsScreen.kt index aba4bbeee..bfec98aad 100644 --- a/android/app/src/main/java/me/kavishdevar/aln/screens/AirPodsSettingsScreen.kt +++ b/android/app/src/main/java/me/kavishdevar/aln/screens/AirPodsSettingsScreen.kt @@ -86,7 +86,7 @@ import me.kavishdevar.aln.ui.theme.ALNTheme import me.kavishdevar.aln.utils.AirPodsNotifications @OptIn(ExperimentalMaterial3Api::class, ExperimentalHazeMaterialsApi::class) -@SuppressLint("MissingPermission", "NewApi") +@SuppressLint("MissingPermission") @Composable fun AirPodsSettingsScreen(dev: BluetoothDevice?, service: AirPodsService, navController: NavController, isConnected: Boolean) { diff --git a/android/app/src/main/java/me/kavishdevar/aln/screens/DebugScreen.kt b/android/app/src/main/java/me/kavishdevar/aln/screens/DebugScreen.kt index ba9caacbe..2c76372ca 100644 --- a/android/app/src/main/java/me/kavishdevar/aln/screens/DebugScreen.kt +++ b/android/app/src/main/java/me/kavishdevar/aln/screens/DebugScreen.kt @@ -88,7 +88,7 @@ import me.kavishdevar.aln.services.AirPodsService import me.kavishdevar.aln.utils.AirPodsNotifications @OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class) -@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") +@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter", "UnspecifiedRegisterReceiverFlag") @Composable fun DebugScreen(navController: NavController) { val hazeState = remember { HazeState() } @@ -158,6 +158,8 @@ fun DebugScreen(navController: NavController) { val intentFilter = IntentFilter(AirPodsNotifications.Companion.AIRPODS_DATA) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { context.registerReceiver(receiver, intentFilter, Context.RECEIVER_EXPORTED) + } else { + context.registerReceiver(receiver, intentFilter) } } diff --git a/android/app/src/main/java/me/kavishdevar/aln/services/AirPodsQSService.kt b/android/app/src/main/java/me/kavishdevar/aln/services/AirPodsQSService.kt index cf64ea255..42b26ff3d 100644 --- a/android/app/src/main/java/me/kavishdevar/aln/services/AirPodsQSService.kt +++ b/android/app/src/main/java/me/kavishdevar/aln/services/AirPodsQSService.kt @@ -23,6 +23,7 @@ import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter +import android.os.Build import android.service.quicksettings.Tile import android.service.quicksettings.TileService import android.util.Log @@ -35,7 +36,7 @@ class AirPodsQSService: TileService() { private lateinit var ancStatusReceiver: BroadcastReceiver private lateinit var availabilityReceiver: BroadcastReceiver - @SuppressLint("InlinedApi") + @SuppressLint("InlinedApi", "UnspecifiedRegisterReceiverFlag") override fun onStartListening() { super.onStartListening() currentModeIndex = (ServiceManager.getService()?.getANC()?.minus(1)) ?: -1 @@ -77,9 +78,17 @@ class AirPodsQSService: TileService() { } } - registerReceiver(ancStatusReceiver, - IntentFilter(AirPodsNotifications.Companion.ANC_DATA), RECEIVER_EXPORTED) - + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + registerReceiver( + ancStatusReceiver, + IntentFilter(AirPodsNotifications.Companion.ANC_DATA), RECEIVER_EXPORTED + ) + } else { + registerReceiver( + ancStatusReceiver, + IntentFilter(AirPodsNotifications.Companion.ANC_DATA) + ) + } qsTile.state = if (ServiceManager.getService()?.isConnected == true) Tile.STATE_ACTIVE else Tile.STATE_UNAVAILABLE val ancIndex = ServiceManager.getService()?.getANC() currentModeIndex = if (ancIndex != null) { if (ancIndex == 2) 0 else if (ancIndex == 3) 1 else if (ancIndex == 4) 2 else 2 } else 0 diff --git a/android/app/src/main/java/me/kavishdevar/aln/services/AirPodsService.kt b/android/app/src/main/java/me/kavishdevar/aln/services/AirPodsService.kt index 0a362d7c8..1e8dcb1ec 100644 --- a/android/app/src/main/java/me/kavishdevar/aln/services/AirPodsService.kt +++ b/android/app/src/main/java/me/kavishdevar/aln/services/AirPodsService.kt @@ -107,10 +107,14 @@ class AirPodsService: Service() { @Suppress("ClassName") private object bluetoothReceiver: BroadcastReceiver() { - @SuppressLint("NewApi", "MissingPermission") + @SuppressLint("MissingPermission") override fun onReceive(context: Context?, intent: Intent) { val bluetoothDevice = - intent.getParcelableExtra("android.bluetooth.device.extra.DEVICE", BluetoothDevice::class.java) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + intent.getParcelableExtra("android.bluetooth.device.extra.DEVICE", BluetoothDevice::class.java) + } else { + intent.getParcelableExtra("android.bluetooth.device.extra.DEVICE") as BluetoothDevice? + } val action = intent.action val context = context?.applicationContext val name = context?.getSharedPreferences("settings", MODE_PRIVATE)?.getString("name", bluetoothDevice?.name) @@ -148,7 +152,7 @@ class AirPodsService: Service() { notificationManager.createNotificationChannel(notificationChannel) val notification = NotificationCompat.Builder(this, "background_service_status") .setSmallIcon(R.drawable.airpods) - .setContentTitle("AirPods are not connected") + .setContentTitle("AirPods not connected") .setCategory(Notification.CATEGORY_SERVICE) .setPriority(NotificationCompat.PRIORITY_LOW) .setOngoing(true) @@ -261,24 +265,24 @@ class AirPodsService: Service() { updatedNotification = NotificationCompat.Builder(this, "background_service_status") .setSmallIcon(R.drawable.airpods) - .setContentTitle("""AirPods –${batteryList?.find { it.component == BatteryComponent.LEFT }?.let { - // if (it.status != BatteryStatus.DISCONNECTED) { + .setContentTitle("""$airpodsName –${batteryList?.find { it.component == BatteryComponent.LEFT }?.let { + if (it.status != BatteryStatus.DISCONNECTED) { " L:${if (it.status == BatteryStatus.CHARGING) "⚡" else ""} ${it.level}%" - // } else { - // "" - // } + } else { + "" + } } ?: ""}${batteryList?.find { it.component == BatteryComponent.RIGHT }?.let { - // if (it.status != BatteryStatus.DISCONNECTED) { + if (it.status != BatteryStatus.DISCONNECTED) { " R:${if (it.status == BatteryStatus.CHARGING) "⚡" else ""} ${it.level}%" - // } else { - // "" - // } + } else { + "" + } } ?: ""}${batteryList?.find { it.component == BatteryComponent.CASE }?.let { - // if (it.status != BatteryStatus.DISCONNECTED) { + if (it.status != BatteryStatus.DISCONNECTED) { " C:${if (it.status == BatteryStatus.CHARGING) "⚡" else ""} ${it.level}%" - // } else { - // "" - // } + } else { + "" + } } ?: ""}""") .setCategory(Notification.CATEGORY_SERVICE) .setPriority(NotificationCompat.PRIORITY_LOW) @@ -288,7 +292,7 @@ class AirPodsService: Service() { } else { updatedNotification = NotificationCompat.Builder(this, "background_service_status") .setSmallIcon(R.drawable.airpods) - .setContentTitle("AirPods are not connected") + .setContentTitle("AirPods not connected") .setCategory(Notification.CATEGORY_SERVICE) .setPriority(NotificationCompat.PRIORITY_LOW) .setOngoing(true) @@ -301,7 +305,7 @@ class AirPodsService: Service() { private lateinit var connectionReceiver: BroadcastReceiver private lateinit var disconnectionReceiver: BroadcastReceiver - @SuppressLint("InlinedApi", "MissingPermission") + @SuppressLint("InlinedApi", "MissingPermission", "UnspecifiedRegisterReceiverFlag") override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { Log.d("AirPodsService", "Service started") ServiceManager.setService(this) @@ -319,12 +323,20 @@ class AirPodsService: Service() { addAction("android.bluetooth.a2dp.profile.action.PLAYING_STATE_CHANGED") } - registerReceiver(bluetoothReceiver, serviceIntentFilter, RECEIVER_EXPORTED) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + registerReceiver(bluetoothReceiver, serviceIntentFilter, RECEIVER_EXPORTED) + } else { + registerReceiver(bluetoothReceiver, serviceIntentFilter) + } connectionReceiver = object: BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { if (intent?.action == AirPodsNotifications.Companion.AIRPODS_CONNECTION_DETECTED) { - device = intent.getParcelableExtra("device", BluetoothDevice::class.java)!! + device = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + intent.getParcelableExtra("device", BluetoothDevice::class.java)!! + } else { + intent.getParcelableExtra("device") as BluetoothDevice? + } val name = this@AirPodsService.getSharedPreferences("settings", MODE_PRIVATE) .getString("name", device?.name) if (this@AirPodsService.getSharedPreferences("settings", MODE_PRIVATE).getString("name", null) == null) { @@ -349,7 +361,11 @@ class AirPodsService: Service() { addAction(AirPodsNotifications.Companion.AIRPODS_CONNECTION_DETECTED) addAction(AirPodsNotifications.Companion.AIRPODS_DISCONNECTED) } - registerReceiver(connectionReceiver, deviceIntentFilter, RECEIVER_EXPORTED) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + registerReceiver(connectionReceiver, deviceIntentFilter, RECEIVER_EXPORTED) + } else { + registerReceiver(connectionReceiver, deviceIntentFilter) + } val bluetoothAdapter = getSystemService(BluetoothManager::class.java).adapter bluetoothAdapter.bondedDevices.forEach { device -> @@ -500,7 +516,6 @@ class AirPodsService: Service() { ) var justEnabledA2dp = false earReceiver = object : BroadcastReceiver() { - @SuppressLint("NewApi") override fun onReceive(context: Context, intent: Intent) { val data = intent.getByteArrayExtra("data") if (data != null && earDetectionEnabled) { diff --git a/android/app/src/main/java/me/kavishdevar/aln/utils/Window.kt b/android/app/src/main/java/me/kavishdevar/aln/utils/Window.kt index cc45c9cea..87bd24c91 100644 --- a/android/app/src/main/java/me/kavishdevar/aln/utils/Window.kt +++ b/android/app/src/main/java/me/kavishdevar/aln/utils/Window.kt @@ -48,7 +48,6 @@ class Window (context: Context) { private val mView: View @Suppress("DEPRECATION") - @SuppressLint("NewApi") private val mParams: WindowManager.LayoutParams = WindowManager.LayoutParams().apply { height = WindowManager.LayoutParams.WRAP_CONTENT width = WindowManager.LayoutParams.MATCH_PARENT