android: fix the xposed module

skip unecessary parsing the argument for debugging, just return true and hope that it works
This commit is contained in:
Kavish Devar
2025-05-08 23:50:30 +05:30
parent 91675de891
commit 58dfed97b3
15 changed files with 406 additions and 53 deletions

View File

@@ -13,8 +13,8 @@ android {
applicationId = "me.kavishdevar.librepods" applicationId = "me.kavishdevar.librepods"
minSdk = 28 minSdk = 28
targetSdk = 35 targetSdk = 35
versionCode = 4 versionCode = 5
versionName = "0.1.0" versionName = "0.1.0-rc.2"
} }
buildTypes { buildTypes {

View File

@@ -1,24 +1,26 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools"
android:sharedUserId="android.uid.system"
android:sharedUserMaxSdkVersion="32"
tools:targetApi="33">
<uses-feature
android:name="android.hardware.telephony"
android:required="false" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" /> <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE" />
<uses-permission android:name="android.permission.INTERACT_ACROSS_USERS" <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS"
tools:ignore="ProtectedPermissions" /> tools:ignore="ProtectedPermissions" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" /> <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission <uses-permission
android:name="android.permission.BLUETOOTH_PRIVILEGED" android:name="android.permission.BLUETOOTH_PRIVILEGED"
tools:ignore="ProtectedPermissions" /> tools:ignore="ProtectedPermissions" />
<uses-permission
android:name="android.permission.BATTERY_STATS"
tools:ignore="ProtectedPermissions" />
<uses-permission
android:name="android.permission.UPDATE_DEVICE_STATS"
tools:ignore="ProtectedPermissions" />
<uses-permission android:name="android.permission.BLUETOOTH" /> <uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission <uses-permission
android:name="android.permission.BLUETOOTH_SCAN" android:name="android.permission.BLUETOOTH_SCAN"
@@ -30,6 +32,8 @@
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" /> <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<protected-broadcast android:name="batterywidget.impl.action.update_bluetooth_data" />
<application <application
android:allowBackup="true" android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules" android:dataExtractionRules="@xml/data_extraction_rules"

View File

@@ -32,9 +32,7 @@
static HookFunType hook_func = nullptr; static HookFunType hook_func = nullptr;
#define L2CEVT_L2CAP_CONFIG_REQ 4 #define L2CEVT_L2CAP_CONFIG_REQ 4
#define L2CEVT_L2CAP_CONFIG_RSP 15 #define L2CEVT_L2CAP_CONFIG_RSP 15
// Define all necessary structures for the L2CAP stack
// Forward declarations for types needed by the new hook
struct t_l2c_lcb; struct t_l2c_lcb;
typedef struct _BT_HDR { typedef struct _BT_HDR {
uint16_t event; uint16_t event;
@@ -44,7 +42,6 @@ typedef struct _BT_HDR {
uint8_t data[]; uint8_t data[];
} BT_HDR; } BT_HDR;
// Define base FCR structures
typedef struct { typedef struct {
uint8_t mode; uint8_t mode;
uint8_t tx_win_sz; uint8_t tx_win_sz;
@@ -130,17 +127,7 @@ static void (*original_l2c_csm_config)(tL2C_CCB* p_ccb, uint8_t event, void* p_d
static void (*original_l2cu_send_peer_info_req)(tL2C_LCB* p_lcb, uint16_t info_type) = nullptr; static void (*original_l2cu_send_peer_info_req)(tL2C_LCB* p_lcb, uint16_t info_type) = nullptr;
uint8_t fake_l2c_fcr_chk_chan_modes(void* p_ccb) { uint8_t fake_l2c_fcr_chk_chan_modes(void* p_ccb) {
LOGI("l2c_fcr_chk_chan_modes hooked"); LOGI("l2c_fcr_chk_chan_modes hooked, returning true.");
auto* ccb = static_cast<tL2C_CCB*>(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);
return 1; return 1;
} }

View File

@@ -186,6 +186,8 @@ fun Main() {
permissions = listOf( permissions = listOf(
"android.permission.BLUETOOTH_CONNECT", "android.permission.BLUETOOTH_CONNECT",
"android.permission.BLUETOOTH_SCAN", "android.permission.BLUETOOTH_SCAN",
"android.permission.BLUETOOTH",
"android.permission.BLUETOOTH_ADMIN",
"android.permission.BLUETOOTH_ADVERTISE", "android.permission.BLUETOOTH_ADVERTISE",
"android.permission.POST_NOTIFICATIONS", "android.permission.POST_NOTIFICATIONS",
"android.permission.READ_PHONE_STATE", "android.permission.READ_PHONE_STATE",
@@ -517,16 +519,16 @@ fun PermissionsScreen(
), ),
) )
} }
if (!canDrawOverlays && basicPermissionsGranted) { if (!canDrawOverlays && basicPermissionsGranted) {
Spacer(modifier = Modifier.height(12.dp)) Spacer(modifier = Modifier.height(12.dp))
Button( Button(
onClick = { onClick = {
val editor = context.getSharedPreferences("settings", MODE_PRIVATE).edit() val editor = context.getSharedPreferences("settings", MODE_PRIVATE).edit()
editor.putBoolean("overlay_permission_skipped", true) editor.putBoolean("overlay_permission_skipped", true)
editor.apply() editor.apply()
val intent = Intent(context, MainActivity::class.java) val intent = Intent(context, MainActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
context.startActivity(intent) context.startActivity(intent)

View File

@@ -71,6 +71,7 @@ import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.Font import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight 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.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.navigation.NavController import androidx.navigation.NavController
@@ -106,6 +107,7 @@ fun AppSettingsScreen(navController: NavController) {
navController.popBackStack() navController.popBackStack()
}, },
shape = RoundedCornerShape(8.dp), shape = RoundedCornerShape(8.dp),
modifier = Modifier.width(180.dp)
) { ) {
Icon( Icon(
Icons.AutoMirrored.Filled.KeyboardArrowLeft, Icons.AutoMirrored.Filled.KeyboardArrowLeft,
@@ -121,6 +123,9 @@ fun AppSettingsScreen(navController: NavController) {
color = if (isDarkTheme) Color(0xFF007AFF) else Color(0xFF3C6DF5), color = if (isDarkTheme) Color(0xFF007AFF) else Color(0xFF3C6DF5),
fontFamily = FontFamily(Font(R.font.sf_pro)) 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 backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
val textColor = if (isDarkTheme) Color.White else Color.Black 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) 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 ( Column (
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@@ -170,17 +187,6 @@ fun AppSettingsScreen(navController: NavController) {
val thumbColor = if (isDarkTheme) Color(0xFFFFFFFF) else Color(0xFFFFFFFF) val thumbColor = if (isDarkTheme) Color(0xFFFFFFFF) else Color(0xFFFFFFFF)
val labelTextColor = if (isDarkTheme) Color.White else Color.Black 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 { var conversationalAwarenessPauseMusicEnabled by remember {
mutableStateOf( mutableStateOf(
sharedPreferences.getBoolean("conversational_awareness_pause_music", true) sharedPreferences.getBoolean("conversational_awareness_pause_music", true)
@@ -367,6 +373,70 @@ fun AppSettingsScreen(navController: NavController) {
Spacer(modifier = Modifier.height(24.dp)) 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( Button(
onClick = { showResetDialog = true }, onClick = { showResetDialog = true },
modifier = Modifier modifier = Modifier

View File

@@ -39,6 +39,7 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowLeft 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.font.FontWeight
import androidx.compose.ui.text.rememberTextMeasurer import androidx.compose.ui.text.rememberTextMeasurer
import androidx.compose.ui.text.style.TextAlign 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.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
@@ -138,6 +140,7 @@ fun HeadTrackingScreen(navController: NavController) {
if (ServiceManager.getService()?.isHeadTrackingActive == true) ServiceManager.getService()?.stopHeadTracking() if (ServiceManager.getService()?.isHeadTrackingActive == true) ServiceManager.getService()?.stopHeadTracking()
}, },
shape = RoundedCornerShape(8.dp), shape = RoundedCornerShape(8.dp),
modifier = Modifier.width(180.dp)
) { ) {
Icon( Icon(
Icons.AutoMirrored.Filled.KeyboardArrowLeft, Icons.AutoMirrored.Filled.KeyboardArrowLeft,
@@ -153,6 +156,9 @@ fun HeadTrackingScreen(navController: NavController) {
color = if (isDarkTheme) Color(0xFF007AFF) else Color(0xFF3C6DF5), color = if (isDarkTheme) Color(0xFF007AFF) else Color(0xFF3C6DF5),
fontFamily = FontFamily(Font(R.font.sf_pro)) fontFamily = FontFamily(Font(R.font.sf_pro))
), ),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.weight(1f)
) )
} }
}, },

View File

@@ -31,6 +31,8 @@ import android.service.quicksettings.Tile
import android.service.quicksettings.TileService import android.service.quicksettings.TileService
import android.util.Log import android.util.Log
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.compose.material3.ExperimentalMaterial3Api
import me.kavishdevar.librepods.MainActivity
import me.kavishdevar.librepods.QuickSettingsDialogActivity import me.kavishdevar.librepods.QuickSettingsDialogActivity
import me.kavishdevar.librepods.R import me.kavishdevar.librepods.R
import me.kavishdevar.librepods.utils.AirPodsNotifications import me.kavishdevar.librepods.utils.AirPodsNotifications
@@ -260,4 +262,42 @@ class AirPodsQSService : TileService() {
else -> R.drawable.airpods 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")
}
}
} }

View File

@@ -26,12 +26,14 @@ import android.app.NotificationManager
import android.app.PendingIntent import android.app.PendingIntent
import android.app.Service import android.app.Service
import android.appwidget.AppWidgetManager import android.appwidget.AppWidgetManager
import android.bluetooth.BluetoothAssignedNumbers.APPLE
import android.bluetooth.BluetoothDevice import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothManager import android.bluetooth.BluetoothManager
import android.bluetooth.BluetoothProfile import android.bluetooth.BluetoothProfile
import android.bluetooth.BluetoothSocket import android.bluetooth.BluetoothSocket
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
import android.content.ComponentName import android.content.ComponentName
import android.content.ContentResolver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.IntentFilter import android.content.IntentFilter
@@ -39,6 +41,7 @@ import android.content.SharedPreferences
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.content.res.Resources import android.content.res.Resources
import android.media.AudioManager import android.media.AudioManager
import android.net.Uri
import android.os.BatteryManager import android.os.BatteryManager
import android.os.Binder import android.os.Binder
import android.os.Build import android.os.Build
@@ -46,6 +49,7 @@ import android.os.Handler
import android.os.IBinder import android.os.IBinder
import android.os.Looper import android.os.Looper
import android.os.ParcelUuid import android.os.ParcelUuid
import android.os.UserHandle
import android.provider.Settings import android.provider.Settings
import android.telecom.TelecomManager import android.telecom.TelecomManager
import android.telephony.PhoneStateListener 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.MediaController
import me.kavishdevar.librepods.utils.PopupWindow import me.kavishdevar.librepods.utils.PopupWindow
import me.kavishdevar.librepods.utils.RadareOffsetFinder 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.utils.isHeadTrackingData
import me.kavishdevar.librepods.widgets.BatteryWidget import me.kavishdevar.librepods.widgets.BatteryWidget
import me.kavishdevar.librepods.widgets.NoiseControlWidget import me.kavishdevar.librepods.widgets.NoiseControlWidget
@@ -295,7 +319,7 @@ class AirPodsService : Service() {
object BatteryChangedIntentReceiver : BroadcastReceiver() { object BatteryChangedIntentReceiver : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent) { override fun onReceive(context: Context?, intent: Intent) {
if (intent.action == Intent.ACTION_BATTERY_CHANGED) { if (intent.action == Intent.ACTION_BATTERY_CHANGED) {
ServiceManager.getService()?.updateBatteryWidget() ServiceManager.getService()?.updateBattery()
} else if (intent.action == AirPodsNotifications.DISCONNECT_RECEIVERS) { } else if (intent.action == AirPodsNotifications.DISCONNECT_RECEIVERS) {
try { try {
context?.unregisterReceiver(this) context?.unregisterReceiver(this)
@@ -308,7 +332,7 @@ class AirPodsService : Service() {
fun setPhoneBatteryInWidget(enabled: Boolean) { fun setPhoneBatteryInWidget(enabled: Boolean) {
widgetMobileBatteryEnabled = enabled widgetMobileBatteryEnabled = enabled
updateBatteryWidget() updateBattery()
} }
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@@ -369,7 +393,8 @@ class AirPodsService : Service() {
} }
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
fun updateBatteryWidget() { fun updateBattery() {
// Update widget
val appWidgetManager = AppWidgetManager.getInstance(this) val appWidgetManager = AppWidgetManager.getInstance(this)
val componentName = ComponentName(this, BatteryWidget::class.java) val componentName = ComponentName(this, BatteryWidget::class.java)
val widgetIds = appWidgetManager.getAppWidgetIds(componentName) val widgetIds = appWidgetManager.getAppWidgetIds(componentName)
@@ -463,6 +488,43 @@ class AirPodsService : Service() {
} }
} }
appWidgetManager.updateAppWidget(widgetIds, remoteViews) 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() { fun updateNoiseControlWidget() {
@@ -678,6 +740,168 @@ class AirPodsService : Service() {
private lateinit var connectionReceiver: BroadcastReceiver private lateinit var connectionReceiver: BroadcastReceiver
private lateinit var disconnectionReceiver: 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<Any>(
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") @SuppressLint("InlinedApi", "MissingPermission", "UnspecifiedRegisterReceiverFlag")
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Log.d("AirPodsService", "Service started") Log.d("AirPodsService", "Service started")
@@ -778,6 +1002,8 @@ class AirPodsService : Service() {
Log.d("AirPodsService", "$name connected") Log.d("AirPodsService", "$name connected")
showPopup(this@AirPodsService, name.toString()) showPopup(this@AirPodsService, name.toString())
connectToSocket(device!!) connectToSocket(device!!)
Log.d("AirPodsService", "Setting metadata")
setMetadatas(device!!)
isConnectedLocally = true isConnectedLocally = true
macAddress = device!!.address macAddress = device!!.address
sharedPreferences.edit { sharedPreferences.edit {
@@ -850,6 +1076,7 @@ class AirPodsService : Service() {
if (connectedDevices.isNotEmpty()) { if (connectedDevices.isNotEmpty()) {
if (!CrossDevice.isAvailable) { if (!CrossDevice.isAvailable) {
connectToSocket(device) connectToSocket(device)
setMetadatas(device)
macAddress = device.address macAddress = device.address
sharedPreferences.edit { sharedPreferences.edit {
putString("mac_address", macAddress) putString("mac_address", macAddress)
@@ -1186,7 +1413,7 @@ class AirPodsService : Service() {
ArrayList(batteryNotification.getBattery()) ArrayList(batteryNotification.getBattery())
) )
}) })
updateBatteryWidget() updateBattery()
updateNotificationContent( updateNotificationContent(
true, true,
this@AirPodsService.getSharedPreferences( this@AirPodsService.getSharedPreferences(

View File

@@ -238,7 +238,7 @@ object CrossDevice {
batteryBytes = trimmedPacket batteryBytes = trimmedPacket
ServiceManager.getService()?.batteryNotification?.setBattery(trimmedPacket) ServiceManager.getService()?.batteryNotification?.setBattery(trimmedPacket)
Log.d("CrossDevice", "Battery data: ${ServiceManager.getService()?.batteryNotification?.getBattery()[0]?.level}") Log.d("CrossDevice", "Battery data: ${ServiceManager.getService()?.batteryNotification?.getBattery()[0]?.level}")
ServiceManager.getService()?.updateBatteryWidget() ServiceManager.getService()?.updateBattery()
ServiceManager.getService()?.sendBatteryBroadcast() ServiceManager.getService()?.sendBatteryBroadcast()
ServiceManager.getService()?.sendBatteryNotification() ServiceManager.getService()?.sendBatteryNotification()
} else if (ServiceManager.getService()?.ancNotification?.isANCData(trimmedPacket) == true) { } else if (ServiceManager.getService()?.ancNotification?.isANCData(trimmedPacket) == true) {

View File

@@ -416,9 +416,9 @@ class RadareOffsetFinder(context: Context) {
Log.e(TAG, "rabin2 command failed with exit code $exitCode") Log.e(TAG, "rabin2 command failed with exit code $exitCode")
} }
// findAndSaveL2cuProcessCfgReqOffset(libraryPath, envSetup) // findAndSaveL2cuProcessCfgReqOffset(libraryPath, envSetup)
// findAndSaveL2cCsmConfigOffset(libraryPath, envSetup) // findAndSaveL2cCsmConfigOffset(libraryPath, envSetup)
findAndSaveL2cuSendPeerInfoReqOffset(libraryPath, envSetup) // findAndSaveL2cuSendPeerInfoReqOffset(libraryPath, envSetup)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Failed to find function offset", e) Log.e(TAG, "Failed to find function offset", e)

View File

@@ -1,6 +1,8 @@
package me.kavishdevar.librepods.utils package me.kavishdevar.librepods.utils
import android.bluetooth.BluetoothDevice import android.bluetooth.BluetoothDevice
import android.util.Log
import org.lsposed.hiddenapibypass.HiddenApiBypass
object SystemApisUtils { object SystemApisUtils {
@@ -282,4 +284,23 @@ object SystemApisUtils {
const val ACTION_BLUETOOTH_HANDSFREE_BATTERY_CHANGED = "android.intent.action.BLUETOOTH_HANDSFREE_BATTERY_CHANGED" 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_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" 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
}
}
} }

View File

@@ -19,15 +19,9 @@
package me.kavishdevar.librepods.widgets package me.kavishdevar.librepods.widgets
import android.app.PendingIntent
import android.appwidget.AppWidgetManager import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProvider import android.appwidget.AppWidgetProvider
import android.content.Context 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 import me.kavishdevar.librepods.services.ServiceManager
class BatteryWidget : AppWidgetProvider() { class BatteryWidget : AppWidgetProvider() {
@@ -36,6 +30,6 @@ class BatteryWidget : AppWidgetProvider() {
appWidgetManager: AppWidgetManager, appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray appWidgetIds: IntArray
) { ) {
ServiceManager.getService()?.updateBatteryWidget() ServiceManager.getService()?.updateBattery()
} }
} }

View File

@@ -24,6 +24,7 @@ import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProvider import android.appwidget.AppWidgetProvider
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.util.Log
import android.widget.RemoteViews import android.widget.RemoteViews
import me.kavishdevar.librepods.R import me.kavishdevar.librepods.R
import me.kavishdevar.librepods.services.ServiceManager import me.kavishdevar.librepods.services.ServiceManager
@@ -77,6 +78,7 @@ class NoiseControlWidget : AppWidgetProvider() {
super.onReceive(context, intent) super.onReceive(context, intent)
if (intent.action == "ACTION_SET_ANC_MODE") { if (intent.action == "ACTION_SET_ANC_MODE") {
val mode = intent.getIntExtra("ANC_MODE", 1) val mode = intent.getIntExtra("ANC_MODE", 1)
Log.d("NoiseControlWidget", "Setting ANC mode to $mode")
ServiceManager.getService()?.setANCMode(mode) ServiceManager.getService()?.setANCMode(mode)
} }
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB