mirror of
https://github.com/kavishdevar/librepods.git
synced 2026-01-28 22:01:50 +00:00
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:
@@ -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 {
|
||||
|
||||
@@ -1,24 +1,26 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<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.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE" />
|
||||
<uses-permission android:name="android.permission.INTERACT_ACROSS_USERS"
|
||||
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.RECEIVE_BOOT_COMPLETED" />
|
||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
|
||||
<uses-permission
|
||||
android:name="android.permission.BLUETOOTH_PRIVILEGED"
|
||||
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_SCAN"
|
||||
@@ -30,6 +32,8 @@
|
||||
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
|
||||
<protected-broadcast android:name="batterywidget.impl.action.update_bluetooth_data" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
|
||||
@@ -32,9 +32,7 @@
|
||||
static HookFunType hook_func = nullptr;
|
||||
#define L2CEVT_L2CAP_CONFIG_REQ 4
|
||||
#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;
|
||||
typedef struct _BT_HDR {
|
||||
uint16_t event;
|
||||
@@ -44,7 +42,6 @@ typedef struct _BT_HDR {
|
||||
uint8_t data[];
|
||||
} BT_HDR;
|
||||
|
||||
// Define base FCR structures
|
||||
typedef struct {
|
||||
uint8_t mode;
|
||||
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;
|
||||
|
||||
uint8_t fake_l2c_fcr_chk_chan_modes(void* p_ccb) {
|
||||
LOGI("l2c_fcr_chk_chan_modes hooked");
|
||||
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);
|
||||
|
||||
LOGI("l2c_fcr_chk_chan_modes hooked, returning true.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<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")
|
||||
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(
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
BIN
android/app/src/main/res/drawable/pro_2_left.png
Normal file
BIN
android/app/src/main/res/drawable/pro_2_left.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 35 KiB |
BIN
android/app/src/main/res/drawable/pro_2_right.png
Normal file
BIN
android/app/src/main/res/drawable/pro_2_right.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 36 KiB |
Reference in New Issue
Block a user