diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 05f1c78..c74a56b 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -13,8 +13,8 @@ android { applicationId = "me.kavishdevar.librepods" minSdk = 28 targetSdk = 35 - versionCode = 4 - versionName = "0.1.0" + versionCode = 5 + versionName = "0.1.0-rc.2" } buildTypes { diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 4c1f11b..7249171 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,24 +1,26 @@ + xmlns:tools="http://schemas.android.com/tools" + android:sharedUserId="android.uid.system" + android:sharedUserMaxSdkVersion="32" + tools:targetApi="33"> + + + - - + + (p_ccb); - LOGI("Original FCR mode: 0x%02x", ccb->our_cfg.fcr.mode); - - ccb->our_cfg.fcr.mode = 0; - ccb->our_cfg.fcr_present = true; - ccb->peer_cfg.fcr.mode = 0; - ccb->peer_cfg.fcr_present = true; - - LOGI("FCR mode set to Basic Mode (0) for both local and peer config, here's the new desired FCR mode: 0x%02x, and the peer's FCR mode: 0x%02x", ccb->our_cfg.fcr.mode, ccb->peer_cfg.fcr.mode); - + LOGI("l2c_fcr_chk_chan_modes hooked, returning true."); return 1; } diff --git a/android/app/src/main/java/me/kavishdevar/librepods/MainActivity.kt b/android/app/src/main/java/me/kavishdevar/librepods/MainActivity.kt index ce25b1b..505d7e3 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/MainActivity.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/MainActivity.kt @@ -186,6 +186,8 @@ fun Main() { permissions = listOf( "android.permission.BLUETOOTH_CONNECT", "android.permission.BLUETOOTH_SCAN", + "android.permission.BLUETOOTH", + "android.permission.BLUETOOTH_ADMIN", "android.permission.BLUETOOTH_ADVERTISE", "android.permission.POST_NOTIFICATIONS", "android.permission.READ_PHONE_STATE", @@ -517,16 +519,16 @@ fun PermissionsScreen( ), ) } - + if (!canDrawOverlays && basicPermissionsGranted) { Spacer(modifier = Modifier.height(12.dp)) - + Button( onClick = { val editor = context.getSharedPreferences("settings", MODE_PRIVATE).edit() editor.putBoolean("overlay_permission_skipped", true) editor.apply() - + val intent = Intent(context, MainActivity::class.java) intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) context.startActivity(intent) diff --git a/android/app/src/main/java/me/kavishdevar/librepods/screens/AppSettingsScreen.kt b/android/app/src/main/java/me/kavishdevar/librepods/screens/AppSettingsScreen.kt index 67289af..67bf3cd 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/screens/AppSettingsScreen.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/screens/AppSettingsScreen.kt @@ -71,6 +71,7 @@ import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.Font import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.navigation.NavController @@ -106,6 +107,7 @@ fun AppSettingsScreen(navController: NavController) { navController.popBackStack() }, shape = RoundedCornerShape(8.dp), + modifier = Modifier.width(180.dp) ) { Icon( Icons.AutoMirrored.Filled.KeyboardArrowLeft, @@ -121,6 +123,9 @@ fun AppSettingsScreen(navController: NavController) { color = if (isDarkTheme) Color(0xFF007AFF) else Color(0xFF3C6DF5), fontFamily = FontFamily(Font(R.font.sf_pro)) ), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.weight(1f) ) } }, @@ -142,10 +147,22 @@ fun AppSettingsScreen(navController: NavController) { val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF) val textColor = if (isDarkTheme) Color.White else Color.Black - val accentColor = if (isDarkTheme) Color(0xFF007AFF) else Color(0xFF3C6DF5) IndependentToggle("Show phone battery in widget", ServiceManager.getService()!!, "setPhoneBatteryInWidget", sharedPreferences) + Text( + text = stringResource(R.string.conversational_awareness_customization).uppercase(), + style = TextStyle( + fontSize = 14.sp, + fontWeight = FontWeight.Light, + color = (if (isSystemInDarkTheme()) Color.White else Color.Black).copy(alpha = 0.6f), + fontFamily = FontFamily(Font(R.font.sf_pro)) + ), + modifier = Modifier.padding(8.dp, bottom = 2.dp) + ) + + Spacer(modifier = Modifier.height(2.dp)) + Column ( modifier = Modifier .fillMaxWidth() @@ -170,17 +187,6 @@ fun AppSettingsScreen(navController: NavController) { val thumbColor = if (isDarkTheme) Color(0xFFFFFFFF) else Color(0xFFFFFFFF) val labelTextColor = if (isDarkTheme) Color.White else Color.Black - Text( - text = stringResource(R.string.conversational_awareness_customization), - style = TextStyle( - fontSize = 20.sp, - color = textColor - ), - modifier = Modifier - .padding(top = 12.dp, bottom = 4.dp) - ) - - var conversationalAwarenessPauseMusicEnabled by remember { mutableStateOf( sharedPreferences.getBoolean("conversational_awareness_pause_music", true) @@ -367,6 +373,70 @@ fun AppSettingsScreen(navController: NavController) { Spacer(modifier = Modifier.height(24.dp)) + Column( + modifier = Modifier + .fillMaxWidth() + .background( + backgroundColor, + RoundedCornerShape(14.dp) + ) + .padding(horizontal = 16.dp, vertical = 8.dp) + ) { + var openDialogForControlling by remember { + mutableStateOf( + sharedPreferences.getString("qs_click_behavior", "dialog") == "dialog" + ) + } + + fun updateQsClickBehavior(enabled: Boolean) { + openDialogForControlling = enabled + sharedPreferences.edit().putString("qs_click_behavior", if (enabled) "dialog" else "cycle").apply() + } + + Row( + modifier = Modifier + .fillMaxWidth() + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { + updateQsClickBehavior(!openDialogForControlling) + }, + verticalAlignment = Alignment.CenterVertically + ) { + Column( + modifier = Modifier + .weight(1f) + .padding(vertical = 16.dp) + .padding(end = 4.dp) + ) { + Text( + text = "Open dialog for controlling", + fontSize = 16.sp, + color = textColor + ) + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = if (openDialogForControlling) + "If disabled, clicking on the QS will cycle through modes" + else "If enabled, it will show a dialog for controlling noise control mode and conversational awareness", + fontSize = 14.sp, + color = textColor.copy(0.6f), + lineHeight = 16.sp, + ) + } + + StyledSwitch( + checked = openDialogForControlling, + onCheckedChange = { + updateQsClickBehavior(it) + } + ) + } + } + + Spacer(modifier = Modifier.height(24.dp)) + Button( onClick = { showResetDialog = true }, modifier = Modifier diff --git a/android/app/src/main/java/me/kavishdevar/librepods/screens/HeadTrackingScreen.kt b/android/app/src/main/java/me/kavishdevar/librepods/screens/HeadTrackingScreen.kt index b60666b..69f4a20 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/screens/HeadTrackingScreen.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/screens/HeadTrackingScreen.kt @@ -39,6 +39,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.KeyboardArrowLeft @@ -92,6 +93,7 @@ import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.rememberTextMeasurer import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -138,6 +140,7 @@ fun HeadTrackingScreen(navController: NavController) { if (ServiceManager.getService()?.isHeadTrackingActive == true) ServiceManager.getService()?.stopHeadTracking() }, shape = RoundedCornerShape(8.dp), + modifier = Modifier.width(180.dp) ) { Icon( Icons.AutoMirrored.Filled.KeyboardArrowLeft, @@ -153,6 +156,9 @@ fun HeadTrackingScreen(navController: NavController) { color = if (isDarkTheme) Color(0xFF007AFF) else Color(0xFF3C6DF5), fontFamily = FontFamily(Font(R.font.sf_pro)) ), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.weight(1f) ) } }, diff --git a/android/app/src/main/java/me/kavishdevar/librepods/services/AirPodsQSService.kt b/android/app/src/main/java/me/kavishdevar/librepods/services/AirPodsQSService.kt index 277a221..4106256 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/services/AirPodsQSService.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/services/AirPodsQSService.kt @@ -31,6 +31,8 @@ import android.service.quicksettings.Tile import android.service.quicksettings.TileService import android.util.Log import androidx.annotation.RequiresApi +import androidx.compose.material3.ExperimentalMaterial3Api +import me.kavishdevar.librepods.MainActivity import me.kavishdevar.librepods.QuickSettingsDialogActivity import me.kavishdevar.librepods.R import me.kavishdevar.librepods.utils.AirPodsNotifications @@ -260,4 +262,42 @@ class AirPodsQSService : TileService() { else -> R.drawable.airpods } } + + @ExperimentalMaterial3Api + override fun onTileAdded() { + super.onTileAdded() + Log.d("AirPodsQSService", "Tile added") + + val intent = Intent(this, MainActivity::class.java).apply { + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } + } + + @ExperimentalMaterial3Api + fun openMainActivity() { + Log.d("AirPodsQSService", "Opening MainActivity") + + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + val pendingIntent = PendingIntent.getActivity( + this, + 0, + Intent(this, MainActivity::class.java).apply { + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + }, + PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT + ) + startActivityAndCollapse(pendingIntent) + } else { + @Suppress("DEPRECATION") + val intent = Intent(this, MainActivity::class.java).apply { + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } + startActivityAndCollapse(intent) + } + Log.d("AirPodsQSService", "Called startActivityAndCollapse for MainActivity") + } catch (e: Exception) { + Log.e("AirPodsQSService", "Error launching MainActivity: $e") + } + } } diff --git a/android/app/src/main/java/me/kavishdevar/librepods/services/AirPodsService.kt b/android/app/src/main/java/me/kavishdevar/librepods/services/AirPodsService.kt index 6102a6e..9b88369 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/services/AirPodsService.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/services/AirPodsService.kt @@ -26,12 +26,14 @@ import android.app.NotificationManager import android.app.PendingIntent import android.app.Service import android.appwidget.AppWidgetManager +import android.bluetooth.BluetoothAssignedNumbers.APPLE import android.bluetooth.BluetoothDevice import android.bluetooth.BluetoothManager import android.bluetooth.BluetoothProfile import android.bluetooth.BluetoothSocket import android.content.BroadcastReceiver import android.content.ComponentName +import android.content.ContentResolver import android.content.Context import android.content.Intent import android.content.IntentFilter @@ -39,6 +41,7 @@ import android.content.SharedPreferences import android.content.pm.PackageManager import android.content.res.Resources import android.media.AudioManager +import android.net.Uri import android.os.BatteryManager import android.os.Binder import android.os.Build @@ -46,6 +49,7 @@ import android.os.Handler import android.os.IBinder import android.os.Looper import android.os.ParcelUuid +import android.os.UserHandle import android.provider.Settings import android.telecom.TelecomManager import android.telephony.PhoneStateListener @@ -85,6 +89,26 @@ import me.kavishdevar.librepods.utils.LongPressPackets import me.kavishdevar.librepods.utils.MediaController import me.kavishdevar.librepods.utils.PopupWindow import me.kavishdevar.librepods.utils.RadareOffsetFinder +import me.kavishdevar.librepods.utils.SystemApisUtils +import me.kavishdevar.librepods.utils.SystemApisUtils.DEVICE_TYPE_UNTETHERED_HEADSET +import me.kavishdevar.librepods.utils.SystemApisUtils.METADATA_COMPANION_APP +import me.kavishdevar.librepods.utils.SystemApisUtils.METADATA_DEVICE_TYPE +import me.kavishdevar.librepods.utils.SystemApisUtils.METADATA_MAIN_BATTERY +import me.kavishdevar.librepods.utils.SystemApisUtils.METADATA_MAIN_ICON +import me.kavishdevar.librepods.utils.SystemApisUtils.METADATA_MANUFACTURER_NAME +import me.kavishdevar.librepods.utils.SystemApisUtils.METADATA_MODEL_NAME +import me.kavishdevar.librepods.utils.SystemApisUtils.METADATA_UNTETHERED_CASE_BATTERY +import me.kavishdevar.librepods.utils.SystemApisUtils.METADATA_UNTETHERED_CASE_CHARGING +import me.kavishdevar.librepods.utils.SystemApisUtils.METADATA_UNTETHERED_CASE_ICON +import me.kavishdevar.librepods.utils.SystemApisUtils.METADATA_UNTETHERED_CASE_LOW_BATTERY_THRESHOLD +import me.kavishdevar.librepods.utils.SystemApisUtils.METADATA_UNTETHERED_LEFT_BATTERY +import me.kavishdevar.librepods.utils.SystemApisUtils.METADATA_UNTETHERED_LEFT_CHARGING +import me.kavishdevar.librepods.utils.SystemApisUtils.METADATA_UNTETHERED_LEFT_ICON +import me.kavishdevar.librepods.utils.SystemApisUtils.METADATA_UNTETHERED_LEFT_LOW_BATTERY_THRESHOLD +import me.kavishdevar.librepods.utils.SystemApisUtils.METADATA_UNTETHERED_RIGHT_BATTERY +import me.kavishdevar.librepods.utils.SystemApisUtils.METADATA_UNTETHERED_RIGHT_CHARGING +import me.kavishdevar.librepods.utils.SystemApisUtils.METADATA_UNTETHERED_RIGHT_ICON +import me.kavishdevar.librepods.utils.SystemApisUtils.METADATA_UNTETHERED_RIGHT_LOW_BATTERY_THRESHOLD import me.kavishdevar.librepods.utils.isHeadTrackingData import me.kavishdevar.librepods.widgets.BatteryWidget import me.kavishdevar.librepods.widgets.NoiseControlWidget @@ -295,7 +319,7 @@ class AirPodsService : Service() { object BatteryChangedIntentReceiver : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent) { if (intent.action == Intent.ACTION_BATTERY_CHANGED) { - ServiceManager.getService()?.updateBatteryWidget() + ServiceManager.getService()?.updateBattery() } else if (intent.action == AirPodsNotifications.DISCONNECT_RECEIVERS) { try { context?.unregisterReceiver(this) @@ -308,7 +332,7 @@ class AirPodsService : Service() { fun setPhoneBatteryInWidget(enabled: Boolean) { widgetMobileBatteryEnabled = enabled - updateBatteryWidget() + updateBattery() } @OptIn(ExperimentalMaterial3Api::class) @@ -369,7 +393,8 @@ class AirPodsService : Service() { } @OptIn(ExperimentalMaterial3Api::class) - fun updateBatteryWidget() { + fun updateBattery() { + // Update widget val appWidgetManager = AppWidgetManager.getInstance(this) val componentName = ComponentName(this, BatteryWidget::class.java) val widgetIds = appWidgetManager.getAppWidgetIds(componentName) @@ -463,6 +488,43 @@ class AirPodsService : Service() { } } appWidgetManager.updateAppWidget(widgetIds, remoteViews) + + // set metadata + device?.let { + SystemApisUtils.setMetadata( + it, + it.METADATA_UNTETHERED_CASE_BATTERY, + batteryNotification.getBattery().find { it.component == BatteryComponent.CASE }?.level.toString().toByteArray() + ) + SystemApisUtils.setMetadata( + it, + it.METADATA_UNTETHERED_CASE_CHARGING, + (if (batteryNotification.getBattery().find { it.component == BatteryComponent.CASE}?.status == BatteryStatus.CHARGING) "1".toByteArray() else "0".toByteArray()) + ) + SystemApisUtils.setMetadata( + it, + it.METADATA_UNTETHERED_LEFT_BATTERY, + batteryNotification.getBattery().find { it.component == BatteryComponent.LEFT }?.level.toString().toByteArray() + ) + SystemApisUtils.setMetadata( + it, + it.METADATA_UNTETHERED_LEFT_CHARGING, + (if (batteryNotification.getBattery().find { it.component == BatteryComponent.LEFT}?.status == BatteryStatus.CHARGING) "1".toByteArray() else "0".toByteArray()) + ) + SystemApisUtils.setMetadata( + it, + it.METADATA_UNTETHERED_RIGHT_BATTERY, + batteryNotification.getBattery().find { it.component == BatteryComponent.RIGHT }?.level.toString().toByteArray() + ) + SystemApisUtils.setMetadata( + it, + it.METADATA_UNTETHERED_RIGHT_CHARGING, + (if (batteryNotification.getBattery().find { it.component == BatteryComponent.RIGHT}?.status == BatteryStatus.CHARGING) "1".toByteArray() else "0".toByteArray()) + ) + } + + // broadcast +// broadcastBatteryInformation() } fun updateNoiseControlWidget() { @@ -678,6 +740,168 @@ class AirPodsService : Service() { private lateinit var connectionReceiver: BroadcastReceiver private lateinit var disconnectionReceiver: BroadcastReceiver + private fun resToUri(resId: Int): Uri? { + return try { + Uri.Builder() + .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE) + .authority("me.kavishdevar.librepods") + .appendPath(applicationContext.resources.getResourceTypeName(resId)) + .appendPath(applicationContext.resources.getResourceEntryName(resId)) + .build() + } catch (e: Resources.NotFoundException) { + null + } + } + + private val VENDOR_SPECIFIC_HEADSET_EVENT_IPHONEACCEV = "+IPHONEACCEV" + private val VENDOR_SPECIFIC_HEADSET_EVENT_IPHONEACCEV_BATTERY_LEVEL = 1 + private val APPLE = 0x004C + private val ACTION_BATTERY_LEVEL_CHANGED = "android.bluetooth.device.action.BATTERY_LEVEL_CHANGED" + private val EXTRA_BATTERY_LEVEL = "android.bluetooth.device.extra.BATTERY_LEVEL" + private val PACKAGE_ASI = "com.google.android.settings.intelligence" + private val ACTION_ASI_UPDATE_BLUETOOTH_DATA = "batterywidget.impl.action.update_bluetooth_data" + + @Suppress("MissingPermission") + fun broadcastBatteryInformation() { + if (device == null) return + + val batteryList = batteryNotification.getBattery() + val leftBattery = batteryList.find { it.component == BatteryComponent.LEFT } + val rightBattery = batteryList.find { it.component == BatteryComponent.RIGHT } + + // Calculate unified battery level (minimum of left and right) + val batteryUnified = minOf( + leftBattery?.level ?: 100, + rightBattery?.level ?: 100 + ) + + // Check charging status + val isLeftCharging = leftBattery?.status == BatteryStatus.CHARGING + val isRightCharging = rightBattery?.status == BatteryStatus.CHARGING + val isChargingMain = isLeftCharging && isRightCharging + + // Create arguments for vendor-specific event + val arguments = arrayOf( + 1, // Number of key/value pairs + VENDOR_SPECIFIC_HEADSET_EVENT_IPHONEACCEV_BATTERY_LEVEL, // IndicatorType: Battery Level + batteryUnified // Battery Level + ) + + // 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) + putExtra(BluetoothDevice.EXTRA_DEVICE, device) + putExtra(BluetoothDevice.EXTRA_NAME, device?.name) + addCategory("${android.bluetooth.BluetoothHeadset.VENDOR_SPECIFIC_HEADSET_EVENT_COMPANY_ID_CATEGORY}.$APPLE") + } + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + sendBroadcastAsUser( + intent, + UserHandle.getUserHandleForUid(-1), + Manifest.permission.BLUETOOTH_CONNECT + ) + } else { + sendBroadcastAsUser(intent, UserHandle.getUserHandleForUid(-1)) + } + } catch (e: Exception) { + Log.e("AirPodsService", "Failed to send vendor-specific event: ${e.message}") + } + + // Broadcast battery level changes + val batteryIntent = Intent(ACTION_BATTERY_LEVEL_CHANGED).apply { + putExtra(BluetoothDevice.EXTRA_DEVICE, device) + putExtra(EXTRA_BATTERY_LEVEL, batteryUnified) + } + + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + sendBroadcast(batteryIntent, Manifest.permission.BLUETOOTH_CONNECT) + } else { + sendBroadcastAsUser(batteryIntent, UserHandle.getUserHandleForUid(-1)) + } + } catch (e: Exception) { + Log.e("AirPodsService", "Failed to send battery level broadcast: ${e.message}") + } + + // Update Android Settings Intelligence's battery widget + val statusIntent = Intent(ACTION_ASI_UPDATE_BLUETOOTH_DATA).apply { + setPackage(PACKAGE_ASI) + putExtra(ACTION_BATTERY_LEVEL_CHANGED, intent) + } + + try { + sendBroadcastAsUser(statusIntent, UserHandle.getUserHandleForUid(-1)) + } catch (e: Exception) { + Log.e("AirPodsService", "Failed to send ASI battery level broadcast: ${e.message}") + } + + Log.d("AirPodsService", "Broadcast battery level $batteryUnified% to system") + } + + private fun setMetadatas(d: BluetoothDevice) { + d.let{ device -> + val metadataSet = SystemApisUtils.setMetadata( + device, + device.METADATA_MAIN_ICON, + resToUri(R.drawable.pro_2).toString().toByteArray() + ) && + SystemApisUtils.setMetadata( + device, + device.METADATA_MODEL_NAME, + "AirPods Pro (2 Gen.)".toByteArray() + ) && + SystemApisUtils.setMetadata( + device, + device.METADATA_DEVICE_TYPE, + device.DEVICE_TYPE_UNTETHERED_HEADSET.toByteArray() + ) && + SystemApisUtils.setMetadata( + device, + device.METADATA_UNTETHERED_CASE_ICON, + resToUri(R.drawable.pro_2_case).toString().toByteArray() + ) && + SystemApisUtils.setMetadata( + device, + device.METADATA_UNTETHERED_RIGHT_ICON, + resToUri(R.drawable.pro_2_right).toString().toByteArray() + ) && + SystemApisUtils.setMetadata( + device, + device.METADATA_UNTETHERED_LEFT_ICON, + resToUri(R.drawable.pro_2_left).toString().toByteArray() + ) && + SystemApisUtils.setMetadata( + device, + device.METADATA_MANUFACTURER_NAME, + "Apple".toByteArray() + ) && + SystemApisUtils.setMetadata( + device, + device.METADATA_COMPANION_APP, + "me.kavisdevar.librepods".toByteArray() + ) && + SystemApisUtils.setMetadata( + device, + device.METADATA_UNTETHERED_CASE_LOW_BATTERY_THRESHOLD, + "20".toByteArray() + ) && + SystemApisUtils.setMetadata( + device, + device.METADATA_UNTETHERED_LEFT_LOW_BATTERY_THRESHOLD, + "20".toByteArray() + ) && + SystemApisUtils.setMetadata( + device, + device.METADATA_UNTETHERED_RIGHT_LOW_BATTERY_THRESHOLD, + "20".toByteArray() + ) + Log.d("AirPodsService", "Metadata set: $metadataSet") + } + } + @SuppressLint("InlinedApi", "MissingPermission", "UnspecifiedRegisterReceiverFlag") override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { Log.d("AirPodsService", "Service started") @@ -778,6 +1002,8 @@ class AirPodsService : Service() { Log.d("AirPodsService", "$name connected") showPopup(this@AirPodsService, name.toString()) connectToSocket(device!!) + Log.d("AirPodsService", "Setting metadata") + setMetadatas(device!!) isConnectedLocally = true macAddress = device!!.address sharedPreferences.edit { @@ -850,6 +1076,7 @@ class AirPodsService : Service() { if (connectedDevices.isNotEmpty()) { if (!CrossDevice.isAvailable) { connectToSocket(device) + setMetadatas(device) macAddress = device.address sharedPreferences.edit { putString("mac_address", macAddress) @@ -1186,7 +1413,7 @@ class AirPodsService : Service() { ArrayList(batteryNotification.getBattery()) ) }) - updateBatteryWidget() + updateBattery() updateNotificationContent( true, this@AirPodsService.getSharedPreferences( diff --git a/android/app/src/main/java/me/kavishdevar/librepods/utils/CrossDevice.kt b/android/app/src/main/java/me/kavishdevar/librepods/utils/CrossDevice.kt index bfbc611..deb1f29 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/utils/CrossDevice.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/utils/CrossDevice.kt @@ -238,7 +238,7 @@ object CrossDevice { batteryBytes = trimmedPacket ServiceManager.getService()?.batteryNotification?.setBattery(trimmedPacket) Log.d("CrossDevice", "Battery data: ${ServiceManager.getService()?.batteryNotification?.getBattery()[0]?.level}") - ServiceManager.getService()?.updateBatteryWidget() + ServiceManager.getService()?.updateBattery() ServiceManager.getService()?.sendBatteryBroadcast() ServiceManager.getService()?.sendBatteryNotification() } else if (ServiceManager.getService()?.ancNotification?.isANCData(trimmedPacket) == true) { diff --git a/android/app/src/main/java/me/kavishdevar/librepods/utils/RadareOffsetFinder.kt b/android/app/src/main/java/me/kavishdevar/librepods/utils/RadareOffsetFinder.kt index 2ab4159..5e6381a 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/utils/RadareOffsetFinder.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/utils/RadareOffsetFinder.kt @@ -416,9 +416,9 @@ class RadareOffsetFinder(context: Context) { Log.e(TAG, "rabin2 command failed with exit code $exitCode") } - // findAndSaveL2cuProcessCfgReqOffset(libraryPath, envSetup) - // findAndSaveL2cCsmConfigOffset(libraryPath, envSetup) - findAndSaveL2cuSendPeerInfoReqOffset(libraryPath, envSetup) +// findAndSaveL2cuProcessCfgReqOffset(libraryPath, envSetup) +// findAndSaveL2cCsmConfigOffset(libraryPath, envSetup) +// findAndSaveL2cuSendPeerInfoReqOffset(libraryPath, envSetup) } catch (e: Exception) { Log.e(TAG, "Failed to find function offset", e) diff --git a/android/app/src/main/java/me/kavishdevar/librepods/utils/SystemAPIUtils.kt b/android/app/src/main/java/me/kavishdevar/librepods/utils/SystemAPIUtils.kt index cb2a9b8..694fc86 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/utils/SystemAPIUtils.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/utils/SystemAPIUtils.kt @@ -1,6 +1,8 @@ package me.kavishdevar.librepods.utils import android.bluetooth.BluetoothDevice +import android.util.Log +import org.lsposed.hiddenapibypass.HiddenApiBypass object SystemApisUtils { @@ -282,4 +284,23 @@ object SystemApisUtils { const val ACTION_BLUETOOTH_HANDSFREE_BATTERY_CHANGED = "android.intent.action.BLUETOOTH_HANDSFREE_BATTERY_CHANGED" const val EXTRA_SHOW_BT_HANDSFREE_BATTERY = "android.intent.extra.show_bluetooth_handsfree_battery" const val EXTRA_BT_HANDSFREE_BATTERY_LEVEL = "android.intent.extra.bluetooth_handsfree_battery_level" + + /** + * Helper method to set metadata using HiddenApiBypass + */ + fun setMetadata(device: BluetoothDevice, key: Int, value: ByteArray): Boolean { + return try { + val result = HiddenApiBypass.invoke( + BluetoothDevice::class.java, + device, + "setMetadata", + key, + value + ) as Boolean + result + } catch (e: Exception) { + Log.e("SystemApisUtils", "Failed to set metadata for key $key", e) + false + } + } } diff --git a/android/app/src/main/java/me/kavishdevar/librepods/widgets/BatteryWidget.kt b/android/app/src/main/java/me/kavishdevar/librepods/widgets/BatteryWidget.kt index 279bf46..ae4c33c 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/widgets/BatteryWidget.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/widgets/BatteryWidget.kt @@ -19,15 +19,9 @@ package me.kavishdevar.librepods.widgets -import android.app.PendingIntent import android.appwidget.AppWidgetManager import android.appwidget.AppWidgetProvider import android.content.Context -import android.content.Intent -import android.widget.RemoteViews -import androidx.compose.material3.ExperimentalMaterial3Api -import me.kavishdevar.librepods.MainActivity -import me.kavishdevar.librepods.R import me.kavishdevar.librepods.services.ServiceManager class BatteryWidget : AppWidgetProvider() { @@ -36,6 +30,6 @@ class BatteryWidget : AppWidgetProvider() { appWidgetManager: AppWidgetManager, appWidgetIds: IntArray ) { - ServiceManager.getService()?.updateBatteryWidget() + ServiceManager.getService()?.updateBattery() } } diff --git a/android/app/src/main/java/me/kavishdevar/librepods/widgets/NoiseControlWidget.kt b/android/app/src/main/java/me/kavishdevar/librepods/widgets/NoiseControlWidget.kt index b791f3c..710257f 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/widgets/NoiseControlWidget.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/widgets/NoiseControlWidget.kt @@ -24,6 +24,7 @@ import android.appwidget.AppWidgetManager import android.appwidget.AppWidgetProvider import android.content.Context import android.content.Intent +import android.util.Log import android.widget.RemoteViews import me.kavishdevar.librepods.R import me.kavishdevar.librepods.services.ServiceManager @@ -77,6 +78,7 @@ class NoiseControlWidget : AppWidgetProvider() { super.onReceive(context, intent) if (intent.action == "ACTION_SET_ANC_MODE") { val mode = intent.getIntExtra("ANC_MODE", 1) + Log.d("NoiseControlWidget", "Setting ANC mode to $mode") ServiceManager.getService()?.setANCMode(mode) } } diff --git a/android/app/src/main/res/drawable/pro_2_left.png b/android/app/src/main/res/drawable/pro_2_left.png new file mode 100644 index 0000000..a50b7a0 Binary files /dev/null and b/android/app/src/main/res/drawable/pro_2_left.png differ diff --git a/android/app/src/main/res/drawable/pro_2_right.png b/android/app/src/main/res/drawable/pro_2_right.png new file mode 100644 index 0000000..589e254 Binary files /dev/null and b/android/app/src/main/res/drawable/pro_2_right.png differ