mirror of
https://github.com/d4rken-org/capod.git
synced 2026-09-14 18:26:11 -04:00
Compare commits
22
Commits
v2.7.1-rc1
...
v2.8.1-rc0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
870e518ff6 | ||
|
|
6d89169bae | ||
|
|
ac77ee2703 | ||
|
|
18dcf603e9 | ||
|
|
71aaba1b86 | ||
|
|
64b9d5f17a | ||
|
|
a9f23c4bfe | ||
|
|
f454f46778 | ||
|
|
432936ea2c | ||
|
|
8211b0ac88 | ||
|
|
c314987e13 | ||
|
|
fc6cd74752 | ||
|
|
a47011e579 | ||
|
|
fbeeec6539 | ||
|
|
e1cd79cd9d | ||
|
|
692ced1ee7 | ||
|
|
c5cc127df4 | ||
|
|
5f00f10ae8 | ||
|
|
0fc9716f06 | ||
|
|
48fd3543e3 | ||
|
|
41985794c2 | ||
|
|
87a92fd001 |
@@ -21,6 +21,10 @@ android {
|
||||
buildConfigField("String", "APPLICATION_ID", "\"${ProjectConfig.packageName}\"")
|
||||
buildConfigField("String", "GITSHA", "\"${lastCommitHash()}\"")
|
||||
buildConfigField("String", "BUILDTIME", "\"${buildTime()}\"")
|
||||
|
||||
manifestPlaceholders["bugsnagApiKey"] = getBugSnagApiKey(
|
||||
File(System.getProperty("user.home"), ".appconfig/${ProjectConfig.packageName}/bugsnag.properties")
|
||||
) ?: "bugsnag_apikey_placeholder"
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
@@ -95,6 +99,7 @@ dependencies {
|
||||
|
||||
addTesting()
|
||||
|
||||
implementation("com.bugsnag:bugsnag-android:5.9.2")
|
||||
"gplayImplementation"("com.bugsnag:bugsnag-android:5.9.2")
|
||||
"gplayImplementation"("com.getkeepsafe.relinker:relinker:1.4.3")
|
||||
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest>
|
||||
|
||||
</manifest>
|
||||
@@ -0,0 +1,17 @@
|
||||
package eu.darken.capod.debug.autoreport
|
||||
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import eu.darken.capod.common.debug.autoreport.AutomaticBugReporter
|
||||
import eu.darken.capod.debug.autoreport.FossAutoReporting
|
||||
import javax.inject.Singleton
|
||||
|
||||
@InstallIn(SingletonComponent::class)
|
||||
@Module
|
||||
abstract class AutoReportingModule {
|
||||
@Binds
|
||||
@Singleton
|
||||
abstract fun autoreporting(foss: FossAutoReporting): AutomaticBugReporter
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package eu.darken.capod.debug.autoreport
|
||||
|
||||
import android.app.Application
|
||||
import eu.darken.capod.common.debug.autoreport.AutomaticBugReporter
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class FossAutoReporting @Inject constructor() : AutomaticBugReporter {
|
||||
override fun setup(application: Application) {
|
||||
// NOOP
|
||||
}
|
||||
|
||||
override fun notify(throwable: Throwable) {
|
||||
throw IllegalStateException("Who initliazed this? Without setup no calls to here!")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<application>
|
||||
<meta-data
|
||||
android:name="com.bugsnag.android.API_KEY"
|
||||
android:value="${bugsnagApiKey}" />
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -0,0 +1,16 @@
|
||||
package eu.darken.capod.debug.autoreport
|
||||
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import eu.darken.capod.common.debug.autoreport.AutomaticBugReporter
|
||||
import javax.inject.Singleton
|
||||
|
||||
@InstallIn(SingletonComponent::class)
|
||||
@Module
|
||||
abstract class AutoReportingModule {
|
||||
@Binds
|
||||
@Singleton
|
||||
abstract fun autoreporting(foss: GplayAutoReporting): AutomaticBugReporter
|
||||
}
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
package eu.darken.capod.common.debug.autoreport.bugsnag
|
||||
package eu.darken.capod.debug.autoreport
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
@@ -7,7 +7,7 @@ import com.bugsnag.android.Event
|
||||
import com.bugsnag.android.OnErrorCallback
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import eu.darken.capod.common.BuildConfigWrap
|
||||
import eu.darken.capod.common.debug.autoreport.DebugSettings
|
||||
import eu.darken.capod.common.debug.DebugSettings
|
||||
import eu.darken.capod.common.debug.logging.Logging.Priority.WARN
|
||||
import eu.darken.capod.common.debug.logging.asLog
|
||||
import eu.darken.capod.common.debug.logging.log
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package eu.darken.capod.common.debug.autoreport.bugsnag
|
||||
package eu.darken.capod.debug.autoreport
|
||||
|
||||
import com.bugsnag.android.Event
|
||||
import eu.darken.capod.common.debug.logging.Logging
|
||||
+20
-8
@@ -1,15 +1,16 @@
|
||||
package eu.darken.capod.common.debug.autoreport
|
||||
package eu.darken.capod.debug.autoreport
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import com.bugsnag.android.Bugsnag
|
||||
import com.bugsnag.android.Configuration
|
||||
import com.getkeepsafe.relinker.ReLinker
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import eu.darken.capod.common.BuildConfigWrap
|
||||
import eu.darken.capod.common.InstallId
|
||||
import eu.darken.capod.common.debug.Bugs
|
||||
import eu.darken.capod.common.debug.autoreport.bugsnag.BugsnagErrorHandler
|
||||
import eu.darken.capod.common.debug.autoreport.bugsnag.BugsnagLogger
|
||||
import eu.darken.capod.common.debug.autoreport.bugsnag.NOPBugsnagErrorHandler
|
||||
import eu.darken.capod.common.debug.DebugSettings
|
||||
import eu.darken.capod.common.debug.autoreport.AutomaticBugReporter
|
||||
import eu.darken.capod.common.debug.logging.Logging
|
||||
import eu.darken.capod.common.debug.logging.log
|
||||
import eu.darken.capod.common.debug.logging.logTag
|
||||
@@ -18,19 +19,25 @@ import javax.inject.Provider
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class AutoReporting @Inject constructor(
|
||||
class GplayAutoReporting @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
private val debugSettings: DebugSettings,
|
||||
private val installId: InstallId,
|
||||
private val bugsnagLogger: Provider<BugsnagLogger>,
|
||||
private val bugsnagErrorHandler: Provider<BugsnagErrorHandler>,
|
||||
private val nopBugsnagErrorHandler: Provider<NOPBugsnagErrorHandler>,
|
||||
) {
|
||||
) : AutomaticBugReporter {
|
||||
|
||||
fun setup() {
|
||||
override fun setup(application: Application) {
|
||||
val isEnabled = debugSettings.isAutoReportingEnabled.value
|
||||
log(TAG) { "setup(): isEnabled=$isEnabled" }
|
||||
|
||||
if (!isEnabled) return
|
||||
|
||||
ReLinker
|
||||
.log { message -> log(TAG) { "ReLinker: $message" } }
|
||||
.loadLibrary(application, "bugsnag-plugin-android-anr")
|
||||
|
||||
try {
|
||||
val bugsnagConfig = Configuration.load(context).apply {
|
||||
if (debugSettings.isAutoReportingEnabled.value) {
|
||||
@@ -48,13 +55,18 @@ class AutoReporting @Inject constructor(
|
||||
}
|
||||
|
||||
Bugsnag.start(context, bugsnagConfig)
|
||||
Bugs.ready = true
|
||||
Bugs.reporter = this
|
||||
} catch (e: IllegalStateException) {
|
||||
log(TAG) { "Bugsnag API Key not configured." }
|
||||
}
|
||||
}
|
||||
|
||||
override fun notify(throwable: Throwable) {
|
||||
Bugsnag.notify(throwable)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = logTag("Debug", "AutoReport")
|
||||
}
|
||||
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package eu.darken.capod.common.debug.autoreport.bugsnag
|
||||
package eu.darken.capod.debug.autoreport
|
||||
|
||||
import com.bugsnag.android.Event
|
||||
import com.bugsnag.android.OnErrorCallback
|
||||
@@ -2,7 +2,7 @@ package eu.darken.capod.common.bluetooth
|
||||
|
||||
import dagger.Reusable
|
||||
import eu.darken.capod.common.SystemClockWrap
|
||||
import eu.darken.capod.common.debug.autoreport.DebugSettings
|
||||
import eu.darken.capod.common.debug.DebugSettings
|
||||
import java.time.Instant
|
||||
import javax.inject.Inject
|
||||
import kotlin.random.Random
|
||||
@@ -27,9 +27,7 @@ class FakeBleData @Inject constructor(
|
||||
generatedAtNanos = SystemClockWrap.elapsedRealtimeNanos + 100,
|
||||
manufacturerSpecificData = mapOf(76 to "07 19 01 02 20 75 AA B6 31 00 05 9C 5A A4 5D C0 2C A0 B4 6F B9 ED 8E CE 03 97 CA".hexToByteArray())
|
||||
).run {
|
||||
if (Random.nextBoolean()) {
|
||||
fakeDevices.add(this)
|
||||
}
|
||||
fakeDevices.add(this)
|
||||
}
|
||||
|
||||
// AirPods Gen2
|
||||
@@ -40,9 +38,7 @@ class FakeBleData @Inject constructor(
|
||||
generatedAtNanos = SystemClockWrap.elapsedRealtimeNanos + 100,
|
||||
manufacturerSpecificData = mapOf(76 to "07 19 01 0F 20 75 AA B6 31 00 05 9C 5A A4 5D C0 2C A0 B4 6F B9 ED 8E CE 03 97 CA".hexToByteArray())
|
||||
).run {
|
||||
if (Random.nextBoolean()) {
|
||||
fakeDevices.add(this)
|
||||
}
|
||||
fakeDevices.add(this)
|
||||
}
|
||||
|
||||
// AirPods Gen3
|
||||
@@ -53,9 +49,7 @@ class FakeBleData @Inject constructor(
|
||||
generatedAtNanos = SystemClockWrap.elapsedRealtimeNanos + 200,
|
||||
manufacturerSpecificData = mapOf(76 to "07 19 01 13 20 55 AF 56 31 00 06 6F E4 DF 10 AF 10 60 81 03 3B 76 D9 C7 11 22 88".hexToByteArray())
|
||||
).run {
|
||||
if (Random.nextBoolean()) {
|
||||
fakeDevices.add(this)
|
||||
}
|
||||
fakeDevices.add(this)
|
||||
}
|
||||
// AirPods Max
|
||||
BleScanResult(
|
||||
@@ -65,9 +59,7 @@ class FakeBleData @Inject constructor(
|
||||
generatedAtNanos = SystemClockWrap.elapsedRealtimeNanos + 300,
|
||||
manufacturerSpecificData = mapOf(76 to "07 19 01 0A 20 02 05 80 04 0F 44 A7 60 9B F8 3C FD B1 D8 1C 61 EA 82 60 A3 2C 4E".hexToByteArray())
|
||||
).run {
|
||||
if (Random.nextBoolean()) {
|
||||
fakeDevices.add(this)
|
||||
}
|
||||
fakeDevices.add(this)
|
||||
}
|
||||
// BeatsFlex
|
||||
BleScanResult(
|
||||
@@ -77,9 +69,7 @@ class FakeBleData @Inject constructor(
|
||||
generatedAtNanos = SystemClockWrap.elapsedRealtimeNanos + 400,
|
||||
manufacturerSpecificData = mapOf(76 to "07 19 01 10 20 0A F4 8F 00 01 00 C4 71 9F 9C EF A2 E3 BA 66 FE 1D 45 9F C9 2F A0".hexToByteArray())
|
||||
).run {
|
||||
if (Random.nextBoolean()) {
|
||||
fakeDevices.add(this)
|
||||
}
|
||||
fakeDevices.add(this)
|
||||
}
|
||||
|
||||
// Tws i99999
|
||||
@@ -90,9 +80,7 @@ class FakeBleData @Inject constructor(
|
||||
generatedAtNanos = SystemClockWrap.elapsedRealtimeNanos + 400,
|
||||
manufacturerSpecificData = mapOf(76 to "07 13 01 02 20 71 AA 37 32 00 10 00 64 64 FF 00 00 00 00 00 00".hexToByteArray())
|
||||
).run {
|
||||
if (Random.nextBoolean()) {
|
||||
fakeDevices.add(this)
|
||||
}
|
||||
fakeDevices.add(this)
|
||||
}
|
||||
|
||||
// Unknown Device
|
||||
@@ -103,9 +91,7 @@ class FakeBleData @Inject constructor(
|
||||
generatedAtNanos = SystemClockWrap.elapsedRealtimeNanos + 500,
|
||||
manufacturerSpecificData = mapOf(76 to "07 19 01 FF 20 0A F4 8F 00 01 00 C4 71 9F 9C EF A2 E3 BA 66 FE 1D 45 9F C9 2F A0".hexToByteArray())
|
||||
).run {
|
||||
if (Random.nextBoolean()) {
|
||||
fakeDevices.add(this)
|
||||
}
|
||||
fakeDevices.add(this)
|
||||
}
|
||||
|
||||
return fakeDevices
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
package eu.darken.capod.common.collections
|
||||
|
||||
|
||||
fun Collection<Int>.median(): Int = this.sorted().let {
|
||||
if (it.size % 2 == 0) {
|
||||
(it[it.size / 2] + it[(it.size - 1) / 2]) / 2
|
||||
} else {
|
||||
it[it.size / 2]
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
package eu.darken.capod.common.debug
|
||||
|
||||
import com.bugsnag.android.Bugsnag
|
||||
import eu.darken.capod.common.debug.autoreport.AutomaticBugReporter
|
||||
import eu.darken.capod.common.debug.logging.Logging.Priority.*
|
||||
import eu.darken.capod.common.debug.logging.asLog
|
||||
import eu.darken.capod.common.debug.logging.log
|
||||
import eu.darken.capod.common.debug.logging.logTag
|
||||
|
||||
object Bugs {
|
||||
var ready = false
|
||||
var reporter: AutomaticBugReporter? = null
|
||||
fun report(
|
||||
tag: String,
|
||||
message: String,
|
||||
@@ -16,11 +16,9 @@ object Bugs {
|
||||
log(TAG, VERBOSE) { "Reporting $exception" }
|
||||
log(tag, ERROR) { "$message\n${exception.asLog()}" }
|
||||
|
||||
if (!ready) {
|
||||
reporter?.notify(exception) ?: run {
|
||||
log(TAG, WARN) { "Bug tracking not initialized yet." }
|
||||
return
|
||||
}
|
||||
Bugsnag.notify(exception)
|
||||
}
|
||||
|
||||
private val TAG = logTag("Bugs")
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package eu.darken.capod.common.debug.autoreport
|
||||
package eu.darken.capod.common.debug
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
package eu.darken.capod.common.debug.autoreport
|
||||
|
||||
import android.app.Application
|
||||
|
||||
interface AutomaticBugReporter {
|
||||
|
||||
fun setup(application: Application)
|
||||
|
||||
fun notify(throwable: Throwable)
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import androidx.preference.PreferenceDataStore
|
||||
import com.squareup.moshi.Moshi
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import eu.darken.capod.common.bluetooth.ScannerMode
|
||||
import eu.darken.capod.common.debug.autoreport.DebugSettings
|
||||
import eu.darken.capod.common.debug.DebugSettings
|
||||
import eu.darken.capod.common.preferences.PreferenceStoreMapper
|
||||
import eu.darken.capod.common.preferences.Settings
|
||||
import eu.darken.capod.common.preferences.createFlowPreference
|
||||
@@ -26,7 +26,7 @@ class GeneralSettings @Inject constructor(
|
||||
val monitorMode = preferences.createFlowPreference("core.monitor.mode", MonitorMode.AUTOMATIC, moshi)
|
||||
val scannerMode = preferences.createFlowPreference("core.scanner.mode", ScannerMode.BALANCED, moshi)
|
||||
|
||||
val showAll = preferences.createFlowPreference("core.showall.enabled", false)
|
||||
val showAll = preferences.createFlowPreference("core.showall.enabled", true)
|
||||
|
||||
val minimumSignalQuality = preferences.createFlowPreference("core.signal.minimum", 0.25f)
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import eu.darken.capod.common.bluetooth.BleScanner
|
||||
import eu.darken.capod.common.bluetooth.BluetoothManager2
|
||||
import eu.darken.capod.common.bluetooth.ScannerMode
|
||||
import eu.darken.capod.common.coroutine.AppScope
|
||||
import eu.darken.capod.common.debug.autoreport.DebugSettings
|
||||
import eu.darken.capod.common.debug.DebugSettings
|
||||
import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE
|
||||
import eu.darken.capod.common.debug.logging.Logging.Priority.WARN
|
||||
import eu.darken.capod.common.debug.logging.asLog
|
||||
|
||||
@@ -6,6 +6,9 @@ import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import eu.darken.capod.common.R
|
||||
import eu.darken.capod.common.bluetooth.BleScanResult
|
||||
import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE
|
||||
import eu.darken.capod.common.debug.logging.log
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
import kotlin.math.abs
|
||||
@@ -31,14 +34,20 @@ interface PodDevice {
|
||||
val rssi: Int
|
||||
get() = scanResult.rssi
|
||||
|
||||
val confidence: Float
|
||||
val reliability: Float
|
||||
|
||||
/**
|
||||
* This is not correct but it works ¯\_(ツ)_/¯
|
||||
* The range of the RSSI is device specific (ROMs).
|
||||
*/
|
||||
val signalQuality: Float
|
||||
get() = ((100 - abs(rssi)) / 100f) * (max(BASE_CONFIDENCE, confidence))
|
||||
get() {
|
||||
/**
|
||||
* This is not correct but it works ¯\_(ツ)_/¯
|
||||
* The range of the RSSI is device specific (ROMs).
|
||||
*/
|
||||
val sqRssi = ((100 - abs(rssi)) / 100f)
|
||||
val sqReliability = max(BASE_CONFIDENCE, reliability)
|
||||
val sqAge = (Duration.between(seenFirstAt, Instant.now()).toMinutes().coerceAtMost(60) / 60f) * 0.25f
|
||||
log(VERBOSE) { "Signal Quality ($address): rssi=$sqRssi, reliability=$reliability, age=$sqAge" }
|
||||
return (sqRssi + sqReliability + sqAge) / 2f
|
||||
}
|
||||
|
||||
val rawData: Map<Int, ByteArray>
|
||||
get() = scanResult.manufacturerSpecificData
|
||||
@@ -133,6 +142,6 @@ interface PodDevice {
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val BASE_CONFIDENCE = 0.5f
|
||||
const val BASE_CONFIDENCE = 0.0f
|
||||
}
|
||||
}
|
||||
@@ -25,8 +25,8 @@ fun SinglePodDevice.getBatteryLevelHeadset(context: Context): String =
|
||||
?: context.getString(R.string.general_value_not_available_label)
|
||||
|
||||
fun PodDevice.getSignalQuality(context: Context): String {
|
||||
val percentage = 100 * signalQuality
|
||||
return "~${percentage.roundToInt()}%"
|
||||
val multiplier = 100 * signalQuality
|
||||
return "${multiplier.roundToInt()}"
|
||||
}
|
||||
|
||||
@DrawableRes
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package eu.darken.capod.pods.core.apple
|
||||
|
||||
import eu.darken.capod.common.bluetooth.BleScanResult
|
||||
import eu.darken.capod.common.collections.median
|
||||
import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE
|
||||
import eu.darken.capod.common.debug.logging.Logging.Priority.WARN
|
||||
import eu.darken.capod.common.debug.logging.log
|
||||
@@ -8,10 +9,10 @@ import eu.darken.capod.common.lowerNibble
|
||||
import eu.darken.capod.common.upperNibble
|
||||
import eu.darken.capod.pods.core.HasCase
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.apple.airpods.AirPodsPro
|
||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
import kotlin.math.max
|
||||
|
||||
abstract class ApplePodsFactory<PodType : ApplePods>(private val tag: String) {
|
||||
|
||||
@@ -38,7 +39,8 @@ abstract class ApplePodsFactory<PodType : ApplePods>(private val tag: String) {
|
||||
val id: PodDevice.Id,
|
||||
val seenFirstAt: Instant,
|
||||
val seenCounter: Int,
|
||||
val history: List<ApplePods>
|
||||
val history: List<ApplePods>,
|
||||
val lastCaseBattery: Float?,
|
||||
) {
|
||||
val lastMessage: ProximityPairing.Message
|
||||
get() = history.last().proximityMessage
|
||||
@@ -46,63 +48,81 @@ abstract class ApplePodsFactory<PodType : ApplePods>(private val tag: String) {
|
||||
val lastAddress: String
|
||||
get() = history.last().address
|
||||
|
||||
val confidence: Float
|
||||
get() = 0.75f + (history.size / (MAX_HISTORY * 4f))
|
||||
val reliability: Float
|
||||
get() {
|
||||
if (history.size < 2) return 0f
|
||||
|
||||
fun averageRssi(latest: Int): Int =
|
||||
history.map { it.rssi }.plus(latest).takeLast(10).median()
|
||||
val now = Instant.now()
|
||||
|
||||
val pingInterval = List(history.dropLast(1).size) { index ->
|
||||
Duration.between(history[index].seenLastAt, history[index + 1].seenLastAt).toMillis()
|
||||
}.map { it.toInt() }.median()
|
||||
|
||||
val expectedPings: Float = LOOKBACK.toMillis() / pingInterval.toFloat()
|
||||
|
||||
val recentPings = history.filter {
|
||||
Duration.between(it.seenLastAt, now).toMillis() < LOOKBACK.toMillis() + pingInterval
|
||||
}.size
|
||||
|
||||
val reliability: Float = (recentPings / expectedPings)
|
||||
// log("#####") { "interval=$pingInterval, expectedPerMinute=$expectedPings, count=$recentPings" }
|
||||
|
||||
return max(0.0f, reliability).coerceAtMost(1f)
|
||||
}
|
||||
|
||||
fun rssiSmoothed(latest: Int): Int {
|
||||
val now = Instant.now()
|
||||
return history
|
||||
.filter { Duration.between(it.seenLastAt, now) < LOOKBACK }
|
||||
.map { it.rssi }
|
||||
.plus(latest)
|
||||
.median()
|
||||
}
|
||||
|
||||
fun isOlderThan(age: Duration): Boolean {
|
||||
val now = Instant.now()
|
||||
return Duration.between(history.last().seenLastAt, now) > age
|
||||
}
|
||||
|
||||
private fun List<Int>.median(): Int = this.sorted().let {
|
||||
if (it.size % 2 == 0)
|
||||
(it[it.size / 2] + it[(it.size - 1) / 2]) / 2
|
||||
else
|
||||
it[it.size / 2]
|
||||
}
|
||||
|
||||
override fun toString(): String = "KnownDevice(history=${history.size}, last=${history.last()})"
|
||||
|
||||
companion object {
|
||||
const val MAX_HISTORY = 20
|
||||
// 30 seconds at fastest interval
|
||||
const val MAX_HISTORY = 60
|
||||
val LOOKBACK = Duration.ofSeconds(30)
|
||||
}
|
||||
}
|
||||
|
||||
internal val knownDevices = mutableMapOf<PodDevice.Id, KnownDevice>()
|
||||
|
||||
|
||||
fun KnownDevice.getLatestCaseBattery(): Float? = history
|
||||
fun KnownDevice.getLatestCaseBattery(): Float? = this.lastCaseBattery
|
||||
|
||||
private fun Collection<ApplePods>.determineLatestCaseBattery(): Float? = this
|
||||
.filterIsInstance<HasCase>()
|
||||
.mapNotNull { it.batteryCasePercent }
|
||||
.lastOrNull()
|
||||
|
||||
fun KnownDevice.getLatestCaseLidState(basic: DualApplePods): DualApplePods.LidState? {
|
||||
val definitive = setOf(DualApplePods.LidState.OPEN, DualApplePods.LidState.CLOSED)
|
||||
if (definitive.contains(basic.caseLidState)) return null
|
||||
val definitive = setOf(
|
||||
DualApplePods.LidState.OPEN,
|
||||
DualApplePods.LidState.CLOSED,
|
||||
DualApplePods.LidState.NOT_IN_CASE,
|
||||
)
|
||||
if (definitive.contains(basic.caseLidState)) return basic.caseLidState
|
||||
|
||||
return history
|
||||
.filterIsInstance<AirPodsPro>() // TODO why is this AirPodsPro specific here?
|
||||
.lastOrNull { it.caseLidState != DualApplePods.LidState.NOT_IN_CASE }
|
||||
.takeLast(2)
|
||||
.filterIsInstance<DualApplePods>()
|
||||
.lastOrNull { it.caseLidState != DualApplePods.LidState.UNKNOWN }
|
||||
?.caseLidState
|
||||
?: DualApplePods.LidState.NOT_IN_CASE
|
||||
}
|
||||
|
||||
open fun historyTrimmer(
|
||||
pods: List<ApplePods>
|
||||
): List<ApplePods> {
|
||||
var trimmed = pods.takeLast(KnownDevice.MAX_HISTORY)
|
||||
|
||||
// We want to keep the case information
|
||||
// If both pods are removed, and produce more history, we otherwise lose the case battery info.
|
||||
val caseInfoFat = pods.lastOrNull { it is HasCase && it.batteryCasePercent != null }
|
||||
val caseInfoTrimmed = trimmed.lastOrNull { it is HasCase && it.batteryCasePercent != null }
|
||||
if (caseInfoFat != null && caseInfoTrimmed == null) {
|
||||
trimmed = listOf(caseInfoFat) + trimmed.takeLast(KnownDevice.MAX_HISTORY - 1)
|
||||
}
|
||||
|
||||
return trimmed
|
||||
return pods.takeLast(KnownDevice.MAX_HISTORY)
|
||||
}
|
||||
|
||||
internal open fun searchHistory(current: PodType): KnownDevice? {
|
||||
@@ -110,7 +130,7 @@ abstract class ApplePodsFactory<PodType : ApplePods>(private val tag: String) {
|
||||
val message = current.proximityMessage
|
||||
|
||||
knownDevices.values.toList().forEach { knownDevice ->
|
||||
if (knownDevice.isOlderThan(Duration.ofSeconds(20))) {
|
||||
if (knownDevice.isOlderThan(Duration.ofSeconds(30))) {
|
||||
log(tag, VERBOSE) { "searchHistory1: Removing stale known device: $knownDevice" }
|
||||
knownDevices.remove(knownDevice.id)
|
||||
}
|
||||
@@ -146,18 +166,22 @@ abstract class ApplePodsFactory<PodType : ApplePods>(private val tag: String) {
|
||||
|
||||
knownDevices[device.identifier] = when {
|
||||
existing != null -> {
|
||||
val history = existing.history.plus(device)
|
||||
existing.copy(
|
||||
seenCounter = existing.seenCounter + 1,
|
||||
history = existing.history.plus(device)
|
||||
history = history,
|
||||
lastCaseBattery = history.determineLatestCaseBattery() ?: existing.lastCaseBattery
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
log(tag) { "searchHistory1: Creating new history for $device" }
|
||||
val history = listOf(device)
|
||||
KnownDevice(
|
||||
id = device.identifier,
|
||||
seenFirstAt = device.seenFirstAt,
|
||||
seenCounter = 1,
|
||||
history = listOf(device)
|
||||
history = history,
|
||||
lastCaseBattery = history.determineLatestCaseBattery()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ data class AirPodsGen1 constructor(
|
||||
override val seenCounter: Int = 1,
|
||||
override val scanResult: BleScanResult,
|
||||
override val proximityMessage: ProximityPairing.Message,
|
||||
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
||||
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
|
||||
private val rssiAverage: Int? = null,
|
||||
private val cachedBatteryPercentage: Float? = null,
|
||||
private val cachedCaseState: DualApplePods.LidState? = null
|
||||
@@ -55,9 +55,9 @@ data class AirPodsGen1 constructor(
|
||||
seenFirstAt = result.seenFirstAt,
|
||||
seenLastAt = scanResult.receivedAt,
|
||||
seenCounter = result.seenCounter,
|
||||
confidence = result.confidence,
|
||||
reliability = result.reliability,
|
||||
cachedBatteryPercentage = result.getLatestCaseBattery(),
|
||||
rssiAverage = result.averageRssi(basic.rssi),
|
||||
rssiAverage = result.rssiSmoothed(basic.rssi),
|
||||
cachedCaseState = result.getLatestCaseLidState(basic)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ data class AirPodsGen2 constructor(
|
||||
override val seenCounter: Int = 1,
|
||||
override val scanResult: BleScanResult,
|
||||
override val proximityMessage: ProximityPairing.Message,
|
||||
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
||||
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
|
||||
private val rssiAverage: Int? = null,
|
||||
private val cachedBatteryPercentage: Float? = null,
|
||||
private val cachedCaseState: DualApplePods.LidState? = null
|
||||
@@ -54,9 +54,9 @@ data class AirPodsGen2 constructor(
|
||||
seenFirstAt = result.seenFirstAt,
|
||||
seenLastAt = scanResult.receivedAt,
|
||||
seenCounter = result.seenCounter,
|
||||
confidence = result.confidence,
|
||||
reliability = result.reliability,
|
||||
cachedBatteryPercentage = result.getLatestCaseBattery(),
|
||||
rssiAverage = result.averageRssi(basic.rssi),
|
||||
rssiAverage = result.rssiSmoothed(basic.rssi),
|
||||
cachedCaseState = result.getLatestCaseLidState(basic)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ data class AirPodsGen3 constructor(
|
||||
override val seenCounter: Int = 1,
|
||||
override val scanResult: BleScanResult,
|
||||
override val proximityMessage: ProximityPairing.Message,
|
||||
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
||||
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
|
||||
private val rssiAverage: Int? = null,
|
||||
private val cachedBatteryPercentage: Float? = null,
|
||||
private val cachedCaseState: DualApplePods.LidState? = null
|
||||
@@ -54,9 +54,9 @@ data class AirPodsGen3 constructor(
|
||||
seenFirstAt = result.seenFirstAt,
|
||||
seenLastAt = scanResult.receivedAt,
|
||||
seenCounter = result.seenCounter,
|
||||
confidence = result.confidence,
|
||||
reliability = result.reliability,
|
||||
cachedBatteryPercentage = result.getLatestCaseBattery(),
|
||||
rssiAverage = result.averageRssi(basic.rssi),
|
||||
rssiAverage = result.rssiSmoothed(basic.rssi),
|
||||
cachedCaseState = result.getLatestCaseLidState(basic)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import eu.darken.capod.common.isBitSet
|
||||
import eu.darken.capod.pods.core.HasEarDetection
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.apple.ApplePods
|
||||
import eu.darken.capod.pods.core.apple.HasAppleColor
|
||||
import eu.darken.capod.pods.core.apple.SingleApplePods
|
||||
import eu.darken.capod.pods.core.apple.SingleApplePodsFactory
|
||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||
@@ -19,9 +20,9 @@ data class AirPodsMax(
|
||||
override val seenCounter: Int = 1,
|
||||
override val scanResult: BleScanResult,
|
||||
override val proximityMessage: ProximityPairing.Message,
|
||||
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
||||
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
|
||||
private val rssiAverage: Int? = null,
|
||||
) : SingleApplePods, HasEarDetection, HasStateDetectionAirPods {
|
||||
) : SingleApplePods, HasEarDetection, HasAppleColor {
|
||||
|
||||
override val model: PodDevice.Model = PodDevice.Model.AIRPODS_MAX
|
||||
|
||||
@@ -57,8 +58,8 @@ data class AirPodsMax(
|
||||
seenFirstAt = result.seenFirstAt,
|
||||
seenLastAt = scanResult.receivedAt,
|
||||
seenCounter = result.seenCounter,
|
||||
confidence = result.confidence,
|
||||
rssiAverage = result.averageRssi(basic.rssi),
|
||||
reliability = result.reliability,
|
||||
rssiAverage = result.rssiSmoothed(basic.rssi),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ data class AirPodsPro(
|
||||
override val seenCounter: Int = 1,
|
||||
override val scanResult: BleScanResult,
|
||||
override val proximityMessage: ProximityPairing.Message,
|
||||
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
||||
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
|
||||
private val rssiAverage: Int? = null,
|
||||
private val cachedBatteryPercentage: Float? = null,
|
||||
private val cachedCaseState: LidState? = null
|
||||
@@ -73,9 +73,9 @@ data class AirPodsPro(
|
||||
seenFirstAt = result.seenFirstAt,
|
||||
seenLastAt = scanResult.receivedAt,
|
||||
seenCounter = result.seenCounter,
|
||||
confidence = result.confidence,
|
||||
reliability = result.reliability,
|
||||
cachedBatteryPercentage = result.getLatestCaseBattery(),
|
||||
rssiAverage = result.averageRssi(basic.rssi),
|
||||
rssiAverage = result.rssiSmoothed(basic.rssi),
|
||||
cachedCaseState = result.getLatestCaseLidState(basic)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ data class AirPodsPro2(
|
||||
override val seenCounter: Int = 1,
|
||||
override val scanResult: BleScanResult,
|
||||
override val proximityMessage: ProximityPairing.Message,
|
||||
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
||||
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
|
||||
private val rssiAverage: Int? = null,
|
||||
private val cachedBatteryPercentage: Float? = null,
|
||||
private val cachedCaseState: LidState? = null
|
||||
@@ -73,9 +73,9 @@ data class AirPodsPro2(
|
||||
seenFirstAt = result.seenFirstAt,
|
||||
seenLastAt = scanResult.receivedAt,
|
||||
seenCounter = result.seenCounter,
|
||||
confidence = result.confidence,
|
||||
reliability = result.reliability,
|
||||
cachedBatteryPercentage = result.getLatestCaseBattery(),
|
||||
rssiAverage = result.averageRssi(basic.rssi),
|
||||
rssiAverage = result.rssiSmoothed(basic.rssi),
|
||||
cachedCaseState = result.getLatestCaseLidState(basic)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ data class BeatsFitPro(
|
||||
override val seenCounter: Int = 1,
|
||||
override val scanResult: BleScanResult,
|
||||
override val proximityMessage: ProximityPairing.Message,
|
||||
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
||||
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
|
||||
private val rssiAverage: Int? = null,
|
||||
private val cachedBatteryPercentage: Float? = null,
|
||||
private val cachedCaseState: DualApplePods.LidState? = null
|
||||
@@ -54,9 +54,9 @@ data class BeatsFitPro(
|
||||
seenFirstAt = result.seenFirstAt,
|
||||
seenLastAt = scanResult.receivedAt,
|
||||
seenCounter = result.seenCounter,
|
||||
confidence = result.confidence,
|
||||
reliability = result.reliability,
|
||||
cachedBatteryPercentage = result.getLatestCaseBattery(),
|
||||
rssiAverage = result.averageRssi(basic.rssi),
|
||||
rssiAverage = result.rssiSmoothed(basic.rssi),
|
||||
cachedCaseState = result.getLatestCaseLidState(basic)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ data class BeatsFlex(
|
||||
override val seenCounter: Int = 1,
|
||||
override val scanResult: BleScanResult,
|
||||
override val proximityMessage: ProximityPairing.Message,
|
||||
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
||||
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
|
||||
private val rssiAverage: Int? = null,
|
||||
) : SingleApplePods {
|
||||
|
||||
@@ -46,8 +46,8 @@ data class BeatsFlex(
|
||||
seenFirstAt = result.seenFirstAt,
|
||||
seenLastAt = scanResult.receivedAt,
|
||||
seenCounter = result.seenCounter,
|
||||
confidence = result.confidence,
|
||||
rssiAverage = result.averageRssi(basic.rssi),
|
||||
reliability = result.reliability,
|
||||
rssiAverage = result.rssiSmoothed(basic.rssi),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ data class BeatsSolo3(
|
||||
override val seenCounter: Int = 1,
|
||||
override val scanResult: BleScanResult,
|
||||
override val proximityMessage: ProximityPairing.Message,
|
||||
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
||||
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
|
||||
private val rssiAverage: Int? = null,
|
||||
) : SingleApplePods {
|
||||
|
||||
@@ -45,8 +45,8 @@ data class BeatsSolo3(
|
||||
seenFirstAt = result.seenFirstAt,
|
||||
seenLastAt = scanResult.receivedAt,
|
||||
seenCounter = result.seenCounter,
|
||||
confidence = result.confidence,
|
||||
rssiAverage = result.averageRssi(basic.rssi),
|
||||
reliability = result.reliability,
|
||||
rssiAverage = result.rssiSmoothed(basic.rssi),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ data class BeatsStudio3(
|
||||
override val seenCounter: Int = 1,
|
||||
override val scanResult: BleScanResult,
|
||||
override val proximityMessage: ProximityPairing.Message,
|
||||
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
||||
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
|
||||
private val rssiAverage: Int? = null,
|
||||
) : SingleApplePods {
|
||||
|
||||
@@ -43,8 +43,8 @@ data class BeatsStudio3(
|
||||
seenFirstAt = result.seenFirstAt,
|
||||
seenLastAt = scanResult.receivedAt,
|
||||
seenCounter = result.seenCounter,
|
||||
confidence = result.confidence,
|
||||
rssiAverage = result.averageRssi(basic.rssi),
|
||||
reliability = result.reliability,
|
||||
rssiAverage = result.rssiSmoothed(basic.rssi),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ data class BeatsX(
|
||||
override val seenCounter: Int = 1,
|
||||
override val scanResult: BleScanResult,
|
||||
override val proximityMessage: ProximityPairing.Message,
|
||||
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
||||
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
|
||||
private val rssiAverage: Int? = null,
|
||||
) : SingleApplePods {
|
||||
|
||||
@@ -46,8 +46,8 @@ data class BeatsX(
|
||||
seenFirstAt = result.seenFirstAt,
|
||||
seenLastAt = scanResult.receivedAt,
|
||||
seenCounter = result.seenCounter,
|
||||
confidence = result.confidence,
|
||||
rssiAverage = result.averageRssi(basic.rssi),
|
||||
reliability = result.reliability,
|
||||
rssiAverage = result.rssiSmoothed(basic.rssi),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ data class PowerBeats3(
|
||||
override val seenCounter: Int = 1,
|
||||
override val scanResult: BleScanResult,
|
||||
override val proximityMessage: ProximityPairing.Message,
|
||||
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
||||
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
|
||||
private val rssiAverage: Int? = null,
|
||||
) : SingleApplePods {
|
||||
|
||||
@@ -46,8 +46,8 @@ data class PowerBeats3(
|
||||
seenFirstAt = result.seenFirstAt,
|
||||
seenLastAt = scanResult.receivedAt,
|
||||
seenCounter = result.seenCounter,
|
||||
confidence = result.confidence,
|
||||
rssiAverage = result.averageRssi(basic.rssi),
|
||||
reliability = result.reliability,
|
||||
rssiAverage = result.rssiSmoothed(basic.rssi),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ data class PowerBeatsPro(
|
||||
override val seenCounter: Int = 1,
|
||||
override val scanResult: BleScanResult,
|
||||
override val proximityMessage: ProximityPairing.Message,
|
||||
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
||||
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
|
||||
private val rssiAverage: Int? = null,
|
||||
private val cachedBatteryPercentage: Float? = null,
|
||||
private val cachedCaseState: DualApplePods.LidState? = null
|
||||
@@ -54,9 +54,9 @@ data class PowerBeatsPro(
|
||||
seenFirstAt = result.seenFirstAt,
|
||||
seenLastAt = scanResult.receivedAt,
|
||||
seenCounter = result.seenCounter,
|
||||
confidence = result.confidence,
|
||||
reliability = result.reliability,
|
||||
cachedBatteryPercentage = result.getLatestCaseBattery(),
|
||||
rssiAverage = result.averageRssi(basic.rssi),
|
||||
rssiAverage = result.rssiSmoothed(basic.rssi),
|
||||
cachedCaseState = result.getLatestCaseLidState(basic)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ data class FakeAirPodsGen1 constructor(
|
||||
override val seenCounter: Int = 1,
|
||||
override val scanResult: BleScanResult,
|
||||
override val proximityMessage: ProximityPairing.Message,
|
||||
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
||||
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
|
||||
private val rssiAverage: Int? = null,
|
||||
private val cachedBatteryPercentage: Float? = null,
|
||||
) : ApplePods, DualPodDevice, HasEarDetectionDual, HasChargeDetectionDual, HasCase {
|
||||
@@ -138,9 +138,9 @@ data class FakeAirPodsGen1 constructor(
|
||||
seenFirstAt = result.seenFirstAt,
|
||||
seenLastAt = scanResult.receivedAt,
|
||||
seenCounter = result.seenCounter,
|
||||
confidence = result.confidence,
|
||||
reliability = result.reliability,
|
||||
cachedBatteryPercentage = result.getLatestCaseBattery(),
|
||||
rssiAverage = result.averageRssi(basic.rssi),
|
||||
rssiAverage = result.rssiSmoothed(basic.rssi),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ data class FakeAirPodsGen2 constructor(
|
||||
override val seenCounter: Int = 1,
|
||||
override val scanResult: BleScanResult,
|
||||
override val proximityMessage: ProximityPairing.Message,
|
||||
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
||||
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
|
||||
private val rssiAverage: Int? = null,
|
||||
private val cachedBatteryPercentage: Float? = null,
|
||||
) : ApplePods, DualPodDevice, HasEarDetectionDual, HasChargeDetectionDual, HasCase {
|
||||
@@ -136,9 +136,9 @@ data class FakeAirPodsGen2 constructor(
|
||||
seenFirstAt = result.seenFirstAt,
|
||||
seenLastAt = scanResult.receivedAt,
|
||||
seenCounter = result.seenCounter,
|
||||
confidence = result.confidence,
|
||||
reliability = result.reliability,
|
||||
cachedBatteryPercentage = result.getLatestCaseBattery(),
|
||||
rssiAverage = result.averageRssi(basic.rssi),
|
||||
rssiAverage = result.rssiSmoothed(basic.rssi),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ data class FakeAirPodsGen3 constructor(
|
||||
override val seenCounter: Int = 1,
|
||||
override val scanResult: BleScanResult,
|
||||
override val proximityMessage: ProximityPairing.Message,
|
||||
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
||||
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
|
||||
private val rssiAverage: Int? = null,
|
||||
private val cachedBatteryPercentage: Float? = null,
|
||||
) : ApplePods, DualPodDevice, HasEarDetectionDual, HasChargeDetectionDual, HasCase {
|
||||
@@ -138,9 +138,9 @@ data class FakeAirPodsGen3 constructor(
|
||||
seenFirstAt = result.seenFirstAt,
|
||||
seenLastAt = scanResult.receivedAt,
|
||||
seenCounter = result.seenCounter,
|
||||
confidence = result.confidence,
|
||||
reliability = result.reliability,
|
||||
cachedBatteryPercentage = result.getLatestCaseBattery(),
|
||||
rssiAverage = result.averageRssi(basic.rssi),
|
||||
rssiAverage = result.rssiSmoothed(basic.rssi),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ data class FakeAirPodsPro constructor(
|
||||
override val seenCounter: Int = 1,
|
||||
override val scanResult: BleScanResult,
|
||||
override val proximityMessage: ProximityPairing.Message,
|
||||
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
||||
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
|
||||
private val rssiAverage: Int? = null,
|
||||
private val cachedBatteryPercentage: Float? = null,
|
||||
) : ApplePods, DualPodDevice, HasChargeDetectionDual, HasEarDetectionDual, HasCase {
|
||||
@@ -156,9 +156,9 @@ data class FakeAirPodsPro constructor(
|
||||
seenFirstAt = result.seenFirstAt,
|
||||
seenLastAt = scanResult.receivedAt,
|
||||
seenCounter = result.seenCounter,
|
||||
confidence = result.confidence,
|
||||
reliability = result.reliability,
|
||||
cachedBatteryPercentage = result.getLatestCaseBattery(),
|
||||
rssiAverage = result.averageRssi(basic.rssi),
|
||||
rssiAverage = result.rssiSmoothed(basic.rssi),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ data class FakeAirPodsPro2 constructor(
|
||||
override val seenCounter: Int = 1,
|
||||
override val scanResult: BleScanResult,
|
||||
override val proximityMessage: ProximityPairing.Message,
|
||||
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
||||
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
|
||||
private val rssiAverage: Int? = null,
|
||||
private val cachedBatteryPercentage: Float? = null,
|
||||
private val cachedCaseState: DualApplePods.LidState? = null,
|
||||
@@ -153,9 +153,9 @@ data class FakeAirPodsPro2 constructor(
|
||||
seenFirstAt = result.seenFirstAt,
|
||||
seenLastAt = scanResult.receivedAt,
|
||||
seenCounter = result.seenCounter,
|
||||
confidence = result.confidence,
|
||||
reliability = result.reliability,
|
||||
cachedBatteryPercentage = result.getLatestCaseBattery(),
|
||||
rssiAverage = result.averageRssi(basic.rssi),
|
||||
rssiAverage = result.rssiSmoothed(basic.rssi),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ data class UnknownAppleDevice(
|
||||
override val seenCounter: Int = 1,
|
||||
override val scanResult: BleScanResult,
|
||||
override val proximityMessage: ProximityPairing.Message,
|
||||
override val confidence: Float = 0f,
|
||||
override val reliability: Float = 0f,
|
||||
private val rssiAverage: Int? = null,
|
||||
) : ApplePods {
|
||||
|
||||
@@ -49,8 +49,8 @@ data class UnknownAppleDevice(
|
||||
seenFirstAt = result.seenFirstAt,
|
||||
seenLastAt = scanResult.receivedAt,
|
||||
seenCounter = result.seenCounter,
|
||||
confidence = result.confidence,
|
||||
rssiAverage = result.averageRssi(basic.rssi),
|
||||
reliability = result.reliability,
|
||||
rssiAverage = result.rssiSmoothed(basic.rssi),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ data class UnknownDevice(
|
||||
override val seenFirstAt: Instant = Instant.now(),
|
||||
override val seenCounter: Int = 1,
|
||||
override val scanResult: BleScanResult,
|
||||
override val confidence: Float = 0f,
|
||||
override val reliability: Float = 0f,
|
||||
private val rssiAverage: Int? = null,
|
||||
) : PodDevice {
|
||||
|
||||
|
||||
@@ -34,8 +34,8 @@ class UnknownDeviceFactory @Inject constructor() {
|
||||
seenFirstAt = result.seenFirstAt,
|
||||
seenLastAt = scanResult.receivedAt,
|
||||
seenCounter = result.seenCounter,
|
||||
confidence = result.confidence,
|
||||
rssiAverage = result.averageRssi(basic.rssi),
|
||||
reliability = result.confidence,
|
||||
rssiAverage = result.rssiSmoothed(basic.rssi),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ class UnknownDeviceFactory @Inject constructor() {
|
||||
val confidence: Float
|
||||
get() = 0.75f + (history.size / (MAX_HISTORY * 4f))
|
||||
|
||||
fun averageRssi(latest: Int): Int =
|
||||
fun rssiSmoothed(latest: Int): Int =
|
||||
history.map { it.rssi }.plus(latest).takeLast(10).median()
|
||||
|
||||
fun isOlderThan(age: Duration): Boolean {
|
||||
@@ -70,7 +70,7 @@ class UnknownDeviceFactory @Inject constructor() {
|
||||
override fun toString(): String = "KnownDevice(history=${history.size}, last=${history.last()})"
|
||||
|
||||
companion object {
|
||||
const val MAX_HISTORY = 10
|
||||
const val MAX_HISTORY = 20
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package eu.darken.capod.pods.core.apple.airpods
|
||||
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.apple.BaseAirPodsTest
|
||||
import eu.darken.capod.pods.core.apple.HasAppleColor
|
||||
import io.kotest.matchers.shouldBe
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Test
|
||||
@@ -53,4 +54,27 @@ class AirPodsMaxTest : BaseAirPodsTest() {
|
||||
isHeadphonesBeingWorn shouldBe true
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `some dude at the gym`() = runTest {
|
||||
create<AirPodsMax>("07 19 01 0A 20 23 07 80 03 03 65 1F 28 32 D0 D9 71 43 00 9A 40 E7 6B EA 6C 2C FB") {
|
||||
rawPrefix shouldBe 0x01.toUByte()
|
||||
rawDeviceModel shouldBe 0x0A20.toUShort()
|
||||
rawStatus shouldBe 0x23.toUByte()
|
||||
rawPodsBattery shouldBe 0x07.toUByte()
|
||||
rawFlags shouldBe 0x8.toUShort()
|
||||
rawCaseBattery shouldBe 0x0.toUShort()
|
||||
rawCaseLidState shouldBe 0x03.toUByte()
|
||||
rawDeviceColor shouldBe 0x03.toUByte()
|
||||
rawSuffix shouldBe 0x65.toUByte()
|
||||
|
||||
batteryHeadsetPercent shouldBe 0.7f
|
||||
|
||||
isHeadsetBeingCharged shouldBe false
|
||||
|
||||
isHeadphonesBeingWorn shouldBe true
|
||||
|
||||
podStyle shouldBe HasAppleColor.DeviceColor.BLUE
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,6 @@ plugins {
|
||||
}
|
||||
apply(plugin = "dagger.hilt.android.plugin")
|
||||
apply(plugin = "androidx.navigation.safeargs.kotlin")
|
||||
apply(plugin = "com.bugsnag.android.gradle")
|
||||
|
||||
android {
|
||||
compileSdk = ProjectConfig.compileSdk
|
||||
@@ -24,10 +23,6 @@ android {
|
||||
|
||||
buildConfigField("String", "GITSHA", "\"${lastCommitHash()}\"")
|
||||
buildConfigField("String", "BUILDTIME", "\"${buildTime()}\"")
|
||||
|
||||
manifestPlaceholders["bugsnagApiKey"] = getBugSnagApiKey(
|
||||
File(System.getProperty("user.home"), ".appconfig/${ProjectConfig.packageName}/bugsnag.properties")
|
||||
) ?: "fake"
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
@@ -49,9 +44,12 @@ android {
|
||||
create("gplay") {
|
||||
dimension = "version"
|
||||
signingConfig = signingConfigs["releaseGplay"]
|
||||
extra["useBugsnag"] = true
|
||||
}
|
||||
}
|
||||
|
||||
setupBugsnagPlugin()
|
||||
|
||||
buildTypes {
|
||||
val customProguardRules = fileTree(File("../proguard")) {
|
||||
include("*.pro")
|
||||
@@ -92,7 +90,7 @@ android {
|
||||
if (listOf("release", "beta").any { variantName.toLowerCase().contains(it) }) {
|
||||
val outputFileName = ProjectConfig.packageName + "-WEAROS" +
|
||||
"-v${defaultConfig.versionName}-${defaultConfig.versionCode}" +
|
||||
"-${variantName.toUpperCase()}-${lastCommitHash()}.apk"
|
||||
"-${variantName.toUpperCase()}.apk"
|
||||
|
||||
variantOutputImpl.outputFileName = outputFileName
|
||||
}
|
||||
@@ -138,9 +136,9 @@ dependencies {
|
||||
|
||||
addTesting()
|
||||
|
||||
implementation("com.bugsnag:bugsnag-android:5.9.2")
|
||||
implementation("com.getkeepsafe.relinker:relinker:1.4.3")
|
||||
|
||||
implementation("androidx.wear:wear:1.2.0")
|
||||
implementation("androidx.wear.tiles:tiles-material:1.1.0")
|
||||
|
||||
"gplayImplementation"("com.bugsnag:bugsnag-android:5.9.2")
|
||||
"gplayImplementation"("com.getkeepsafe.relinker:relinker:1.4.3")
|
||||
}
|
||||
@@ -39,10 +39,6 @@
|
||||
android:name="androidx.startup.InitializationProvider"
|
||||
android:authorities="${applicationId}.androidx-startup"
|
||||
tools:node="remove" />
|
||||
|
||||
<meta-data
|
||||
android:name="com.bugsnag.android.API_KEY"
|
||||
android:value="${bugsnagApiKey}" />
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -3,11 +3,10 @@ package eu.darken.capod
|
||||
import android.app.Application
|
||||
import androidx.hilt.work.HiltWorkerFactory
|
||||
import androidx.work.*
|
||||
import com.getkeepsafe.relinker.ReLinker
|
||||
import dagger.hilt.android.HiltAndroidApp
|
||||
import eu.darken.capod.common.BuildConfigWrap
|
||||
import eu.darken.capod.common.coroutine.AppScope
|
||||
import eu.darken.capod.common.debug.autoreport.AutoReporting
|
||||
import eu.darken.capod.common.debug.autoreport.AutomaticBugReporter
|
||||
import eu.darken.capod.common.debug.logging.*
|
||||
import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE
|
||||
import eu.darken.capod.main.core.GeneralSettings
|
||||
@@ -20,7 +19,7 @@ import javax.inject.Inject
|
||||
open class App : Application(), Configuration.Provider {
|
||||
|
||||
@Inject lateinit var workerFactory: HiltWorkerFactory
|
||||
@Inject lateinit var autoReporting: AutoReporting
|
||||
@Inject lateinit var autoReporting: AutomaticBugReporter
|
||||
@Inject lateinit var generalSettings: GeneralSettings
|
||||
@Inject lateinit var workManager: WorkManager
|
||||
@Inject @AppScope lateinit var appScope: CoroutineScope
|
||||
@@ -29,11 +28,7 @@ open class App : Application(), Configuration.Provider {
|
||||
super.onCreate()
|
||||
if (BuildConfig.DEBUG) Logging.install(LogCatLogger())
|
||||
|
||||
ReLinker
|
||||
.log { message -> log(TAG) { "ReLinker: $message" } }
|
||||
.loadLibrary(this, "bugsnag-plugin-android-anr")
|
||||
|
||||
autoReporting.setup()
|
||||
autoReporting.setup(this)
|
||||
|
||||
setupWorker()
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import androidx.lifecycle.SavedStateHandle
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import eu.darken.capod.common.bluetooth.BluetoothManager2
|
||||
import eu.darken.capod.common.coroutine.DispatcherProvider
|
||||
import eu.darken.capod.common.debug.autoreport.DebugSettings
|
||||
import eu.darken.capod.common.debug.DebugSettings
|
||||
import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE
|
||||
import eu.darken.capod.common.debug.logging.log
|
||||
import eu.darken.capod.common.flow.combine
|
||||
|
||||
@@ -6,7 +6,6 @@ plugins {
|
||||
}
|
||||
apply(plugin = "dagger.hilt.android.plugin")
|
||||
apply(plugin = "androidx.navigation.safeargs.kotlin")
|
||||
apply(plugin = "com.bugsnag.android.gradle")
|
||||
|
||||
android {
|
||||
|
||||
@@ -22,10 +21,6 @@ android {
|
||||
versionName = ProjectConfig.Version.name
|
||||
|
||||
testInstrumentationRunner = "eu.darken.capod.HiltTestRunner"
|
||||
|
||||
manifestPlaceholders["bugsnagApiKey"] = getBugSnagApiKey(
|
||||
File(System.getProperty("user.home"), ".appconfig/${ProjectConfig.packageName}/bugsnag.properties")
|
||||
) ?: "fake"
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
@@ -47,9 +42,12 @@ android {
|
||||
create("gplay") {
|
||||
dimension = "version"
|
||||
signingConfig = signingConfigs["releaseGplay"]
|
||||
extra["useBugsnag"] = true
|
||||
}
|
||||
}
|
||||
|
||||
setupBugsnagPlugin()
|
||||
|
||||
buildTypes {
|
||||
val customProguardRules = fileTree(File(projectDir, "proguard")) {
|
||||
include("*.pro")
|
||||
@@ -90,7 +88,7 @@ android {
|
||||
if (listOf("release", "beta").any { variantName.toLowerCase().contains(it) }) {
|
||||
val outputFileName = ProjectConfig.packageName +
|
||||
"-v${defaultConfig.versionName}-${defaultConfig.versionCode}" +
|
||||
"-${variantName.toUpperCase()}-${lastCommitHash()}.apk"
|
||||
"-${variantName.toUpperCase()}.apk"
|
||||
|
||||
variantOutputImpl.outputFileName = outputFileName
|
||||
}
|
||||
@@ -148,8 +146,8 @@ dependencies {
|
||||
|
||||
addTesting()
|
||||
|
||||
implementation("com.bugsnag:bugsnag-android:5.9.2")
|
||||
implementation("com.getkeepsafe.relinker:relinker:1.4.3")
|
||||
|
||||
"gplayImplementation"("com.android.billingclient:billing:4.0.0")
|
||||
|
||||
"gplayImplementation"("com.bugsnag:bugsnag-android:5.9.2")
|
||||
"gplayImplementation"("com.getkeepsafe.relinker:relinker:1.4.3")
|
||||
}
|
||||
@@ -86,10 +86,6 @@
|
||||
android:name="androidx.startup.InitializationProvider"
|
||||
android:authorities="${applicationId}.androidx-startup"
|
||||
tools:node="remove" />
|
||||
|
||||
<meta-data
|
||||
android:name="com.bugsnag.android.API_KEY"
|
||||
android:value="${bugsnagApiKey}" />
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -3,10 +3,9 @@ package eu.darken.capod
|
||||
import android.app.Application
|
||||
import androidx.hilt.work.HiltWorkerFactory
|
||||
import androidx.work.Configuration
|
||||
import com.getkeepsafe.relinker.ReLinker
|
||||
import dagger.hilt.android.HiltAndroidApp
|
||||
import eu.darken.capod.common.coroutine.AppScope
|
||||
import eu.darken.capod.common.debug.autoreport.AutoReporting
|
||||
import eu.darken.capod.common.debug.autoreport.AutomaticBugReporter
|
||||
import eu.darken.capod.common.debug.logging.*
|
||||
import eu.darken.capod.common.flow.throttleLatest
|
||||
import eu.darken.capod.common.upgrade.UpgradeRepo
|
||||
@@ -25,7 +24,7 @@ import javax.inject.Inject
|
||||
open class App : Application(), Configuration.Provider {
|
||||
|
||||
@Inject lateinit var workerFactory: HiltWorkerFactory
|
||||
@Inject lateinit var autoReporting: AutoReporting
|
||||
@Inject lateinit var autoReporting: AutomaticBugReporter
|
||||
@Inject lateinit var monitorControl: MonitorControl
|
||||
@Inject lateinit var podMonitor: PodMonitor
|
||||
@Inject lateinit var widgetManager: WidgetManager
|
||||
@@ -36,11 +35,7 @@ open class App : Application(), Configuration.Provider {
|
||||
super.onCreate()
|
||||
if (BuildConfig.DEBUG) Logging.install(LogCatLogger())
|
||||
|
||||
ReLinker
|
||||
.log { message -> log(TAG) { "ReLinker: $message" } }
|
||||
.loadLibrary(this, "bugsnag-plugin-android-anr")
|
||||
|
||||
autoReporting.setup()
|
||||
autoReporting.setup(this)
|
||||
|
||||
log(TAG) { "onCreate() done! ${Exception().asLog()}" }
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import androidx.lifecycle.SavedStateHandle
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import eu.darken.capod.common.bluetooth.BluetoothManager2
|
||||
import eu.darken.capod.common.coroutine.DispatcherProvider
|
||||
import eu.darken.capod.common.debug.autoreport.DebugSettings
|
||||
import eu.darken.capod.common.debug.DebugSettings
|
||||
import eu.darken.capod.common.debug.logging.log
|
||||
import eu.darken.capod.common.flow.combine
|
||||
import eu.darken.capod.common.flow.throttleLatest
|
||||
|
||||
@@ -42,8 +42,8 @@ class SinglePodsCardVH(parent: ViewGroup) :
|
||||
// Charge state
|
||||
device.apply {
|
||||
if (this is HasChargeDetection) {
|
||||
chargingIcon.isInvisible = isHeadsetBeingCharged
|
||||
chargingLabel.isInvisible = isHeadsetBeingCharged
|
||||
chargingIcon.isInvisible = !isHeadsetBeingCharged
|
||||
chargingLabel.isInvisible = !isHeadsetBeingCharged
|
||||
} else {
|
||||
chargingIcon.isGone = true
|
||||
chargingLabel.isGone = true
|
||||
@@ -53,8 +53,8 @@ class SinglePodsCardVH(parent: ViewGroup) :
|
||||
// Has ear detection
|
||||
device.apply {
|
||||
if (this is HasEarDetection) {
|
||||
wearIcon.isInvisible = isBeingWorn
|
||||
wearLabel.isInvisible = isBeingWorn
|
||||
wearIcon.isInvisible = !isBeingWorn
|
||||
wearLabel.isInvisible = !isBeingWorn
|
||||
} else {
|
||||
wearIcon.isGone = true
|
||||
wearLabel.isGone = true
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@ import androidx.fragment.app.viewModels
|
||||
import androidx.preference.Preference
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import eu.darken.capod.R
|
||||
import eu.darken.capod.common.debug.autoreport.DebugSettings
|
||||
import eu.darken.capod.common.debug.DebugSettings
|
||||
import eu.darken.capod.common.observe2
|
||||
import eu.darken.capod.common.uix.PreferenceFragment3
|
||||
import javax.inject.Inject
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@ package eu.darken.capod.main.ui.settings.general.debug
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import eu.darken.capod.common.coroutine.DispatcherProvider
|
||||
import eu.darken.capod.common.debug.autoreport.DebugSettings
|
||||
import eu.darken.capod.common.debug.DebugSettings
|
||||
import eu.darken.capod.common.debug.logging.log
|
||||
import eu.darken.capod.common.debug.logging.logTag
|
||||
import eu.darken.capod.common.debug.recording.core.RecorderModule
|
||||
|
||||
@@ -37,15 +37,13 @@ class WidgetProvider : AppWidgetProvider() {
|
||||
@Inject lateinit var upgradeRepo: UpgradeRepo
|
||||
@AppScope @Inject lateinit var appScope: CoroutineScope
|
||||
|
||||
private var asyncBarrier: PendingResult? = null
|
||||
|
||||
private fun executeAsync(
|
||||
tag: String,
|
||||
timeout: Duration = Duration.ofSeconds(10),
|
||||
timeout: Duration = Duration.ofSeconds(8),
|
||||
block: suspend () -> Unit
|
||||
) {
|
||||
val start = System.currentTimeMillis()
|
||||
asyncBarrier = goAsync()
|
||||
val asyncBarrier = goAsync()
|
||||
log(TAG, VERBOSE) { "executeAsync($tag) starting asyncBarrier=$asyncBarrier " }
|
||||
|
||||
appScope.launch {
|
||||
@@ -54,7 +52,7 @@ class WidgetProvider : AppWidgetProvider() {
|
||||
} catch (e: Exception) {
|
||||
log(TAG, ERROR) { "executeAsync($tag) failed: ${e.asLog()}" }
|
||||
} finally {
|
||||
asyncBarrier?.finish()
|
||||
asyncBarrier.finish()
|
||||
val stop = System.currentTimeMillis()
|
||||
log(TAG, VERBOSE) { "executeAsync($tag) DONE (${stop - start}ms) " }
|
||||
}
|
||||
|
||||
@@ -6,8 +6,6 @@ import android.widget.RemoteViews
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import eu.darken.capod.R
|
||||
import eu.darken.capod.pods.core.*
|
||||
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||
import eu.darken.capod.pods.core.apple.SingleApplePods
|
||||
import javax.inject.Inject
|
||||
|
||||
|
||||
@@ -16,12 +14,12 @@ class MonitorNotificationViewFactory @Inject constructor(
|
||||
) {
|
||||
|
||||
fun createContentView(device: PodDevice): RemoteViews = when (device) {
|
||||
is DualApplePods -> createDualApplePods(device)
|
||||
is SingleApplePods -> createSingleApplePods(device)
|
||||
is DualPodDevice -> createDualPods(device)
|
||||
is SinglePodDevice -> createSinglePod(device)
|
||||
else -> createUnknownDevice(device)
|
||||
}
|
||||
|
||||
private fun createDualApplePods(device: DualApplePods): RemoteViews = RemoteViews(
|
||||
private fun createDualPods(device: DualPodDevice): RemoteViews = RemoteViews(
|
||||
context.packageName,
|
||||
R.layout.monitor_notification_dual_pods_small
|
||||
).apply {
|
||||
@@ -29,23 +27,30 @@ class MonitorNotificationViewFactory @Inject constructor(
|
||||
// Left
|
||||
setImageViewResource(R.id.pod_left_icon, device.leftPodIcon)
|
||||
setTextViewText(R.id.pod_left_label, getBatteryLevelLeftPod(context))
|
||||
val isLeftPodCharging = (device as? HasChargeDetectionDual)?.isLeftPodCharging ?: false
|
||||
setViewVisibility(R.id.pod_left_charging, if (isLeftPodCharging) View.VISIBLE else View.GONE)
|
||||
val isLeftPodInEar = (device as? HasEarDetectionDual)?.isLeftPodInEar ?: false
|
||||
setViewVisibility(R.id.pod_left_ear, if (isLeftPodInEar) View.VISIBLE else View.GONE)
|
||||
|
||||
// Case
|
||||
setImageViewResource(R.id.pod_case_icon, device.caseIcon)
|
||||
setTextViewText(R.id.pod_case_label, getBatteryLevelCase(context))
|
||||
setViewVisibility(R.id.pod_case_charging, if (isCaseCharging) View.VISIBLE else View.GONE)
|
||||
setViewVisibility(R.id.pod_case_charging, if (device is HasCase) View.VISIBLE else View.GONE)
|
||||
(device as? HasCase)?.let { case ->
|
||||
setImageViewResource(R.id.pod_case_icon, device.caseIcon)
|
||||
setTextViewText(R.id.pod_case_label, case.getBatteryLevelCase(context))
|
||||
setViewVisibility(R.id.pod_case_charging, if (case.isCaseCharging) View.VISIBLE else View.GONE)
|
||||
}
|
||||
|
||||
// Right
|
||||
setImageViewResource(R.id.pod_right_icon, device.rightPodIcon)
|
||||
setTextViewText(R.id.pod_right_label, getBatteryLevelRightPod(context))
|
||||
val isRightPodCharging = (device as? HasChargeDetectionDual)?.isRightPodCharging ?: false
|
||||
setViewVisibility(R.id.pod_right_charging, if (isRightPodCharging) View.VISIBLE else View.GONE)
|
||||
val isRightPodInEar = (device as? HasEarDetectionDual)?.isRightPodInEar ?: false
|
||||
setViewVisibility(R.id.pod_right_ear, if (isRightPodInEar) View.VISIBLE else View.GONE)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createSingleApplePods(device: SingleApplePods): RemoteViews = RemoteViews(
|
||||
private fun createSinglePod(device: SinglePodDevice): RemoteViews = RemoteViews(
|
||||
context.packageName,
|
||||
R.layout.monitor_notification_single_pods_small
|
||||
).apply {
|
||||
|
||||
@@ -6,14 +6,13 @@ import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.view.ContextThemeWrapper
|
||||
import androidx.core.view.isInvisible
|
||||
import androidx.core.view.isVisible
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import eu.darken.capod.R
|
||||
import eu.darken.capod.common.debug.autoreport.DebugSettings
|
||||
import eu.darken.capod.common.debug.DebugSettings
|
||||
import eu.darken.capod.databinding.PopupNotificationDualPodsBinding
|
||||
import eu.darken.capod.databinding.PopupNotificationSinglePodsBinding
|
||||
import eu.darken.capod.pods.core.*
|
||||
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||
import eu.darken.capod.pods.core.apple.SingleApplePods
|
||||
import javax.inject.Inject
|
||||
|
||||
|
||||
@@ -26,13 +25,13 @@ class PopUpPodViewFactory @Inject constructor(
|
||||
private val layoutInflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
|
||||
|
||||
fun createContentView(parent: ViewGroup, device: PodDevice): View = when (device) {
|
||||
is DualApplePods -> createDualApplePods(parent, device)
|
||||
is DualPodDevice -> createDualPods(parent, device)
|
||||
// Unused, has no case to trigger reaction?
|
||||
is SingleApplePods -> createSingleApplePods(parent, device)
|
||||
is SinglePodDevice -> createSinglePod(parent, device)
|
||||
else -> throw IllegalArgumentException("Unexpected device: $device")
|
||||
}
|
||||
|
||||
private fun createDualApplePods(parent: ViewGroup, device: DualApplePods): View =
|
||||
private fun createDualPods(parent: ViewGroup, device: DualPodDevice): View =
|
||||
PopupNotificationDualPodsBinding.inflate(layoutInflater, parent, false).apply {
|
||||
device.apply {
|
||||
podIcon.setImageResource(iconRes)
|
||||
@@ -46,9 +45,12 @@ class PopUpPodViewFactory @Inject constructor(
|
||||
podLeftBatteryLabel.text = getBatteryLevelLeftPod(context)
|
||||
|
||||
// Case
|
||||
podCaseIcon.setImageResource(device.caseIcon)
|
||||
podCaseBatteryIcon.setImageResource(getBatteryDrawable(batteryCasePercent))
|
||||
podCaseBatteryLabel.text = getBatteryLevelCase(context)
|
||||
podCaseContainer.isVisible = device is HasCase
|
||||
(device as? HasCase)?.let { case ->
|
||||
podCaseIcon.setImageResource(case.caseIcon)
|
||||
podCaseBatteryIcon.setImageResource(getBatteryDrawable(case.batteryCasePercent))
|
||||
podCaseBatteryLabel.text = case.getBatteryLevelCase(context)
|
||||
}
|
||||
|
||||
// Right
|
||||
podRightIcon.setImageResource(device.rightPodIcon)
|
||||
@@ -57,7 +59,7 @@ class PopUpPodViewFactory @Inject constructor(
|
||||
}
|
||||
}.root
|
||||
|
||||
private fun createSingleApplePods(parent: ViewGroup, device: SingleApplePods): View =
|
||||
private fun createSinglePod(parent: ViewGroup, device: SinglePodDevice): View =
|
||||
PopupNotificationSinglePodsBinding.inflate(layoutInflater, parent, false).apply {
|
||||
device.apply {
|
||||
headphonesIcon.setImageResource(iconRes)
|
||||
|
||||
@@ -57,6 +57,7 @@
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:id="@+id/pod_case_container"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ImageView
|
||||
|
||||
@@ -7,9 +7,7 @@
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/pod_icon"
|
||||
style="@style/PodInfoItemIcon"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
style="@style/PodInfoItemIcon.Popup"
|
||||
android:src="@drawable/devic_earbuds_generic_both"
|
||||
app:layout_constraintBottom_toBottomOf="@id/pod_label"
|
||||
app:layout_constraintEnd_toStartOf="@id/pod_label"
|
||||
@@ -55,9 +53,7 @@
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/pod_left_icon"
|
||||
style="@style/PodInfoItemIcon"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
style="@style/PodInfoItemIcon.Popup"
|
||||
android:layout_gravity="center"
|
||||
android:src="@drawable/devic_airpods_gen1_left"
|
||||
app:layout_constraintBottom_toBottomOf="@id/pod_left_battery_label"
|
||||
@@ -67,9 +63,7 @@
|
||||
|
||||
<ImageView
|
||||
android:id="@id/pod_left_battery_icon"
|
||||
style="@style/PodInfoItemIcon"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
style="@style/PodInfoItemIcon.Popup"
|
||||
android:src="@drawable/ic_baseline_battery_unknown_24"
|
||||
app:layout_constraintBottom_toBottomOf="@id/pod_left_battery_label"
|
||||
app:layout_constraintEnd_toStartOf="@id/pod_left_battery_label"
|
||||
@@ -103,9 +97,7 @@
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/pod_case_icon"
|
||||
style="@style/PodInfoItemIcon"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
style="@style/PodInfoItemIcon.Popup"
|
||||
android:layout_gravity="center"
|
||||
android:src="@drawable/devic_airpods_gen1_case"
|
||||
app:layout_constraintBottom_toBottomOf="@id/pod_case_battery_label"
|
||||
@@ -115,9 +107,7 @@
|
||||
|
||||
<ImageView
|
||||
android:id="@id/pod_case_battery_icon"
|
||||
style="@style/PodInfoItemIcon"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
style="@style/PodInfoItemIcon.Popup"
|
||||
android:src="@drawable/ic_baseline_battery_unknown_24"
|
||||
app:layout_constraintBottom_toBottomOf="@id/pod_case_battery_label"
|
||||
app:layout_constraintEnd_toStartOf="@id/pod_case_battery_label"
|
||||
@@ -150,9 +140,7 @@
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/pod_right_icon"
|
||||
style="@style/PodInfoItemIcon"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
style="@style/PodInfoItemIcon.Popup"
|
||||
android:layout_gravity="center"
|
||||
android:src="@drawable/devic_airpods_gen1_right"
|
||||
app:layout_constraintBottom_toBottomOf="@id/pod_right_battery_label"
|
||||
@@ -162,9 +150,7 @@
|
||||
|
||||
<ImageView
|
||||
android:id="@id/pod_right_battery_icon"
|
||||
style="@style/PodInfoItemIcon"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
style="@style/PodInfoItemIcon.Popup"
|
||||
android:src="@drawable/ic_baseline_battery_unknown_24"
|
||||
app:layout_constraintBottom_toBottomOf="@id/pod_right_battery_label"
|
||||
app:layout_constraintEnd_toStartOf="@id/pod_right_battery_label"
|
||||
|
||||
@@ -7,9 +7,7 @@
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/headphones_icon"
|
||||
android:layout_width="wrap_content"
|
||||
style="@style/PodInfoItemIcon"
|
||||
android:layout_height="wrap_content"
|
||||
style="@style/PodInfoItemIcon.Popup"
|
||||
android:src="@drawable/devic_earbuds_generic_both"
|
||||
app:layout_constraintBottom_toBottomOf="@id/headphones_label"
|
||||
app:layout_constraintEnd_toStartOf="@id/headphones_label"
|
||||
@@ -56,9 +54,7 @@
|
||||
|
||||
<ImageView
|
||||
android:id="@id/headphones_battery_icon"
|
||||
android:layout_width="wrap_content"
|
||||
style="@style/PodInfoItemIcon"
|
||||
android:layout_height="wrap_content"
|
||||
style="@style/PodInfoItemIcon.Popup"
|
||||
android:src="@drawable/ic_baseline_battery_unknown_24"
|
||||
app:layout_constraintBottom_toBottomOf="@id/headphones_battery_label"
|
||||
app:layout_constraintEnd_toStartOf="@id/headphones_battery_label"
|
||||
|
||||
@@ -38,6 +38,11 @@
|
||||
<item name="tint">?android:attr/colorControlNormal</item>
|
||||
</style>
|
||||
|
||||
<style name="PodInfoItemIcon.Popup">
|
||||
<item name="android:layout_height">24dp</item>
|
||||
<item name="android:layout_width">24dp</item>
|
||||
</style>
|
||||
|
||||
<style name="PodInfoItemText" parent="TextAppearance.MaterialComponents.Body2">
|
||||
<item name="android:singleLine">true</item>
|
||||
<item name="android:ellipsize">end</item>
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import com.android.build.gradle.LibraryExtension
|
||||
import org.gradle.api.Action
|
||||
import org.gradle.api.JavaVersion
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.kotlin.dsl.apply
|
||||
import org.gradle.kotlin.dsl.extra
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.time.Instant
|
||||
@@ -122,4 +125,30 @@ fun getBugSnagApiKey(
|
||||
}
|
||||
|
||||
return System.getenv("BUGSNAG_API_KEY") ?: bugsnagProps.getProperty("bugsnag.apikey")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Use extra["useBugsnag"] = true per flavor to enable
|
||||
*/
|
||||
fun <T> T.setupBugsnagPlugin() where T : Project {
|
||||
fun Project.`android`(configure: Action<com.android.build.gradle.internal.dsl.BaseAppModuleExtension>): Unit =
|
||||
(this as org.gradle.api.plugins.ExtensionAware).extensions.configure("android", configure)
|
||||
|
||||
val key = "useBugsnag"
|
||||
|
||||
android {
|
||||
productFlavors {
|
||||
forEach {
|
||||
val paramString = gradle.startParameter.taskRequests.toString().toLowerCase(Locale.ROOT)
|
||||
val useBugsnag = if (it.extra.has(key)) it.extra[key] as Boolean else false
|
||||
|
||||
if (paramString.contains(it.name) && useBugsnag) {
|
||||
println("Bugsnag plugin (com.bugsnag.android.gradle) applied to ${it.name} ($paramString)")
|
||||
apply(plugin = "com.bugsnag.android.gradle")
|
||||
} else {
|
||||
println("Bugsnag plugin (com.bugsnag.android.gradle) NOT applied to ${it.name} ($paramString)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
Bugfixes and performance improvements.
|
||||
¯\_(ツ)_/¯
|
||||
@@ -1,3 +1,2 @@
|
||||
Now with more widgets!
|
||||
But also bugfixes and performance improvements.
|
||||
Bugfixes and performance improvements.
|
||||
¯\_(ツ)_/¯
|
||||
+20
@@ -360,6 +360,25 @@ do-versionfile() {
|
||||
git add VERSION
|
||||
}
|
||||
|
||||
# Update a version file that can be parsed by third-parties, e.g. F-Droid
|
||||
do-fastlane-changelog() {
|
||||
CHANGELOGS_DIR="fastlane/metadata/android/en-US/changelogs"
|
||||
NEW_CHANGELOG="$CHANGELOGS_DIR/$V_CODE.txt"
|
||||
|
||||
[ -f "$NEW_CHANGELOG" ] && return
|
||||
|
||||
echo -e "\n${S_NOTICE}Creating new default fastlane changelog file for $V_CODE...\n"
|
||||
|
||||
cp "$CHANGELOGS_DIR/default.txt" "$NEW_CHANGELOG"
|
||||
|
||||
cat "$NEW_CHANGELOG"
|
||||
|
||||
echo -e "\n\n${I_OK} ${S_NOTICE}${ACTION_MSG} [${S_NORM}$NEW_CHANGELOG${S_NOTICE}] file"
|
||||
|
||||
# Stage file for commit
|
||||
git add "$CHANGELOGS_DIR/$V_CODE.txt"
|
||||
}
|
||||
|
||||
# Does the release branch already exist?
|
||||
check-branch-exist() {
|
||||
[ "$FLAG_NOBRANCH" = true ] && return
|
||||
@@ -448,6 +467,7 @@ echo -e "\n${S_LIGHT}––––––"
|
||||
# Update steps
|
||||
do-version-properties
|
||||
do-versionfile
|
||||
do-fastlane-changelog
|
||||
do-branch
|
||||
do-commit
|
||||
create-tag "${V_NAME}" "${REL_NOTE}"
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
### Updated by release.sh ###
|
||||
project.versioning.major=2
|
||||
project.versioning.minor=7
|
||||
project.versioning.minor=8
|
||||
project.versioning.patch=1
|
||||
project.versioning.build=1
|
||||
project.versioning.build=0
|
||||
#############################
|
||||
|
||||
Reference in New Issue
Block a user