Compare commits

...
6 Commits
Author SHA1 Message Date
darken 9d6ec2b6bb Release: 4.0.3-rc0 2026-03-23 14:06:37 +01:00
darken 9a85b23d91 chore: Enable support-investigator plugin in .claude/settings.json 2026-03-23 14:01:34 +01:00
darken 217d9be177 fix: Show key icon for single-pod devices when encryption keys are set 2026-03-23 13:00:48 +00:00
darken 53447f3fb9 fix: Stop retrying auto-connect after SecurityException
BluetoothHeadset.connect() requires MODIFY_PHONE_STATE on modern Android, which is a system-only permission. Detect the SecurityException and stop further attempts instead of retrying every second.
2026-03-23 11:24:23 +00:00
darken db759bac7c Release: 4.0.2-rc1 2026-03-23 06:11:39 +01:00
darken 6837db5b84 fix: Remove mapping file param from Fastlane supply lanes
AGP 9.0.1 no longer generates a separate mapping.txt for bundle tasks when -dontobfuscate is active. The mapping param was causing supply to fail on a non-existent file.
2026-03-23 06:11:09 +01:00
9 changed files with 94 additions and 23 deletions
+12 -11
View File
@@ -1,16 +1,17 @@
{
"permissions": {
"allow": [
"mcp__ide__getDiagnostics",
"WebSearch"
],
"deny": []
"allow": [
"mcp__ide__getDiagnostics",
"WebSearch"
],
"deny": []
},
"enabledPlugins": {
"android-translation@claude-code-cafe": true,
"debugbadger@claude-code-cafe": true,
"jvm-tools@claude-code-cafe": true,
"frontend-design@claude-plugins-official": true,
"kotlin-lsp@claude-plugins-official": true
"android-translation@claude-code-cafe": true,
"debugbadger@claude-code-cafe": true,
"jvm-tools@claude-code-cafe": true,
"frontend-design@claude-plugins-official": true,
"kotlin-lsp@claude-plugins-official": true,
"support-investigator@claude-code-cafe": true
}
}
}
+1 -1
View File
@@ -1 +1 @@
4.0.2-rc0 40002000
4.0.3-rc0 40003000
@@ -304,6 +304,9 @@ class BluetoothManager2 @Inject constructor(
emit(wrappedDevices)
}
private var _isNudgeAvailable: Boolean = true
val isNudgeAvailable: Boolean get() = _isNudgeAvailable
suspend fun nudgeConnection(device: BluetoothDevice2): Boolean = getBluetoothProfile().map { bluetoothProfile ->
try {
log(TAG) { "Nudging Android connection to $device" }
@@ -317,6 +320,12 @@ class BluetoothManager2 @Inject constructor(
log(TAG) { "Nudged connection to $device" }
true
} catch (e: Exception) {
val isSecurityException = e is SecurityException ||
(e is java.lang.reflect.InvocationTargetException && e.cause is SecurityException)
if (isSecurityException) {
log(TAG, ERROR) { "nudgeConnection is permanently unavailable: missing MODIFY_PHONE_STATE permission" }
_isNudgeAvailable = false
}
Bugs.report(tag = TAG, "BluetoothHeadset.connect(device) is unavailable", exception = e)
false
}
@@ -15,6 +15,8 @@ import eu.darken.capod.pods.core.PodDevice
import eu.darken.capod.pods.core.SinglePodDevice
import eu.darken.capod.pods.core.unknown.UnknownDevice
import eu.darken.capod.profiles.core.AppleDeviceProfile
import eu.darken.capod.pods.core.apple.ApplePods
import eu.darken.capod.pods.core.apple.protocol.ProximityPayload
import eu.darken.capod.profiles.core.DeviceProfile
import java.time.Instant
@@ -114,11 +116,13 @@ object MockPodDataProvider {
_isBeingWorn = true,
)
fun airPodsMaxCharging(): SinglePodDevice = MockSinglePodDevice(
fun airPodsMaxCharging(): SinglePodDevice = MockSingleApplePodDevice(
_model = PodDevice.Model.AIRPODS_MAX,
_label = "AirPods Max",
batteryHeadsetPercent = 0.40f,
_isHeadsetBeingCharged = true,
_isIRKMatch = true,
_hasPrivatePayload = true,
)
fun beatsSolo3(): SinglePodDevice = MockSinglePodDevice(
@@ -273,3 +277,41 @@ private class MockSinglePodDevice(
// HasEarDetection
override val isBeingWorn: Boolean = _isBeingWorn
}
@Suppress("PropertyName")
private class MockSingleApplePodDevice(
private val _model: PodDevice.Model,
private val _label: String,
override val batteryHeadsetPercent: Float?,
private val _isHeadsetBeingCharged: Boolean = false,
private val _isBeingWorn: Boolean = false,
private val _isIRKMatch: Boolean = false,
private val _hasPrivatePayload: Boolean = false,
rssi: Int = -50,
) : SinglePodDevice, ApplePods, HasChargeDetection, HasEarDetection {
override val identifier: PodDevice.Id = PodDevice.Id()
override val model: PodDevice.Model = _model
override val seenLastAt: Instant = MOCK_NOW
override val seenFirstAt: Instant = MOCK_NOW
override val seenCounter: Int = 5
override val scanResult: BleScanResult = MockPodDataProvider.dummyScanResult(rssi)
override val reliability: Float = 1.0f
override val signalQuality: Float = 0.80f
override val iconRes: Int = _model.iconRes
override val meta: ApplePods.AppleMeta = ApplePods.AppleMeta(
isIRKMatch = _isIRKMatch,
profile = AppleDeviceProfile(label = _label, model = _model),
)
override val payload: ProximityPayload = ProximityPayload(
public = ProximityPayload.Public(UByteArray(9)),
private = if (_hasPrivatePayload) ProximityPayload.Private(UByteArray(8)) else null,
)
override fun getLabel(context: Context): String = _model.label
// HasChargeDetection
override val isHeadsetBeingCharged: Boolean = _isHeadsetBeingCharged
// HasEarDetection
override val isBeingWorn: Boolean = _isBeingWorn
}
@@ -19,9 +19,12 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Key
import androidx.compose.material.icons.twotone.BatteryChargingFull
import androidx.compose.material.icons.twotone.Hearing
import androidx.compose.material.icons.twotone.Key
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.MaterialTheme
@@ -44,6 +47,7 @@ import eu.darken.capod.common.compose.preview.MockPodDataProvider
import eu.darken.capod.pods.core.HasChargeDetection
import eu.darken.capod.pods.core.HasEarDetection
import eu.darken.capod.pods.core.SinglePodDevice
import eu.darken.capod.pods.core.apple.ApplePods
import eu.darken.capod.pods.core.firstSeenFormatted
import eu.darken.capod.pods.core.formatBatteryPercent
import eu.darken.capod.pods.core.getSignalQuality
@@ -98,12 +102,23 @@ fun SinglePodsCard(
Spacer(modifier = Modifier.width(12.dp))
Column(modifier = Modifier.weight(1f)) {
Text(
text = device.meta.profile?.label ?: "?",
style = MaterialTheme.typography.titleMedium,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
text = device.meta.profile?.label ?: "?",
style = MaterialTheme.typography.titleMedium,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
if (device is ApplePods && device.meta.isIRKMatch) {
Spacer(modifier = Modifier.width(6.dp))
Icon(
imageVector = if (device.payload.private != null) Icons.TwoTone.Key else Icons.Outlined.Key,
contentDescription = null,
modifier = Modifier.size(14.dp),
tint = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}
Text(
text = device.getLabel(context),
style = MaterialTheme.typography.bodySmall,
@@ -99,6 +99,12 @@ class AutoConnect @Inject constructor(
log(TAG) { "Auto connect condition ($condition) is not fullfilled: ${decision.reason}" }
return@map
}
if (!bluetoothManager.isNudgeAvailable) {
log(TAG, WARN) { "nudgeConnection is not available on this device, skipping" }
return@map
}
val result = bluetoothManager.nudgeConnection(bondedDevice)
log(TAG) { "nudgeConnection($bondedDevice) returned $result" }
}
+1 -1
View File
@@ -61,7 +61,7 @@
<string name="settings_signal_minimum_label">Minimum signal quality</string>
<string name="settings_signal_minimum_description">The minimum signal quality that a device needs to have to be considered yours.</string>
<string name="settings_autoconnect_label">Auto connect</string>
<string name="settings_autoconnect_description">If Android does not automatically connect, we can ask it too. This will set the monitor mode setting to \'Always\'.</string>
<string name="settings_autoconnect_description">If Android does not automatically connect, we can ask it too. This will set the monitor mode setting to \'Always\'. May not work on newer Android versions.</string>
<string name="settings_autoconnect_condition_label">Auto connect condition</string>
<string name="settings_autoconnect_condition_description">When should we try to connect to your device?</string>
<string name="settings_devices_label">Devices</string>
-2
View File
@@ -27,7 +27,6 @@ platform :android do
track: 'beta',
rollout: '0.10',
package_name: 'eu.darken.capod',
mapping: '../app/build/outputs/mapping/gplayBeta/mapping.txt',
skip_upload_changelogs: 'false',
skip_upload_apk: 'true',
skip_upload_images: 'true',
@@ -43,7 +42,6 @@ platform :android do
track: 'beta',
rollout: '0.10',
package_name: 'eu.darken.capod',
mapping: '../app/build/outputs/mapping/gplayRelease/mapping.txt',
skip_upload_changelogs: 'false',
skip_upload_apk: 'true',
skip_upload_images: 'true',
+1 -1
View File
@@ -1,7 +1,7 @@
### Updated by release.sh ###
project.versioning.major=4
project.versioning.minor=0
project.versioning.patch=2
project.versioning.patch=3
project.versioning.build=0
project.versioning.type=rc
#############################