mirror of
https://github.com/kavishdevar/librepods.git
synced 2026-05-12 23:52:29 +00:00
Compare commits
6 Commits
nightly-fb
...
nightly-21
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
216c97f9ca | ||
|
|
fd3774b513 | ||
|
|
b7336940e6 | ||
|
|
b2ba830a80 | ||
|
|
f08769e62f | ||
|
|
d1933c3b67 |
@@ -1,6 +1,6 @@
|
||||
import java.util.Properties
|
||||
|
||||
val appVersionName = "0.2.8"
|
||||
val appVersionName = "0.2.9"
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.android.application)
|
||||
@@ -30,7 +30,7 @@ android {
|
||||
applicationId = "me.kavishdevar.librepods"
|
||||
minSdk = 33
|
||||
targetSdk = 37
|
||||
versionCode = 49
|
||||
versionCode = 50
|
||||
versionName = appVersionName
|
||||
}
|
||||
buildTypes {
|
||||
|
||||
@@ -109,7 +109,8 @@ class AACPManager {
|
||||
EAR_DETECTION_CONFIG(0x0A), AUTOMATIC_CONNECTION_CONFIG(0x20), OWNS_CONNECTION(0x06), PPE_TOGGLE_CONFIG(
|
||||
0x37
|
||||
),
|
||||
PPE_CAP_LEVEL_CONFIG(0x38);
|
||||
PPE_CAP_LEVEL_CONFIG(0x38),
|
||||
DYNAMIC_END_OF_CHARGE(0x3B);
|
||||
|
||||
companion object {
|
||||
fun fromByte(byte: Byte): ControlCommandIdentifiers? =
|
||||
|
||||
@@ -398,6 +398,16 @@ fun AirPodsSettingsScreen(viewModel: AirPodsViewModel, navController: NavControl
|
||||
}
|
||||
}
|
||||
|
||||
item(key = "spacer_dynamic_end_of_charge") { Spacer(modifier = Modifier.height(16.dp)) }
|
||||
item(key = "dynamic_end_of_charge") {
|
||||
StyledToggle(
|
||||
label = stringResource(R.string.optimized_charging),
|
||||
description = stringResource(R.string.optimized_charging_description),
|
||||
checked = state.dynamicEndOfCharge,
|
||||
onCheckedChange = viewModel::setDynamicEndOfCharge
|
||||
)
|
||||
}
|
||||
|
||||
item(key = "spacer_accessibility") { Spacer(modifier = Modifier.height(16.dp)) }
|
||||
item(key = "accessibility") {
|
||||
NavigationButton(
|
||||
@@ -542,19 +552,22 @@ fun AirPodsSettingsScreen(viewModel: AirPodsViewModel, navController: NavControl
|
||||
}
|
||||
Spacer(Modifier.height(16.dp))
|
||||
}
|
||||
StyledButton(
|
||||
onClick = {
|
||||
viewModel.reconnectFromSavedMac()
|
||||
}, backdrop = backdrop, modifier = Modifier.fillMaxWidth(0.9f)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.reconnect_to_last_device), style = TextStyle(
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
||||
color = if (isSystemInDarkTheme()) Color.White else Color.Black
|
||||
if (state.connectionSuccessful) {
|
||||
StyledButton(
|
||||
onClick = {
|
||||
viewModel.reconnectFromSavedMac()
|
||||
}, backdrop = backdrop, modifier = Modifier.fillMaxWidth(0.9f)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.reconnect_to_last_device),
|
||||
style = TextStyle(
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
||||
color = if (isSystemInDarkTheme()) Color.White else Color.Black
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,6 +157,48 @@ fun AppSettingsScreen(
|
||||
enabled = state.isPremium
|
||||
)
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.popup_animations), style = TextStyle(
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = textColor.copy(alpha = 0.6f),
|
||||
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||
), modifier = Modifier.padding(16.dp, bottom = 2.dp, top = 24.dp)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(2.dp))
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(
|
||||
backgroundColor, RoundedCornerShape(28.dp)
|
||||
)
|
||||
.padding(vertical = 4.dp)
|
||||
) {
|
||||
StyledToggle(
|
||||
label = stringResource(R.string.show_bottom_sheet_popup),
|
||||
description = stringResource(R.string.show_bottom_sheet_popup_description),
|
||||
checked = state.showBottomSheetPopup,
|
||||
onCheckedChange = viewModel::setShowBottomSheetPopup,
|
||||
independent = false
|
||||
)
|
||||
|
||||
HorizontalDivider(
|
||||
thickness = 1.dp,
|
||||
color = Color(0x40888888),
|
||||
modifier = Modifier.padding(horizontal = 12.dp)
|
||||
)
|
||||
|
||||
StyledToggle(
|
||||
label = stringResource(R.string.show_island_popup),
|
||||
description = stringResource(R.string.show_island_popup_description),
|
||||
checked = state.showIslandPopup,
|
||||
onCheckedChange = viewModel::setShowIslandPopup,
|
||||
independent = false
|
||||
)
|
||||
}
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.conversational_awareness), style = TextStyle(
|
||||
fontSize = 14.sp,
|
||||
|
||||
@@ -89,7 +89,11 @@ data class AirPodsUiState(
|
||||
val hearingAidData: ByteArray = byteArrayOf(),
|
||||
|
||||
val isPremium: Boolean = false,
|
||||
val vendorIdHook: Boolean = false
|
||||
val vendorIdHook: Boolean = false,
|
||||
|
||||
val dynamicEndOfCharge: Boolean = false,
|
||||
|
||||
val connectionSuccessful: Boolean = false
|
||||
)
|
||||
|
||||
class AirPodsViewModel(
|
||||
@@ -268,9 +272,16 @@ class AirPodsViewModel(
|
||||
val current = state.controlStates[identifier]
|
||||
if (current?.contentEquals(value) == true) return@update state
|
||||
|
||||
state.copy(
|
||||
controlStates = state.controlStates + (identifier to value)
|
||||
)
|
||||
if (identifier == ControlCommandIdentifiers.DYNAMIC_END_OF_CHARGE) {
|
||||
state.copy(
|
||||
dynamicEndOfCharge = value[0] == 0x01.toByte(),
|
||||
controlStates = state.controlStates + (identifier to value)
|
||||
)
|
||||
} else {
|
||||
state.copy(
|
||||
controlStates = state.controlStates + (identifier to value)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -305,6 +316,7 @@ class AirPodsViewModel(
|
||||
ControlCommandIdentifiers.AUTOMATIC_CONNECTION_CONFIG,
|
||||
ControlCommandIdentifiers.OWNS_CONNECTION,
|
||||
ControlCommandIdentifiers.PPE_TOGGLE_CONFIG,
|
||||
ControlCommandIdentifiers.DYNAMIC_END_OF_CHARGE
|
||||
)
|
||||
for (identifier in identifiersList) {
|
||||
observeControl(identifier)
|
||||
@@ -342,6 +354,9 @@ class AirPodsViewModel(
|
||||
) ?: "CYCLE_NOISE_CONTROL_MODES"
|
||||
)
|
||||
val vendorIdHook = xposedRemotePref.getBoolean("vendor_id_hook", false)
|
||||
val dynamicEndOfCharge = sharedPreferences.getBoolean("dynamic_end_of_charge", false)
|
||||
|
||||
val connectionSuccessful = sharedPreferences.getBoolean("connection_successful", false)
|
||||
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
@@ -351,7 +366,9 @@ class AirPodsViewModel(
|
||||
headGesturesEnabled = headGesturesEnabled,
|
||||
leftAction = leftAction,
|
||||
rightAction = rightAction,
|
||||
vendorIdHook = vendorIdHook
|
||||
vendorIdHook = vendorIdHook,
|
||||
dynamicEndOfCharge = dynamicEndOfCharge,
|
||||
connectionSuccessful = connectionSuccessful
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -371,6 +388,14 @@ class AirPodsViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
fun setDynamicEndOfCharge(enabled: Boolean) {
|
||||
service.aacpManager.sendControlCommand(ControlCommandIdentifiers.DYNAMIC_END_OF_CHARGE.value, enabled)
|
||||
sharedPreferences.edit { putBoolean("dynamic_end_of_charge", enabled) }
|
||||
_uiState.update {
|
||||
it.copy(dynamicEndOfCharge = enabled)
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadControlList() {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
|
||||
@@ -32,7 +32,9 @@ data class AppSettingsUiState(
|
||||
val cameraPackageError: String? = null,
|
||||
val vendorIdHook: Boolean = false,
|
||||
val isPremium: Boolean = false,
|
||||
val connectionSuccessful: Boolean = false
|
||||
val connectionSuccessful: Boolean = false,
|
||||
val showBottomSheetPopup: Boolean = true,
|
||||
val showIslandPopup: Boolean = true
|
||||
)
|
||||
|
||||
class AppSettingsViewModel(application: Application) : AndroidViewModel(application) {
|
||||
@@ -86,7 +88,9 @@ class AppSettingsViewModel(application: Application) : AndroidViewModel(applicat
|
||||
conversationalAwarenessVolume = sharedPreferences.getInt("conversational_awareness_volume", 43).toFloat(),
|
||||
cameraPackageValue = sharedPreferences.getString("custom_camera_package", "") ?: "",
|
||||
vendorIdHook = xposedRemotePref.getBoolean("vendor_id_hook", false),
|
||||
connectionSuccessful = sharedPreferences.getBoolean("connection_successful", false)
|
||||
connectionSuccessful = sharedPreferences.getBoolean("connection_successful", false),
|
||||
showBottomSheetPopup = sharedPreferences.getBoolean("show_bottom_sheet_popup", true),
|
||||
showIslandPopup = sharedPreferences.getBoolean("show_island_popup", true)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -176,4 +180,14 @@ class AppSettingsViewModel(application: Application) : AndroidViewModel(applicat
|
||||
xposedRemotePref.putBoolean("vendor_id_hook", enabled)
|
||||
_uiState.update { it.copy(vendorIdHook = enabled) }
|
||||
}
|
||||
|
||||
fun setShowBottomSheetPopup(enabled: Boolean) {
|
||||
sharedPreferences.edit { putBoolean("show_bottom_sheet_popup", enabled) }
|
||||
_uiState.update { it.copy(showBottomSheetPopup = enabled) }
|
||||
}
|
||||
|
||||
fun setShowIslandPopup(enabled: Boolean) {
|
||||
sharedPreferences.edit { putBoolean("show_island_popup", enabled) }
|
||||
_uiState.update { it.copy(showIslandPopup = enabled) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -526,7 +526,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
|
||||
|
||||
initializeConfig()
|
||||
|
||||
ancModeReceiver = object : BroadcastReceiver() {
|
||||
externalBroadcastReceiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
if (intent?.action == "me.kavishdevar.librepods.SET_ANC_MODE") {
|
||||
if (intent.hasExtra("mode")) {
|
||||
@@ -555,15 +555,23 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
|
||||
"Cycling ANC mode from $currentMode to $nextMode"
|
||||
)
|
||||
}
|
||||
} else if (intent?.action == "me.kavishdevar.librepods.CONVO_DETECT") {
|
||||
if (intent.hasExtra("enabled")) {
|
||||
val enabled = intent.getBooleanExtra("enabled", false)
|
||||
aacpManager.sendControlCommand(
|
||||
AACPManager.Companion.ControlCommandIdentifiers.CONVERSATION_DETECT_CONFIG.value,
|
||||
enabled
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
registerReceiver(ancModeReceiver, ancModeFilter, RECEIVER_EXPORTED)
|
||||
registerReceiver(externalBroadcastReceiver, externalBroadcastFilter, RECEIVER_EXPORTED)
|
||||
} else {
|
||||
@Suppress("UnspecifiedRegisterReceiverFlag") registerReceiver(
|
||||
ancModeReceiver, ancModeFilter
|
||||
externalBroadcastReceiver, externalBroadcastFilter
|
||||
)
|
||||
}
|
||||
val audioManager = this@AirPodsService.getSystemService(AUDIO_SERVICE) as AudioManager
|
||||
@@ -1636,6 +1644,9 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
|
||||
|
||||
var popupShown = false
|
||||
fun showPopup(service: Service, name: String) {
|
||||
if (!sharedPreferences.getBoolean("show_bottom_sheet_popup", true)) {
|
||||
return
|
||||
}
|
||||
if (!Settings.canDrawOverlays(service)) {
|
||||
Log.d(TAG, "No permission for SYSTEM_ALERT_WINDOW")
|
||||
return
|
||||
@@ -1660,6 +1671,9 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
|
||||
otherDeviceName: String? = null
|
||||
) {
|
||||
Log.d(TAG, "Showing island window")
|
||||
if (!sharedPreferences.getBoolean("show_island_popup", true)) {
|
||||
return
|
||||
}
|
||||
if (!Settings.canDrawOverlays(service)) {
|
||||
Log.d(TAG, "No permission for SYSTEM_ALERT_WINDOW")
|
||||
return
|
||||
@@ -2391,8 +2405,11 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
|
||||
}
|
||||
}
|
||||
|
||||
val ancModeFilter = IntentFilter("me.kavishdevar.librepods.SET_ANC_MODE")
|
||||
var ancModeReceiver: BroadcastReceiver? = null
|
||||
val externalBroadcastFilter = IntentFilter().apply {
|
||||
addAction("me.kavishdevar.librepods.SET_ANC_MODE")
|
||||
addAction("me.kavishdevar.librepods.CONVO_DETECT")
|
||||
}
|
||||
var externalBroadcastReceiver: BroadcastReceiver? = null
|
||||
|
||||
@SuppressLint("InlinedApi", "MissingPermission", "UnspecifiedRegisterReceiverFlag")
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
@@ -3098,7 +3115,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
|
||||
e.printStackTrace()
|
||||
}
|
||||
try {
|
||||
unregisterReceiver(ancModeReceiver)
|
||||
unregisterReceiver(externalBroadcastReceiver)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ fun isSupported(sharedPreferences: SharedPreferences): Boolean {
|
||||
if (isPixel) {
|
||||
when (Build.VERSION.SDK_INT) {
|
||||
36 -> {
|
||||
return Build.ID == "CP1A.260305.018" || Build.ID == "CP1A.260405.005"
|
||||
return Build.ID == "CP1A.260305.018" || Build.ID == "CP1A.260405.005" || Build.ID == "CP1A.260505.005"
|
||||
}
|
||||
|
||||
37 -> {
|
||||
|
||||
7
android/app/src/main/res/values-de/strings.xml
Normal file
7
android/app/src/main/res/values-de/strings.xml
Normal file
@@ -0,0 +1,7 @@
|
||||
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation">
|
||||
<string name="popup_animations">Popup-Animationen</string>
|
||||
<string name="show_bottom_sheet_popup">Popup unten</string>
|
||||
<string name="show_bottom_sheet_popup_description">Zeigt das Popup im iOS-Stil unten an, wenn AirPods sich verbinden.</string>
|
||||
<string name="show_island_popup">Dynamic Island Popup</string>
|
||||
<string name="show_island_popup_description">Zeigt das Popup im Dynamic-Island-Stil oben für Verbindungs- und Übergabe-Ereignisse.</string>
|
||||
</resources>
|
||||
@@ -210,4 +210,9 @@
|
||||
<string name="listening_mode_transparency_description">Deja entrar los sonidos externos</string>
|
||||
<string name="listening_mode_adaptive_description">Ajuste dinámico del ruido externo</string>
|
||||
<string name="listening_mode_noise_cancellation_description">Bloquea los sonidos externos</string>
|
||||
<string name="popup_animations">Animaciones emergentes</string>
|
||||
<string name="show_bottom_sheet_popup">Ventana emergente inferior</string>
|
||||
<string name="show_bottom_sheet_popup_description">Muestra la ventana emergente estilo iOS en la parte inferior cuando los AirPods se conectan.</string>
|
||||
<string name="show_island_popup">Ventana emergente Dynamic Island</string>
|
||||
<string name="show_island_popup_description">Muestra la ventana emergente estilo Dynamic Island en la parte superior para eventos de conexión y traspaso.</string>
|
||||
</resources>
|
||||
|
||||
@@ -210,4 +210,9 @@
|
||||
<string name="listening_mode_transparency_description">Laisser entrer les sons extérieurs</string>
|
||||
<string name="listening_mode_adaptive_description">Ajuster dynamiquement les sons extérieurs</string>
|
||||
<string name="listening_mode_noise_cancellation_description">Bloquer les sons extérieurs</string>
|
||||
<string name="popup_animations">Animations contextuelles</string>
|
||||
<string name="show_bottom_sheet_popup">Fenêtre contextuelle en bas</string>
|
||||
<string name="show_bottom_sheet_popup_description">Afficher la fenêtre contextuelle de style iOS en bas de l\'écran lors de la connexion des AirPods.</string>
|
||||
<string name="show_island_popup">Fenêtre Dynamic Island</string>
|
||||
<string name="show_island_popup_description">Afficher la fenêtre de style Dynamic Island en haut de l\'écran pour les événements de connexion et de transfert.</string>
|
||||
</resources>
|
||||
|
||||
@@ -210,4 +210,9 @@
|
||||
<string name="listening_mode_transparency_description">Permite sons externos</string>
|
||||
<string name="listening_mode_adaptive_description">Ajusta dinamicamente o ruído externo</string>
|
||||
<string name="listening_mode_noise_cancellation_description">Bloqueia sons externos</string>
|
||||
<string name="popup_animations">Animações de pop-up</string>
|
||||
<string name="show_bottom_sheet_popup">Pop-up inferior</string>
|
||||
<string name="show_bottom_sheet_popup_description">Exibe o pop-up estilo iOS na parte inferior quando os AirPods se conectam.</string>
|
||||
<string name="show_island_popup">Pop-up Dynamic Island</string>
|
||||
<string name="show_island_popup_description">Exibe o pop-up estilo Dynamic Island no topo da tela em eventos de conexão e transferência.</string>
|
||||
</resources>
|
||||
|
||||
@@ -140,6 +140,11 @@
|
||||
<string name="widget">Widget</string>
|
||||
<string name="show_phone_battery_in_widget">Show phone battery in widget</string>
|
||||
<string name="show_phone_battery_in_widget_description">Display your phone\'s battery level in the widget alongside AirPods battery</string>
|
||||
<string name="popup_animations">Popup Animations</string>
|
||||
<string name="show_bottom_sheet_popup">Bottom sheet popup</string>
|
||||
<string name="show_bottom_sheet_popup_description">Show the iOS-style modal popup at the bottom when AirPods connect.</string>
|
||||
<string name="show_island_popup">Dynamic Island popup</string>
|
||||
<string name="show_island_popup_description">Show the Dynamic Island-style popup at the top for connection and takeover events.</string>
|
||||
<string name="conversational_awareness_volume">Conversational Awareness Volume</string>
|
||||
<string name="quick_settings_tile">Quick Settings Tile</string>
|
||||
<string name="open_dialog_for_controlling">Open dialog for controlling</string>
|
||||
@@ -267,4 +272,6 @@
|
||||
<string name="app_enabled_in_xposed">App enabled in Xposed</string>
|
||||
<string name="subject">Subject</string>
|
||||
<string name="describe_your_issue">Describe your issue</string>
|
||||
<string name="optimized_charging">Optimized Charge Limit</string>
|
||||
<string name="optimized_charging_description">AirPods can learn from your daily usage and determine when to charge to an optmized limit and when to allow or full charge. This limit adapts to your daily usage and preserves your battery lifespan over time.\\nThis setting may not affect unsupported AirPods, or AirPods on an older firmware version.</string>
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user