Compare commits

...
16 Commits
Author SHA1 Message Date
darken 18dcf603e9 Release: 2.8.0-rc0 2023-02-03 22:32:01 +01:00
Matthias Urhahn 71aaba1b86 Fix data display for non dual apple pods (#98)
* Fix data display for fake pods and beats devices (with two pods)

* Rename methods
2023-02-03 22:31:23 +01:00
Matthias Urhahn 64b9d5f17a Remove ~ and % from signal quality value, it's not necessarily a 0 to 100 percent indicator. (#96) 2023-02-02 16:47:06 +01:00
Matthias Urhahn a9f23c4bfe Change defaults setting for "Show all devices" (#95)
* Fix potential widget issue where a broadcast race condition causes the goAsync() to fail.

* Enable "show all devices" by default to help new users.
2023-02-02 16:46:52 +01:00
Matthias Urhahn f454f46778 Fix potential widget issue where a broadcast race condition causes the goAsync() to fail. (#94) 2023-02-02 16:46:36 +01:00
Matthias Urhahn 432936ea2c Merge pull request #93 from d4rken-org/crowd_reliability
Improve tracking in crowds
2023-02-02 16:46:08 +01:00
Matthias Urhahn 8211b0ac88 Track case battery state outside of history list and simplify history clean up. (#92) 2023-02-02 16:45:39 +01:00
Matthias Urhahn c314987e13 Fix popup icon sizes (#91) 2023-02-02 16:45:22 +01:00
Matthias Urhahn fc6cd74752 If the current pods don't have a lid state value, and there is none in recent history, we can assume NOT_IN_CASE. (#90) 2023-02-02 16:45:05 +01:00
Matthias Urhahn a47011e579 Merge branch 'main' into crowd_reliability 2023-02-02 14:57:04 +01:00
darken fbeeec6539 Boost signal quality rating by up to 25% depending on how long they have been tracked. 2023-02-02 14:46:29 +01:00
darken e1cd79cd9d Cap reliability at 100% 2023-02-02 14:45:44 +01:00
darken 692ced1ee7 Timeout after 30 not 20 seconds. 2023-02-02 14:45:22 +01:00
darken c5cc127df4 Smooth rssi values and use reliability (missed signals) as boost, not as factor. 2023-02-02 11:50:33 +01:00
Matthias Urhahn 5f00f10ae8 Remove short hash from generated apk file names (#89) 2023-02-02 08:38:22 +01:00
Matthias Urhahn 0fc9716f06 Only apply Bugsnag to gplay build flavor (#88)
* wip

* Setup bugsnag only in gplay builds.

* Bugsnag does not support uploading mapping files or build information from library modules. This should be done from the application module which produces your APK instead.

* Only apply Bugsnag plugin for the right flavors.

* Run unit tests in debug, otherwise we run issues requiring api keys in the env
2023-02-01 11:17:50 +01:00
61 changed files with 367 additions and 249 deletions
+1 -1
View File
@@ -1 +1 @@
2.7.2-rc0 20702000 2.8.0-rc0 20800000
+6 -1
View File
@@ -21,6 +21,10 @@ android {
buildConfigField("String", "APPLICATION_ID", "\"${ProjectConfig.packageName}\"") buildConfigField("String", "APPLICATION_ID", "\"${ProjectConfig.packageName}\"")
buildConfigField("String", "GITSHA", "\"${lastCommitHash()}\"") buildConfigField("String", "GITSHA", "\"${lastCommitHash()}\"")
buildConfigField("String", "BUILDTIME", "\"${buildTime()}\"") buildConfigField("String", "BUILDTIME", "\"${buildTime()}\"")
manifestPlaceholders["bugsnagApiKey"] = getBugSnagApiKey(
File(System.getProperty("user.home"), ".appconfig/${ProjectConfig.packageName}/bugsnag.properties")
) ?: "bugsnag_apikey_placeholder"
} }
buildFeatures { buildFeatures {
@@ -95,6 +99,7 @@ dependencies {
addTesting() 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")
} }
+4
View File
@@ -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!")
}
}
+10
View File
@@ -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
}
@@ -1,4 +1,4 @@
package eu.darken.capod.common.debug.autoreport.bugsnag package eu.darken.capod.debug.autoreport
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context import android.content.Context
@@ -7,7 +7,7 @@ import com.bugsnag.android.Event
import com.bugsnag.android.OnErrorCallback import com.bugsnag.android.OnErrorCallback
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import eu.darken.capod.common.BuildConfigWrap 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.Logging.Priority.WARN
import eu.darken.capod.common.debug.logging.asLog import eu.darken.capod.common.debug.logging.asLog
import eu.darken.capod.common.debug.logging.log import eu.darken.capod.common.debug.logging.log
@@ -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.Event
import eu.darken.capod.common.debug.logging.Logging import eu.darken.capod.common.debug.logging.Logging
@@ -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 android.content.Context
import com.bugsnag.android.Bugsnag import com.bugsnag.android.Bugsnag
import com.bugsnag.android.Configuration import com.bugsnag.android.Configuration
import com.getkeepsafe.relinker.ReLinker
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import eu.darken.capod.common.BuildConfigWrap import eu.darken.capod.common.BuildConfigWrap
import eu.darken.capod.common.InstallId import eu.darken.capod.common.InstallId
import eu.darken.capod.common.debug.Bugs import eu.darken.capod.common.debug.Bugs
import eu.darken.capod.common.debug.autoreport.bugsnag.BugsnagErrorHandler import eu.darken.capod.common.debug.DebugSettings
import eu.darken.capod.common.debug.autoreport.bugsnag.BugsnagLogger import eu.darken.capod.common.debug.autoreport.AutomaticBugReporter
import eu.darken.capod.common.debug.autoreport.bugsnag.NOPBugsnagErrorHandler
import eu.darken.capod.common.debug.logging.Logging import eu.darken.capod.common.debug.logging.Logging
import eu.darken.capod.common.debug.logging.log import eu.darken.capod.common.debug.logging.log
import eu.darken.capod.common.debug.logging.logTag import eu.darken.capod.common.debug.logging.logTag
@@ -18,19 +19,25 @@ import javax.inject.Provider
import javax.inject.Singleton import javax.inject.Singleton
@Singleton @Singleton
class AutoReporting @Inject constructor( class GplayAutoReporting @Inject constructor(
@ApplicationContext private val context: Context, @ApplicationContext private val context: Context,
private val debugSettings: DebugSettings, private val debugSettings: DebugSettings,
private val installId: InstallId, private val installId: InstallId,
private val bugsnagLogger: Provider<BugsnagLogger>, private val bugsnagLogger: Provider<BugsnagLogger>,
private val bugsnagErrorHandler: Provider<BugsnagErrorHandler>, private val bugsnagErrorHandler: Provider<BugsnagErrorHandler>,
private val nopBugsnagErrorHandler: Provider<NOPBugsnagErrorHandler>, private val nopBugsnagErrorHandler: Provider<NOPBugsnagErrorHandler>,
) { ) : AutomaticBugReporter {
fun setup() { override fun setup(application: Application) {
val isEnabled = debugSettings.isAutoReportingEnabled.value val isEnabled = debugSettings.isAutoReportingEnabled.value
log(TAG) { "setup(): isEnabled=$isEnabled" } log(TAG) { "setup(): isEnabled=$isEnabled" }
if (!isEnabled) return
ReLinker
.log { message -> log(TAG) { "ReLinker: $message" } }
.loadLibrary(application, "bugsnag-plugin-android-anr")
try { try {
val bugsnagConfig = Configuration.load(context).apply { val bugsnagConfig = Configuration.load(context).apply {
if (debugSettings.isAutoReportingEnabled.value) { if (debugSettings.isAutoReportingEnabled.value) {
@@ -48,13 +55,18 @@ class AutoReporting @Inject constructor(
} }
Bugsnag.start(context, bugsnagConfig) Bugsnag.start(context, bugsnagConfig)
Bugs.ready = true Bugs.reporter = this
} catch (e: IllegalStateException) { } catch (e: IllegalStateException) {
log(TAG) { "Bugsnag API Key not configured." } log(TAG) { "Bugsnag API Key not configured." }
} }
} }
override fun notify(throwable: Throwable) {
Bugsnag.notify(throwable)
}
companion object { companion object {
private val TAG = logTag("Debug", "AutoReport") private val TAG = logTag("Debug", "AutoReport")
} }
} }
@@ -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.Event
import com.bugsnag.android.OnErrorCallback import com.bugsnag.android.OnErrorCallback
@@ -2,7 +2,7 @@ package eu.darken.capod.common.bluetooth
import dagger.Reusable import dagger.Reusable
import eu.darken.capod.common.SystemClockWrap 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 java.time.Instant
import javax.inject.Inject import javax.inject.Inject
import kotlin.random.Random import kotlin.random.Random
@@ -27,9 +27,7 @@ class FakeBleData @Inject constructor(
generatedAtNanos = SystemClockWrap.elapsedRealtimeNanos + 100, 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()) 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 { ).run {
if (Random.nextBoolean()) { fakeDevices.add(this)
fakeDevices.add(this)
}
} }
// AirPods Gen2 // AirPods Gen2
@@ -40,9 +38,7 @@ class FakeBleData @Inject constructor(
generatedAtNanos = SystemClockWrap.elapsedRealtimeNanos + 100, 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()) 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 { ).run {
if (Random.nextBoolean()) { fakeDevices.add(this)
fakeDevices.add(this)
}
} }
// AirPods Gen3 // AirPods Gen3
@@ -53,9 +49,7 @@ class FakeBleData @Inject constructor(
generatedAtNanos = SystemClockWrap.elapsedRealtimeNanos + 200, 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()) 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 { ).run {
if (Random.nextBoolean()) { fakeDevices.add(this)
fakeDevices.add(this)
}
} }
// AirPods Max // AirPods Max
BleScanResult( BleScanResult(
@@ -65,9 +59,7 @@ class FakeBleData @Inject constructor(
generatedAtNanos = SystemClockWrap.elapsedRealtimeNanos + 300, 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()) 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 { ).run {
if (Random.nextBoolean()) { fakeDevices.add(this)
fakeDevices.add(this)
}
} }
// BeatsFlex // BeatsFlex
BleScanResult( BleScanResult(
@@ -77,9 +69,7 @@ class FakeBleData @Inject constructor(
generatedAtNanos = SystemClockWrap.elapsedRealtimeNanos + 400, 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()) 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 { ).run {
if (Random.nextBoolean()) { fakeDevices.add(this)
fakeDevices.add(this)
}
} }
// Tws i99999 // Tws i99999
@@ -90,9 +80,7 @@ class FakeBleData @Inject constructor(
generatedAtNanos = SystemClockWrap.elapsedRealtimeNanos + 400, 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()) 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 { ).run {
if (Random.nextBoolean()) { fakeDevices.add(this)
fakeDevices.add(this)
}
} }
// Unknown Device // Unknown Device
@@ -103,9 +91,7 @@ class FakeBleData @Inject constructor(
generatedAtNanos = SystemClockWrap.elapsedRealtimeNanos + 500, 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()) 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 { ).run {
if (Random.nextBoolean()) { fakeDevices.add(this)
fakeDevices.add(this)
}
} }
return fakeDevices 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 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.Logging.Priority.*
import eu.darken.capod.common.debug.logging.asLog import eu.darken.capod.common.debug.logging.asLog
import eu.darken.capod.common.debug.logging.log import eu.darken.capod.common.debug.logging.log
import eu.darken.capod.common.debug.logging.logTag import eu.darken.capod.common.debug.logging.logTag
object Bugs { object Bugs {
var ready = false var reporter: AutomaticBugReporter? = null
fun report( fun report(
tag: String, tag: String,
message: String, message: String,
@@ -16,11 +16,9 @@ object Bugs {
log(TAG, VERBOSE) { "Reporting $exception" } log(TAG, VERBOSE) { "Reporting $exception" }
log(tag, ERROR) { "$message\n${exception.asLog()}" } log(tag, ERROR) { "$message\n${exception.asLog()}" }
if (!ready) { reporter?.notify(exception) ?: run {
log(TAG, WARN) { "Bug tracking not initialized yet." } log(TAG, WARN) { "Bug tracking not initialized yet." }
return
} }
Bugsnag.notify(exception)
} }
private val TAG = logTag("Bugs") private val TAG = logTag("Bugs")
@@ -1,4 +1,4 @@
package eu.darken.capod.common.debug.autoreport package eu.darken.capod.common.debug
import android.content.Context import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
@@ -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 com.squareup.moshi.Moshi
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import eu.darken.capod.common.bluetooth.ScannerMode 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.PreferenceStoreMapper
import eu.darken.capod.common.preferences.Settings import eu.darken.capod.common.preferences.Settings
import eu.darken.capod.common.preferences.createFlowPreference 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 monitorMode = preferences.createFlowPreference("core.monitor.mode", MonitorMode.AUTOMATIC, moshi)
val scannerMode = preferences.createFlowPreference("core.scanner.mode", ScannerMode.BALANCED, 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) 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.BluetoothManager2
import eu.darken.capod.common.bluetooth.ScannerMode import eu.darken.capod.common.bluetooth.ScannerMode
import eu.darken.capod.common.coroutine.AppScope 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.VERBOSE
import eu.darken.capod.common.debug.logging.Logging.Priority.WARN 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.asLog
@@ -6,6 +6,9 @@ import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import eu.darken.capod.common.R import eu.darken.capod.common.R
import eu.darken.capod.common.bluetooth.BleScanResult 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.time.Instant
import java.util.* import java.util.*
import kotlin.math.abs import kotlin.math.abs
@@ -31,14 +34,20 @@ interface PodDevice {
val rssi: Int val rssi: Int
get() = scanResult.rssi 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 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> val rawData: Map<Int, ByteArray>
get() = scanResult.manufacturerSpecificData get() = scanResult.manufacturerSpecificData
@@ -133,6 +142,6 @@ interface PodDevice {
} }
companion object { 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) ?: context.getString(R.string.general_value_not_available_label)
fun PodDevice.getSignalQuality(context: Context): String { fun PodDevice.getSignalQuality(context: Context): String {
val percentage = 100 * signalQuality val multiplier = 100 * signalQuality
return "~${percentage.roundToInt()}%" return "${multiplier.roundToInt()}"
} }
@DrawableRes @DrawableRes
@@ -1,6 +1,7 @@
package eu.darken.capod.pods.core.apple package eu.darken.capod.pods.core.apple
import eu.darken.capod.common.bluetooth.BleScanResult 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.VERBOSE
import eu.darken.capod.common.debug.logging.Logging.Priority.WARN import eu.darken.capod.common.debug.logging.Logging.Priority.WARN
import eu.darken.capod.common.debug.logging.log 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.common.upperNibble
import eu.darken.capod.pods.core.HasCase import eu.darken.capod.pods.core.HasCase
import eu.darken.capod.pods.core.PodDevice 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 eu.darken.capod.pods.core.apple.protocol.ProximityPairing
import java.time.Duration import java.time.Duration
import java.time.Instant import java.time.Instant
import kotlin.math.max
abstract class ApplePodsFactory<PodType : ApplePods>(private val tag: String) { 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 id: PodDevice.Id,
val seenFirstAt: Instant, val seenFirstAt: Instant,
val seenCounter: Int, val seenCounter: Int,
val history: List<ApplePods> val history: List<ApplePods>,
val lastCaseBattery: Float?,
) { ) {
val lastMessage: ProximityPairing.Message val lastMessage: ProximityPairing.Message
get() = history.last().proximityMessage get() = history.last().proximityMessage
@@ -46,63 +48,81 @@ abstract class ApplePodsFactory<PodType : ApplePods>(private val tag: String) {
val lastAddress: String val lastAddress: String
get() = history.last().address get() = history.last().address
val confidence: Float val reliability: Float
get() = 0.75f + (history.size / (MAX_HISTORY * 4f)) get() {
if (history.size < 2) return 0f
fun averageRssi(latest: Int): Int = val now = Instant.now()
history.map { it.rssi }.plus(latest).takeLast(10).median()
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 { fun isOlderThan(age: Duration): Boolean {
val now = Instant.now() val now = Instant.now()
return Duration.between(history.last().seenLastAt, now) > age 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()})" override fun toString(): String = "KnownDevice(history=${history.size}, last=${history.last()})"
companion object { 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>() 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>() .filterIsInstance<HasCase>()
.mapNotNull { it.batteryCasePercent } .mapNotNull { it.batteryCasePercent }
.lastOrNull() .lastOrNull()
fun KnownDevice.getLatestCaseLidState(basic: DualApplePods): DualApplePods.LidState? { fun KnownDevice.getLatestCaseLidState(basic: DualApplePods): DualApplePods.LidState? {
val definitive = setOf(DualApplePods.LidState.OPEN, DualApplePods.LidState.CLOSED) val definitive = setOf(
if (definitive.contains(basic.caseLidState)) return null DualApplePods.LidState.OPEN,
DualApplePods.LidState.CLOSED,
DualApplePods.LidState.NOT_IN_CASE,
)
if (definitive.contains(basic.caseLidState)) return basic.caseLidState
return history return history
.filterIsInstance<AirPodsPro>() // TODO why is this AirPodsPro specific here? .takeLast(2)
.lastOrNull { it.caseLidState != DualApplePods.LidState.NOT_IN_CASE } .filterIsInstance<DualApplePods>()
.lastOrNull { it.caseLidState != DualApplePods.LidState.UNKNOWN }
?.caseLidState ?.caseLidState
?: DualApplePods.LidState.NOT_IN_CASE
} }
open fun historyTrimmer( open fun historyTrimmer(
pods: List<ApplePods> pods: List<ApplePods>
): List<ApplePods> { ): List<ApplePods> {
var trimmed = pods.takeLast(KnownDevice.MAX_HISTORY) return 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
} }
internal open fun searchHistory(current: PodType): KnownDevice? { internal open fun searchHistory(current: PodType): KnownDevice? {
@@ -110,7 +130,7 @@ abstract class ApplePodsFactory<PodType : ApplePods>(private val tag: String) {
val message = current.proximityMessage val message = current.proximityMessage
knownDevices.values.toList().forEach { knownDevice -> 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" } log(tag, VERBOSE) { "searchHistory1: Removing stale known device: $knownDevice" }
knownDevices.remove(knownDevice.id) knownDevices.remove(knownDevice.id)
} }
@@ -146,18 +166,22 @@ abstract class ApplePodsFactory<PodType : ApplePods>(private val tag: String) {
knownDevices[device.identifier] = when { knownDevices[device.identifier] = when {
existing != null -> { existing != null -> {
val history = existing.history.plus(device)
existing.copy( existing.copy(
seenCounter = existing.seenCounter + 1, seenCounter = existing.seenCounter + 1,
history = existing.history.plus(device) history = history,
lastCaseBattery = history.determineLatestCaseBattery() ?: existing.lastCaseBattery
) )
} }
else -> { else -> {
log(tag) { "searchHistory1: Creating new history for $device" } log(tag) { "searchHistory1: Creating new history for $device" }
val history = listOf(device)
KnownDevice( KnownDevice(
id = device.identifier, id = device.identifier,
seenFirstAt = device.seenFirstAt, seenFirstAt = device.seenFirstAt,
seenCounter = 1, 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 seenCounter: Int = 1,
override val scanResult: BleScanResult, override val scanResult: BleScanResult,
override val proximityMessage: ProximityPairing.Message, 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 rssiAverage: Int? = null,
private val cachedBatteryPercentage: Float? = null, private val cachedBatteryPercentage: Float? = null,
private val cachedCaseState: DualApplePods.LidState? = null private val cachedCaseState: DualApplePods.LidState? = null
@@ -55,9 +55,9 @@ data class AirPodsGen1 constructor(
seenFirstAt = result.seenFirstAt, seenFirstAt = result.seenFirstAt,
seenLastAt = scanResult.receivedAt, seenLastAt = scanResult.receivedAt,
seenCounter = result.seenCounter, seenCounter = result.seenCounter,
confidence = result.confidence, reliability = result.reliability,
cachedBatteryPercentage = result.getLatestCaseBattery(), cachedBatteryPercentage = result.getLatestCaseBattery(),
rssiAverage = result.averageRssi(basic.rssi), rssiAverage = result.rssiSmoothed(basic.rssi),
cachedCaseState = result.getLatestCaseLidState(basic) cachedCaseState = result.getLatestCaseLidState(basic)
) )
} }
@@ -17,7 +17,7 @@ data class AirPodsGen2 constructor(
override val seenCounter: Int = 1, override val seenCounter: Int = 1,
override val scanResult: BleScanResult, override val scanResult: BleScanResult,
override val proximityMessage: ProximityPairing.Message, 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 rssiAverage: Int? = null,
private val cachedBatteryPercentage: Float? = null, private val cachedBatteryPercentage: Float? = null,
private val cachedCaseState: DualApplePods.LidState? = null private val cachedCaseState: DualApplePods.LidState? = null
@@ -54,9 +54,9 @@ data class AirPodsGen2 constructor(
seenFirstAt = result.seenFirstAt, seenFirstAt = result.seenFirstAt,
seenLastAt = scanResult.receivedAt, seenLastAt = scanResult.receivedAt,
seenCounter = result.seenCounter, seenCounter = result.seenCounter,
confidence = result.confidence, reliability = result.reliability,
cachedBatteryPercentage = result.getLatestCaseBattery(), cachedBatteryPercentage = result.getLatestCaseBattery(),
rssiAverage = result.averageRssi(basic.rssi), rssiAverage = result.rssiSmoothed(basic.rssi),
cachedCaseState = result.getLatestCaseLidState(basic) cachedCaseState = result.getLatestCaseLidState(basic)
) )
} }
@@ -17,7 +17,7 @@ data class AirPodsGen3 constructor(
override val seenCounter: Int = 1, override val seenCounter: Int = 1,
override val scanResult: BleScanResult, override val scanResult: BleScanResult,
override val proximityMessage: ProximityPairing.Message, 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 rssiAverage: Int? = null,
private val cachedBatteryPercentage: Float? = null, private val cachedBatteryPercentage: Float? = null,
private val cachedCaseState: DualApplePods.LidState? = null private val cachedCaseState: DualApplePods.LidState? = null
@@ -54,9 +54,9 @@ data class AirPodsGen3 constructor(
seenFirstAt = result.seenFirstAt, seenFirstAt = result.seenFirstAt,
seenLastAt = scanResult.receivedAt, seenLastAt = scanResult.receivedAt,
seenCounter = result.seenCounter, seenCounter = result.seenCounter,
confidence = result.confidence, reliability = result.reliability,
cachedBatteryPercentage = result.getLatestCaseBattery(), cachedBatteryPercentage = result.getLatestCaseBattery(),
rssiAverage = result.averageRssi(basic.rssi), rssiAverage = result.rssiSmoothed(basic.rssi),
cachedCaseState = result.getLatestCaseLidState(basic) cachedCaseState = result.getLatestCaseLidState(basic)
) )
} }
@@ -20,7 +20,7 @@ data class AirPodsMax(
override val seenCounter: Int = 1, override val seenCounter: Int = 1,
override val scanResult: BleScanResult, override val scanResult: BleScanResult,
override val proximityMessage: ProximityPairing.Message, 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 rssiAverage: Int? = null,
) : SingleApplePods, HasEarDetection, HasAppleColor { ) : SingleApplePods, HasEarDetection, HasAppleColor {
@@ -58,8 +58,8 @@ data class AirPodsMax(
seenFirstAt = result.seenFirstAt, seenFirstAt = result.seenFirstAt,
seenLastAt = scanResult.receivedAt, seenLastAt = scanResult.receivedAt,
seenCounter = result.seenCounter, seenCounter = result.seenCounter,
confidence = result.confidence, reliability = result.reliability,
rssiAverage = result.averageRssi(basic.rssi), rssiAverage = result.rssiSmoothed(basic.rssi),
) )
} }
} }
@@ -20,7 +20,7 @@ data class AirPodsPro(
override val seenCounter: Int = 1, override val seenCounter: Int = 1,
override val scanResult: BleScanResult, override val scanResult: BleScanResult,
override val proximityMessage: ProximityPairing.Message, 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 rssiAverage: Int? = null,
private val cachedBatteryPercentage: Float? = null, private val cachedBatteryPercentage: Float? = null,
private val cachedCaseState: LidState? = null private val cachedCaseState: LidState? = null
@@ -73,9 +73,9 @@ data class AirPodsPro(
seenFirstAt = result.seenFirstAt, seenFirstAt = result.seenFirstAt,
seenLastAt = scanResult.receivedAt, seenLastAt = scanResult.receivedAt,
seenCounter = result.seenCounter, seenCounter = result.seenCounter,
confidence = result.confidence, reliability = result.reliability,
cachedBatteryPercentage = result.getLatestCaseBattery(), cachedBatteryPercentage = result.getLatestCaseBattery(),
rssiAverage = result.averageRssi(basic.rssi), rssiAverage = result.rssiSmoothed(basic.rssi),
cachedCaseState = result.getLatestCaseLidState(basic) cachedCaseState = result.getLatestCaseLidState(basic)
) )
} }
@@ -20,7 +20,7 @@ data class AirPodsPro2(
override val seenCounter: Int = 1, override val seenCounter: Int = 1,
override val scanResult: BleScanResult, override val scanResult: BleScanResult,
override val proximityMessage: ProximityPairing.Message, 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 rssiAverage: Int? = null,
private val cachedBatteryPercentage: Float? = null, private val cachedBatteryPercentage: Float? = null,
private val cachedCaseState: LidState? = null private val cachedCaseState: LidState? = null
@@ -73,9 +73,9 @@ data class AirPodsPro2(
seenFirstAt = result.seenFirstAt, seenFirstAt = result.seenFirstAt,
seenLastAt = scanResult.receivedAt, seenLastAt = scanResult.receivedAt,
seenCounter = result.seenCounter, seenCounter = result.seenCounter,
confidence = result.confidence, reliability = result.reliability,
cachedBatteryPercentage = result.getLatestCaseBattery(), cachedBatteryPercentage = result.getLatestCaseBattery(),
rssiAverage = result.averageRssi(basic.rssi), rssiAverage = result.rssiSmoothed(basic.rssi),
cachedCaseState = result.getLatestCaseLidState(basic) cachedCaseState = result.getLatestCaseLidState(basic)
) )
} }
@@ -17,7 +17,7 @@ data class BeatsFitPro(
override val seenCounter: Int = 1, override val seenCounter: Int = 1,
override val scanResult: BleScanResult, override val scanResult: BleScanResult,
override val proximityMessage: ProximityPairing.Message, 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 rssiAverage: Int? = null,
private val cachedBatteryPercentage: Float? = null, private val cachedBatteryPercentage: Float? = null,
private val cachedCaseState: DualApplePods.LidState? = null private val cachedCaseState: DualApplePods.LidState? = null
@@ -54,9 +54,9 @@ data class BeatsFitPro(
seenFirstAt = result.seenFirstAt, seenFirstAt = result.seenFirstAt,
seenLastAt = scanResult.receivedAt, seenLastAt = scanResult.receivedAt,
seenCounter = result.seenCounter, seenCounter = result.seenCounter,
confidence = result.confidence, reliability = result.reliability,
cachedBatteryPercentage = result.getLatestCaseBattery(), cachedBatteryPercentage = result.getLatestCaseBattery(),
rssiAverage = result.averageRssi(basic.rssi), rssiAverage = result.rssiSmoothed(basic.rssi),
cachedCaseState = result.getLatestCaseLidState(basic) cachedCaseState = result.getLatestCaseLidState(basic)
) )
} }
@@ -17,7 +17,7 @@ data class BeatsFlex(
override val seenCounter: Int = 1, override val seenCounter: Int = 1,
override val scanResult: BleScanResult, override val scanResult: BleScanResult,
override val proximityMessage: ProximityPairing.Message, 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 rssiAverage: Int? = null,
) : SingleApplePods { ) : SingleApplePods {
@@ -46,8 +46,8 @@ data class BeatsFlex(
seenFirstAt = result.seenFirstAt, seenFirstAt = result.seenFirstAt,
seenLastAt = scanResult.receivedAt, seenLastAt = scanResult.receivedAt,
seenCounter = result.seenCounter, seenCounter = result.seenCounter,
confidence = result.confidence, reliability = result.reliability,
rssiAverage = result.averageRssi(basic.rssi), rssiAverage = result.rssiSmoothed(basic.rssi),
) )
} }
@@ -17,7 +17,7 @@ data class BeatsSolo3(
override val seenCounter: Int = 1, override val seenCounter: Int = 1,
override val scanResult: BleScanResult, override val scanResult: BleScanResult,
override val proximityMessage: ProximityPairing.Message, 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 rssiAverage: Int? = null,
) : SingleApplePods { ) : SingleApplePods {
@@ -45,8 +45,8 @@ data class BeatsSolo3(
seenFirstAt = result.seenFirstAt, seenFirstAt = result.seenFirstAt,
seenLastAt = scanResult.receivedAt, seenLastAt = scanResult.receivedAt,
seenCounter = result.seenCounter, seenCounter = result.seenCounter,
confidence = result.confidence, reliability = result.reliability,
rssiAverage = result.averageRssi(basic.rssi), rssiAverage = result.rssiSmoothed(basic.rssi),
) )
} }
@@ -17,7 +17,7 @@ data class BeatsStudio3(
override val seenCounter: Int = 1, override val seenCounter: Int = 1,
override val scanResult: BleScanResult, override val scanResult: BleScanResult,
override val proximityMessage: ProximityPairing.Message, 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 rssiAverage: Int? = null,
) : SingleApplePods { ) : SingleApplePods {
@@ -43,8 +43,8 @@ data class BeatsStudio3(
seenFirstAt = result.seenFirstAt, seenFirstAt = result.seenFirstAt,
seenLastAt = scanResult.receivedAt, seenLastAt = scanResult.receivedAt,
seenCounter = result.seenCounter, seenCounter = result.seenCounter,
confidence = result.confidence, reliability = result.reliability,
rssiAverage = result.averageRssi(basic.rssi), rssiAverage = result.rssiSmoothed(basic.rssi),
) )
} }
} }
@@ -17,7 +17,7 @@ data class BeatsX(
override val seenCounter: Int = 1, override val seenCounter: Int = 1,
override val scanResult: BleScanResult, override val scanResult: BleScanResult,
override val proximityMessage: ProximityPairing.Message, 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 rssiAverage: Int? = null,
) : SingleApplePods { ) : SingleApplePods {
@@ -46,8 +46,8 @@ data class BeatsX(
seenFirstAt = result.seenFirstAt, seenFirstAt = result.seenFirstAt,
seenLastAt = scanResult.receivedAt, seenLastAt = scanResult.receivedAt,
seenCounter = result.seenCounter, seenCounter = result.seenCounter,
confidence = result.confidence, reliability = result.reliability,
rssiAverage = result.averageRssi(basic.rssi), rssiAverage = result.rssiSmoothed(basic.rssi),
) )
} }
@@ -17,7 +17,7 @@ data class PowerBeats3(
override val seenCounter: Int = 1, override val seenCounter: Int = 1,
override val scanResult: BleScanResult, override val scanResult: BleScanResult,
override val proximityMessage: ProximityPairing.Message, 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 rssiAverage: Int? = null,
) : SingleApplePods { ) : SingleApplePods {
@@ -46,8 +46,8 @@ data class PowerBeats3(
seenFirstAt = result.seenFirstAt, seenFirstAt = result.seenFirstAt,
seenLastAt = scanResult.receivedAt, seenLastAt = scanResult.receivedAt,
seenCounter = result.seenCounter, seenCounter = result.seenCounter,
confidence = result.confidence, reliability = result.reliability,
rssiAverage = result.averageRssi(basic.rssi), rssiAverage = result.rssiSmoothed(basic.rssi),
) )
} }
} }
@@ -17,7 +17,7 @@ data class PowerBeatsPro(
override val seenCounter: Int = 1, override val seenCounter: Int = 1,
override val scanResult: BleScanResult, override val scanResult: BleScanResult,
override val proximityMessage: ProximityPairing.Message, 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 rssiAverage: Int? = null,
private val cachedBatteryPercentage: Float? = null, private val cachedBatteryPercentage: Float? = null,
private val cachedCaseState: DualApplePods.LidState? = null private val cachedCaseState: DualApplePods.LidState? = null
@@ -54,9 +54,9 @@ data class PowerBeatsPro(
seenFirstAt = result.seenFirstAt, seenFirstAt = result.seenFirstAt,
seenLastAt = scanResult.receivedAt, seenLastAt = scanResult.receivedAt,
seenCounter = result.seenCounter, seenCounter = result.seenCounter,
confidence = result.confidence, reliability = result.reliability,
cachedBatteryPercentage = result.getLatestCaseBattery(), cachedBatteryPercentage = result.getLatestCaseBattery(),
rssiAverage = result.averageRssi(basic.rssi), rssiAverage = result.rssiSmoothed(basic.rssi),
cachedCaseState = result.getLatestCaseLidState(basic) cachedCaseState = result.getLatestCaseLidState(basic)
) )
} }
@@ -25,7 +25,7 @@ data class FakeAirPodsGen1 constructor(
override val seenCounter: Int = 1, override val seenCounter: Int = 1,
override val scanResult: BleScanResult, override val scanResult: BleScanResult,
override val proximityMessage: ProximityPairing.Message, 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 rssiAverage: Int? = null,
private val cachedBatteryPercentage: Float? = null, private val cachedBatteryPercentage: Float? = null,
) : ApplePods, DualPodDevice, HasEarDetectionDual, HasChargeDetectionDual, HasCase { ) : ApplePods, DualPodDevice, HasEarDetectionDual, HasChargeDetectionDual, HasCase {
@@ -138,9 +138,9 @@ data class FakeAirPodsGen1 constructor(
seenFirstAt = result.seenFirstAt, seenFirstAt = result.seenFirstAt,
seenLastAt = scanResult.receivedAt, seenLastAt = scanResult.receivedAt,
seenCounter = result.seenCounter, seenCounter = result.seenCounter,
confidence = result.confidence, reliability = result.reliability,
cachedBatteryPercentage = result.getLatestCaseBattery(), 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 seenCounter: Int = 1,
override val scanResult: BleScanResult, override val scanResult: BleScanResult,
override val proximityMessage: ProximityPairing.Message, 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 rssiAverage: Int? = null,
private val cachedBatteryPercentage: Float? = null, private val cachedBatteryPercentage: Float? = null,
) : ApplePods, DualPodDevice, HasEarDetectionDual, HasChargeDetectionDual, HasCase { ) : ApplePods, DualPodDevice, HasEarDetectionDual, HasChargeDetectionDual, HasCase {
@@ -136,9 +136,9 @@ data class FakeAirPodsGen2 constructor(
seenFirstAt = result.seenFirstAt, seenFirstAt = result.seenFirstAt,
seenLastAt = scanResult.receivedAt, seenLastAt = scanResult.receivedAt,
seenCounter = result.seenCounter, seenCounter = result.seenCounter,
confidence = result.confidence, reliability = result.reliability,
cachedBatteryPercentage = result.getLatestCaseBattery(), 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 seenCounter: Int = 1,
override val scanResult: BleScanResult, override val scanResult: BleScanResult,
override val proximityMessage: ProximityPairing.Message, 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 rssiAverage: Int? = null,
private val cachedBatteryPercentage: Float? = null, private val cachedBatteryPercentage: Float? = null,
) : ApplePods, DualPodDevice, HasEarDetectionDual, HasChargeDetectionDual, HasCase { ) : ApplePods, DualPodDevice, HasEarDetectionDual, HasChargeDetectionDual, HasCase {
@@ -138,9 +138,9 @@ data class FakeAirPodsGen3 constructor(
seenFirstAt = result.seenFirstAt, seenFirstAt = result.seenFirstAt,
seenLastAt = scanResult.receivedAt, seenLastAt = scanResult.receivedAt,
seenCounter = result.seenCounter, seenCounter = result.seenCounter,
confidence = result.confidence, reliability = result.reliability,
cachedBatteryPercentage = result.getLatestCaseBattery(), 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 seenCounter: Int = 1,
override val scanResult: BleScanResult, override val scanResult: BleScanResult,
override val proximityMessage: ProximityPairing.Message, 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 rssiAverage: Int? = null,
private val cachedBatteryPercentage: Float? = null, private val cachedBatteryPercentage: Float? = null,
) : ApplePods, DualPodDevice, HasChargeDetectionDual, HasEarDetectionDual, HasCase { ) : ApplePods, DualPodDevice, HasChargeDetectionDual, HasEarDetectionDual, HasCase {
@@ -156,9 +156,9 @@ data class FakeAirPodsPro constructor(
seenFirstAt = result.seenFirstAt, seenFirstAt = result.seenFirstAt,
seenLastAt = scanResult.receivedAt, seenLastAt = scanResult.receivedAt,
seenCounter = result.seenCounter, seenCounter = result.seenCounter,
confidence = result.confidence, reliability = result.reliability,
cachedBatteryPercentage = result.getLatestCaseBattery(), 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 seenCounter: Int = 1,
override val scanResult: BleScanResult, override val scanResult: BleScanResult,
override val proximityMessage: ProximityPairing.Message, 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 rssiAverage: Int? = null,
private val cachedBatteryPercentage: Float? = null, private val cachedBatteryPercentage: Float? = null,
private val cachedCaseState: DualApplePods.LidState? = null, private val cachedCaseState: DualApplePods.LidState? = null,
@@ -153,9 +153,9 @@ data class FakeAirPodsPro2 constructor(
seenFirstAt = result.seenFirstAt, seenFirstAt = result.seenFirstAt,
seenLastAt = scanResult.receivedAt, seenLastAt = scanResult.receivedAt,
seenCounter = result.seenCounter, seenCounter = result.seenCounter,
confidence = result.confidence, reliability = result.reliability,
cachedBatteryPercentage = result.getLatestCaseBattery(), 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 seenCounter: Int = 1,
override val scanResult: BleScanResult, override val scanResult: BleScanResult,
override val proximityMessage: ProximityPairing.Message, override val proximityMessage: ProximityPairing.Message,
override val confidence: Float = 0f, override val reliability: Float = 0f,
private val rssiAverage: Int? = null, private val rssiAverage: Int? = null,
) : ApplePods { ) : ApplePods {
@@ -49,8 +49,8 @@ data class UnknownAppleDevice(
seenFirstAt = result.seenFirstAt, seenFirstAt = result.seenFirstAt,
seenLastAt = scanResult.receivedAt, seenLastAt = scanResult.receivedAt,
seenCounter = result.seenCounter, seenCounter = result.seenCounter,
confidence = result.confidence, reliability = result.reliability,
rssiAverage = result.averageRssi(basic.rssi), rssiAverage = result.rssiSmoothed(basic.rssi),
) )
} }
} }
@@ -13,7 +13,7 @@ data class UnknownDevice(
override val seenFirstAt: Instant = Instant.now(), override val seenFirstAt: Instant = Instant.now(),
override val seenCounter: Int = 1, override val seenCounter: Int = 1,
override val scanResult: BleScanResult, override val scanResult: BleScanResult,
override val confidence: Float = 0f, override val reliability: Float = 0f,
private val rssiAverage: Int? = null, private val rssiAverage: Int? = null,
) : PodDevice { ) : PodDevice {
@@ -34,8 +34,8 @@ class UnknownDeviceFactory @Inject constructor() {
seenFirstAt = result.seenFirstAt, seenFirstAt = result.seenFirstAt,
seenLastAt = scanResult.receivedAt, seenLastAt = scanResult.receivedAt,
seenCounter = result.seenCounter, seenCounter = result.seenCounter,
confidence = result.confidence, reliability = result.confidence,
rssiAverage = result.averageRssi(basic.rssi), rssiAverage = result.rssiSmoothed(basic.rssi),
) )
} }
@@ -52,7 +52,7 @@ class UnknownDeviceFactory @Inject constructor() {
val confidence: Float val confidence: Float
get() = 0.75f + (history.size / (MAX_HISTORY * 4f)) 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() history.map { it.rssi }.plus(latest).takeLast(10).median()
fun isOlderThan(age: Duration): Boolean { fun isOlderThan(age: Duration): Boolean {
@@ -70,7 +70,7 @@ class UnknownDeviceFactory @Inject constructor() {
override fun toString(): String = "KnownDevice(history=${history.size}, last=${history.last()})" override fun toString(): String = "KnownDevice(history=${history.size}, last=${history.last()})"
companion object { companion object {
const val MAX_HISTORY = 10 const val MAX_HISTORY = 20
} }
} }
+7 -9
View File
@@ -6,7 +6,6 @@ plugins {
} }
apply(plugin = "dagger.hilt.android.plugin") apply(plugin = "dagger.hilt.android.plugin")
apply(plugin = "androidx.navigation.safeargs.kotlin") apply(plugin = "androidx.navigation.safeargs.kotlin")
apply(plugin = "com.bugsnag.android.gradle")
android { android {
compileSdk = ProjectConfig.compileSdk compileSdk = ProjectConfig.compileSdk
@@ -24,10 +23,6 @@ android {
buildConfigField("String", "GITSHA", "\"${lastCommitHash()}\"") buildConfigField("String", "GITSHA", "\"${lastCommitHash()}\"")
buildConfigField("String", "BUILDTIME", "\"${buildTime()}\"") buildConfigField("String", "BUILDTIME", "\"${buildTime()}\"")
manifestPlaceholders["bugsnagApiKey"] = getBugSnagApiKey(
File(System.getProperty("user.home"), ".appconfig/${ProjectConfig.packageName}/bugsnag.properties")
) ?: "fake"
} }
signingConfigs { signingConfigs {
@@ -49,9 +44,12 @@ android {
create("gplay") { create("gplay") {
dimension = "version" dimension = "version"
signingConfig = signingConfigs["releaseGplay"] signingConfig = signingConfigs["releaseGplay"]
extra["useBugsnag"] = true
} }
} }
setupBugsnagPlugin()
buildTypes { buildTypes {
val customProguardRules = fileTree(File("../proguard")) { val customProguardRules = fileTree(File("../proguard")) {
include("*.pro") include("*.pro")
@@ -92,7 +90,7 @@ android {
if (listOf("release", "beta").any { variantName.toLowerCase().contains(it) }) { if (listOf("release", "beta").any { variantName.toLowerCase().contains(it) }) {
val outputFileName = ProjectConfig.packageName + "-WEAROS" + val outputFileName = ProjectConfig.packageName + "-WEAROS" +
"-v${defaultConfig.versionName}-${defaultConfig.versionCode}" + "-v${defaultConfig.versionName}-${defaultConfig.versionCode}" +
"-${variantName.toUpperCase()}-${lastCommitHash()}.apk" "-${variantName.toUpperCase()}.apk"
variantOutputImpl.outputFileName = outputFileName variantOutputImpl.outputFileName = outputFileName
} }
@@ -138,9 +136,9 @@ dependencies {
addTesting() 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:wear:1.2.0")
implementation("androidx.wear.tiles:tiles-material:1.1.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")
} }
-4
View File
@@ -39,10 +39,6 @@
android:name="androidx.startup.InitializationProvider" android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup" android:authorities="${applicationId}.androidx-startup"
tools:node="remove" /> tools:node="remove" />
<meta-data
android:name="com.bugsnag.android.API_KEY"
android:value="${bugsnagApiKey}" />
</application> </application>
</manifest> </manifest>
@@ -3,11 +3,10 @@ package eu.darken.capod
import android.app.Application import android.app.Application
import androidx.hilt.work.HiltWorkerFactory import androidx.hilt.work.HiltWorkerFactory
import androidx.work.* import androidx.work.*
import com.getkeepsafe.relinker.ReLinker
import dagger.hilt.android.HiltAndroidApp import dagger.hilt.android.HiltAndroidApp
import eu.darken.capod.common.BuildConfigWrap import eu.darken.capod.common.BuildConfigWrap
import eu.darken.capod.common.coroutine.AppScope 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.*
import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE
import eu.darken.capod.main.core.GeneralSettings import eu.darken.capod.main.core.GeneralSettings
@@ -20,7 +19,7 @@ import javax.inject.Inject
open class App : Application(), Configuration.Provider { open class App : Application(), Configuration.Provider {
@Inject lateinit var workerFactory: HiltWorkerFactory @Inject lateinit var workerFactory: HiltWorkerFactory
@Inject lateinit var autoReporting: AutoReporting @Inject lateinit var autoReporting: AutomaticBugReporter
@Inject lateinit var generalSettings: GeneralSettings @Inject lateinit var generalSettings: GeneralSettings
@Inject lateinit var workManager: WorkManager @Inject lateinit var workManager: WorkManager
@Inject @AppScope lateinit var appScope: CoroutineScope @Inject @AppScope lateinit var appScope: CoroutineScope
@@ -29,11 +28,7 @@ open class App : Application(), Configuration.Provider {
super.onCreate() super.onCreate()
if (BuildConfig.DEBUG) Logging.install(LogCatLogger()) if (BuildConfig.DEBUG) Logging.install(LogCatLogger())
ReLinker autoReporting.setup(this)
.log { message -> log(TAG) { "ReLinker: $message" } }
.loadLibrary(this, "bugsnag-plugin-android-anr")
autoReporting.setup()
setupWorker() setupWorker()
@@ -5,7 +5,7 @@ import androidx.lifecycle.SavedStateHandle
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import eu.darken.capod.common.bluetooth.BluetoothManager2 import eu.darken.capod.common.bluetooth.BluetoothManager2
import eu.darken.capod.common.coroutine.DispatcherProvider 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.Logging.Priority.VERBOSE
import eu.darken.capod.common.debug.logging.log import eu.darken.capod.common.debug.logging.log
import eu.darken.capod.common.flow.combine import eu.darken.capod.common.flow.combine
+7 -9
View File
@@ -6,7 +6,6 @@ plugins {
} }
apply(plugin = "dagger.hilt.android.plugin") apply(plugin = "dagger.hilt.android.plugin")
apply(plugin = "androidx.navigation.safeargs.kotlin") apply(plugin = "androidx.navigation.safeargs.kotlin")
apply(plugin = "com.bugsnag.android.gradle")
android { android {
@@ -22,10 +21,6 @@ android {
versionName = ProjectConfig.Version.name versionName = ProjectConfig.Version.name
testInstrumentationRunner = "eu.darken.capod.HiltTestRunner" testInstrumentationRunner = "eu.darken.capod.HiltTestRunner"
manifestPlaceholders["bugsnagApiKey"] = getBugSnagApiKey(
File(System.getProperty("user.home"), ".appconfig/${ProjectConfig.packageName}/bugsnag.properties")
) ?: "fake"
} }
signingConfigs { signingConfigs {
@@ -47,9 +42,12 @@ android {
create("gplay") { create("gplay") {
dimension = "version" dimension = "version"
signingConfig = signingConfigs["releaseGplay"] signingConfig = signingConfigs["releaseGplay"]
extra["useBugsnag"] = true
} }
} }
setupBugsnagPlugin()
buildTypes { buildTypes {
val customProguardRules = fileTree(File(projectDir, "proguard")) { val customProguardRules = fileTree(File(projectDir, "proguard")) {
include("*.pro") include("*.pro")
@@ -90,7 +88,7 @@ android {
if (listOf("release", "beta").any { variantName.toLowerCase().contains(it) }) { if (listOf("release", "beta").any { variantName.toLowerCase().contains(it) }) {
val outputFileName = ProjectConfig.packageName + val outputFileName = ProjectConfig.packageName +
"-v${defaultConfig.versionName}-${defaultConfig.versionCode}" + "-v${defaultConfig.versionName}-${defaultConfig.versionCode}" +
"-${variantName.toUpperCase()}-${lastCommitHash()}.apk" "-${variantName.toUpperCase()}.apk"
variantOutputImpl.outputFileName = outputFileName variantOutputImpl.outputFileName = outputFileName
} }
@@ -148,8 +146,8 @@ dependencies {
addTesting() 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.android.billingclient:billing:4.0.0")
"gplayImplementation"("com.bugsnag:bugsnag-android:5.9.2")
"gplayImplementation"("com.getkeepsafe.relinker:relinker:1.4.3")
} }
-4
View File
@@ -86,10 +86,6 @@
android:name="androidx.startup.InitializationProvider" android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup" android:authorities="${applicationId}.androidx-startup"
tools:node="remove" /> tools:node="remove" />
<meta-data
android:name="com.bugsnag.android.API_KEY"
android:value="${bugsnagApiKey}" />
</application> </application>
</manifest> </manifest>
+3 -8
View File
@@ -3,10 +3,9 @@ package eu.darken.capod
import android.app.Application import android.app.Application
import androidx.hilt.work.HiltWorkerFactory import androidx.hilt.work.HiltWorkerFactory
import androidx.work.Configuration import androidx.work.Configuration
import com.getkeepsafe.relinker.ReLinker
import dagger.hilt.android.HiltAndroidApp import dagger.hilt.android.HiltAndroidApp
import eu.darken.capod.common.coroutine.AppScope 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.*
import eu.darken.capod.common.flow.throttleLatest import eu.darken.capod.common.flow.throttleLatest
import eu.darken.capod.common.upgrade.UpgradeRepo import eu.darken.capod.common.upgrade.UpgradeRepo
@@ -25,7 +24,7 @@ import javax.inject.Inject
open class App : Application(), Configuration.Provider { open class App : Application(), Configuration.Provider {
@Inject lateinit var workerFactory: HiltWorkerFactory @Inject lateinit var workerFactory: HiltWorkerFactory
@Inject lateinit var autoReporting: AutoReporting @Inject lateinit var autoReporting: AutomaticBugReporter
@Inject lateinit var monitorControl: MonitorControl @Inject lateinit var monitorControl: MonitorControl
@Inject lateinit var podMonitor: PodMonitor @Inject lateinit var podMonitor: PodMonitor
@Inject lateinit var widgetManager: WidgetManager @Inject lateinit var widgetManager: WidgetManager
@@ -36,11 +35,7 @@ open class App : Application(), Configuration.Provider {
super.onCreate() super.onCreate()
if (BuildConfig.DEBUG) Logging.install(LogCatLogger()) if (BuildConfig.DEBUG) Logging.install(LogCatLogger())
ReLinker autoReporting.setup(this)
.log { message -> log(TAG) { "ReLinker: $message" } }
.loadLibrary(this, "bugsnag-plugin-android-anr")
autoReporting.setup()
log(TAG) { "onCreate() done! ${Exception().asLog()}" } log(TAG) { "onCreate() done! ${Exception().asLog()}" }
@@ -6,7 +6,7 @@ import androidx.lifecycle.SavedStateHandle
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import eu.darken.capod.common.bluetooth.BluetoothManager2 import eu.darken.capod.common.bluetooth.BluetoothManager2
import eu.darken.capod.common.coroutine.DispatcherProvider 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.log
import eu.darken.capod.common.flow.combine import eu.darken.capod.common.flow.combine
import eu.darken.capod.common.flow.throttleLatest import eu.darken.capod.common.flow.throttleLatest
@@ -7,7 +7,7 @@ import androidx.fragment.app.viewModels
import androidx.preference.Preference import androidx.preference.Preference
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import eu.darken.capod.R 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.observe2
import eu.darken.capod.common.uix.PreferenceFragment3 import eu.darken.capod.common.uix.PreferenceFragment3
import javax.inject.Inject import javax.inject.Inject
@@ -3,7 +3,7 @@ package eu.darken.capod.main.ui.settings.general.debug
import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.SavedStateHandle
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import eu.darken.capod.common.coroutine.DispatcherProvider 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.log
import eu.darken.capod.common.debug.logging.logTag import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.common.debug.recording.core.RecorderModule import eu.darken.capod.common.debug.recording.core.RecorderModule
@@ -37,15 +37,13 @@ class WidgetProvider : AppWidgetProvider() {
@Inject lateinit var upgradeRepo: UpgradeRepo @Inject lateinit var upgradeRepo: UpgradeRepo
@AppScope @Inject lateinit var appScope: CoroutineScope @AppScope @Inject lateinit var appScope: CoroutineScope
private var asyncBarrier: PendingResult? = null
private fun executeAsync( private fun executeAsync(
tag: String, tag: String,
timeout: Duration = Duration.ofSeconds(10), timeout: Duration = Duration.ofSeconds(8),
block: suspend () -> Unit block: suspend () -> Unit
) { ) {
val start = System.currentTimeMillis() val start = System.currentTimeMillis()
asyncBarrier = goAsync() val asyncBarrier = goAsync()
log(TAG, VERBOSE) { "executeAsync($tag) starting asyncBarrier=$asyncBarrier " } log(TAG, VERBOSE) { "executeAsync($tag) starting asyncBarrier=$asyncBarrier " }
appScope.launch { appScope.launch {
@@ -54,7 +52,7 @@ class WidgetProvider : AppWidgetProvider() {
} catch (e: Exception) { } catch (e: Exception) {
log(TAG, ERROR) { "executeAsync($tag) failed: ${e.asLog()}" } log(TAG, ERROR) { "executeAsync($tag) failed: ${e.asLog()}" }
} finally { } finally {
asyncBarrier?.finish() asyncBarrier.finish()
val stop = System.currentTimeMillis() val stop = System.currentTimeMillis()
log(TAG, VERBOSE) { "executeAsync($tag) DONE (${stop - start}ms) " } log(TAG, VERBOSE) { "executeAsync($tag) DONE (${stop - start}ms) " }
} }
@@ -6,8 +6,6 @@ import android.widget.RemoteViews
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import eu.darken.capod.R import eu.darken.capod.R
import eu.darken.capod.pods.core.* 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 import javax.inject.Inject
@@ -16,12 +14,12 @@ class MonitorNotificationViewFactory @Inject constructor(
) { ) {
fun createContentView(device: PodDevice): RemoteViews = when (device) { fun createContentView(device: PodDevice): RemoteViews = when (device) {
is DualApplePods -> createDualApplePods(device) is DualPodDevice -> createDualPods(device)
is SingleApplePods -> createSingleApplePods(device) is SinglePodDevice -> createSinglePod(device)
else -> createUnknownDevice(device) else -> createUnknownDevice(device)
} }
private fun createDualApplePods(device: DualApplePods): RemoteViews = RemoteViews( private fun createDualPods(device: DualPodDevice): RemoteViews = RemoteViews(
context.packageName, context.packageName,
R.layout.monitor_notification_dual_pods_small R.layout.monitor_notification_dual_pods_small
).apply { ).apply {
@@ -29,23 +27,30 @@ class MonitorNotificationViewFactory @Inject constructor(
// Left // Left
setImageViewResource(R.id.pod_left_icon, device.leftPodIcon) setImageViewResource(R.id.pod_left_icon, device.leftPodIcon)
setTextViewText(R.id.pod_left_label, getBatteryLevelLeftPod(context)) 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) 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) setViewVisibility(R.id.pod_left_ear, if (isLeftPodInEar) View.VISIBLE else View.GONE)
// Case // Case
setImageViewResource(R.id.pod_case_icon, device.caseIcon) setViewVisibility(R.id.pod_case_charging, if (device is HasCase) View.VISIBLE else View.GONE)
setTextViewText(R.id.pod_case_label, getBatteryLevelCase(context)) (device as? HasCase)?.let { case ->
setViewVisibility(R.id.pod_case_charging, if (isCaseCharging) View.VISIBLE else View.GONE) 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 // Right
setImageViewResource(R.id.pod_right_icon, device.rightPodIcon) setImageViewResource(R.id.pod_right_icon, device.rightPodIcon)
setTextViewText(R.id.pod_right_label, getBatteryLevelRightPod(context)) 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) 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) 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, context.packageName,
R.layout.monitor_notification_single_pods_small R.layout.monitor_notification_single_pods_small
).apply { ).apply {
@@ -6,14 +6,13 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.appcompat.view.ContextThemeWrapper import androidx.appcompat.view.ContextThemeWrapper
import androidx.core.view.isInvisible import androidx.core.view.isInvisible
import androidx.core.view.isVisible
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import eu.darken.capod.R 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.PopupNotificationDualPodsBinding
import eu.darken.capod.databinding.PopupNotificationSinglePodsBinding import eu.darken.capod.databinding.PopupNotificationSinglePodsBinding
import eu.darken.capod.pods.core.* 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 import javax.inject.Inject
@@ -26,13 +25,13 @@ class PopUpPodViewFactory @Inject constructor(
private val layoutInflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater private val layoutInflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
fun createContentView(parent: ViewGroup, device: PodDevice): View = when (device) { 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? // Unused, has no case to trigger reaction?
is SingleApplePods -> createSingleApplePods(parent, device) is SinglePodDevice -> createSinglePod(parent, device)
else -> throw IllegalArgumentException("Unexpected device: $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 { PopupNotificationDualPodsBinding.inflate(layoutInflater, parent, false).apply {
device.apply { device.apply {
podIcon.setImageResource(iconRes) podIcon.setImageResource(iconRes)
@@ -46,9 +45,12 @@ class PopUpPodViewFactory @Inject constructor(
podLeftBatteryLabel.text = getBatteryLevelLeftPod(context) podLeftBatteryLabel.text = getBatteryLevelLeftPod(context)
// Case // Case
podCaseIcon.setImageResource(device.caseIcon) podCaseContainer.isVisible = device is HasCase
podCaseBatteryIcon.setImageResource(getBatteryDrawable(batteryCasePercent)) (device as? HasCase)?.let { case ->
podCaseBatteryLabel.text = getBatteryLevelCase(context) podCaseIcon.setImageResource(case.caseIcon)
podCaseBatteryIcon.setImageResource(getBatteryDrawable(case.batteryCasePercent))
podCaseBatteryLabel.text = case.getBatteryLevelCase(context)
}
// Right // Right
podRightIcon.setImageResource(device.rightPodIcon) podRightIcon.setImageResource(device.rightPodIcon)
@@ -57,7 +59,7 @@ class PopUpPodViewFactory @Inject constructor(
} }
}.root }.root
private fun createSingleApplePods(parent: ViewGroup, device: SingleApplePods): View = private fun createSinglePod(parent: ViewGroup, device: SinglePodDevice): View =
PopupNotificationSinglePodsBinding.inflate(layoutInflater, parent, false).apply { PopupNotificationSinglePodsBinding.inflate(layoutInflater, parent, false).apply {
device.apply { device.apply {
headphonesIcon.setImageResource(iconRes) headphonesIcon.setImageResource(iconRes)
@@ -57,6 +57,7 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:gravity="center_vertical" android:gravity="center_vertical"
android:id="@+id/pod_case_container"
android:orientation="horizontal"> android:orientation="horizontal">
<ImageView <ImageView
@@ -7,9 +7,7 @@
<ImageView <ImageView
android:id="@+id/pod_icon" android:id="@+id/pod_icon"
style="@style/PodInfoItemIcon" style="@style/PodInfoItemIcon.Popup"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/devic_earbuds_generic_both" android:src="@drawable/devic_earbuds_generic_both"
app:layout_constraintBottom_toBottomOf="@id/pod_label" app:layout_constraintBottom_toBottomOf="@id/pod_label"
app:layout_constraintEnd_toStartOf="@id/pod_label" app:layout_constraintEnd_toStartOf="@id/pod_label"
@@ -55,9 +53,7 @@
<ImageView <ImageView
android:id="@+id/pod_left_icon" android:id="@+id/pod_left_icon"
style="@style/PodInfoItemIcon" style="@style/PodInfoItemIcon.Popup"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center" android:layout_gravity="center"
android:src="@drawable/devic_airpods_gen1_left" android:src="@drawable/devic_airpods_gen1_left"
app:layout_constraintBottom_toBottomOf="@id/pod_left_battery_label" app:layout_constraintBottom_toBottomOf="@id/pod_left_battery_label"
@@ -67,9 +63,7 @@
<ImageView <ImageView
android:id="@id/pod_left_battery_icon" android:id="@id/pod_left_battery_icon"
style="@style/PodInfoItemIcon" style="@style/PodInfoItemIcon.Popup"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_baseline_battery_unknown_24" android:src="@drawable/ic_baseline_battery_unknown_24"
app:layout_constraintBottom_toBottomOf="@id/pod_left_battery_label" app:layout_constraintBottom_toBottomOf="@id/pod_left_battery_label"
app:layout_constraintEnd_toStartOf="@id/pod_left_battery_label" app:layout_constraintEnd_toStartOf="@id/pod_left_battery_label"
@@ -103,9 +97,7 @@
<ImageView <ImageView
android:id="@+id/pod_case_icon" android:id="@+id/pod_case_icon"
style="@style/PodInfoItemIcon" style="@style/PodInfoItemIcon.Popup"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center" android:layout_gravity="center"
android:src="@drawable/devic_airpods_gen1_case" android:src="@drawable/devic_airpods_gen1_case"
app:layout_constraintBottom_toBottomOf="@id/pod_case_battery_label" app:layout_constraintBottom_toBottomOf="@id/pod_case_battery_label"
@@ -115,9 +107,7 @@
<ImageView <ImageView
android:id="@id/pod_case_battery_icon" android:id="@id/pod_case_battery_icon"
style="@style/PodInfoItemIcon" style="@style/PodInfoItemIcon.Popup"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_baseline_battery_unknown_24" android:src="@drawable/ic_baseline_battery_unknown_24"
app:layout_constraintBottom_toBottomOf="@id/pod_case_battery_label" app:layout_constraintBottom_toBottomOf="@id/pod_case_battery_label"
app:layout_constraintEnd_toStartOf="@id/pod_case_battery_label" app:layout_constraintEnd_toStartOf="@id/pod_case_battery_label"
@@ -150,9 +140,7 @@
<ImageView <ImageView
android:id="@+id/pod_right_icon" android:id="@+id/pod_right_icon"
style="@style/PodInfoItemIcon" style="@style/PodInfoItemIcon.Popup"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center" android:layout_gravity="center"
android:src="@drawable/devic_airpods_gen1_right" android:src="@drawable/devic_airpods_gen1_right"
app:layout_constraintBottom_toBottomOf="@id/pod_right_battery_label" app:layout_constraintBottom_toBottomOf="@id/pod_right_battery_label"
@@ -162,9 +150,7 @@
<ImageView <ImageView
android:id="@id/pod_right_battery_icon" android:id="@id/pod_right_battery_icon"
style="@style/PodInfoItemIcon" style="@style/PodInfoItemIcon.Popup"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_baseline_battery_unknown_24" android:src="@drawable/ic_baseline_battery_unknown_24"
app:layout_constraintBottom_toBottomOf="@id/pod_right_battery_label" app:layout_constraintBottom_toBottomOf="@id/pod_right_battery_label"
app:layout_constraintEnd_toStartOf="@id/pod_right_battery_label" app:layout_constraintEnd_toStartOf="@id/pod_right_battery_label"
@@ -7,9 +7,7 @@
<ImageView <ImageView
android:id="@+id/headphones_icon" android:id="@+id/headphones_icon"
android:layout_width="wrap_content" style="@style/PodInfoItemIcon.Popup"
style="@style/PodInfoItemIcon"
android:layout_height="wrap_content"
android:src="@drawable/devic_earbuds_generic_both" android:src="@drawable/devic_earbuds_generic_both"
app:layout_constraintBottom_toBottomOf="@id/headphones_label" app:layout_constraintBottom_toBottomOf="@id/headphones_label"
app:layout_constraintEnd_toStartOf="@id/headphones_label" app:layout_constraintEnd_toStartOf="@id/headphones_label"
@@ -56,9 +54,7 @@
<ImageView <ImageView
android:id="@id/headphones_battery_icon" android:id="@id/headphones_battery_icon"
android:layout_width="wrap_content" style="@style/PodInfoItemIcon.Popup"
style="@style/PodInfoItemIcon"
android:layout_height="wrap_content"
android:src="@drawable/ic_baseline_battery_unknown_24" android:src="@drawable/ic_baseline_battery_unknown_24"
app:layout_constraintBottom_toBottomOf="@id/headphones_battery_label" app:layout_constraintBottom_toBottomOf="@id/headphones_battery_label"
app:layout_constraintEnd_toStartOf="@id/headphones_battery_label" app:layout_constraintEnd_toStartOf="@id/headphones_battery_label"
+5
View File
@@ -38,6 +38,11 @@
<item name="tint">?android:attr/colorControlNormal</item> <item name="tint">?android:attr/colorControlNormal</item>
</style> </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"> <style name="PodInfoItemText" parent="TextAppearance.MaterialComponents.Body2">
<item name="android:singleLine">true</item> <item name="android:singleLine">true</item>
<item name="android:ellipsize">end</item> <item name="android:ellipsize">end</item>
+30 -1
View File
@@ -1,6 +1,9 @@
import com.android.build.gradle.LibraryExtension import com.android.build.gradle.LibraryExtension
import org.gradle.api.Action import org.gradle.api.Action
import org.gradle.api.JavaVersion 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.File
import java.io.FileInputStream import java.io.FileInputStream
import java.time.Instant import java.time.Instant
@@ -122,4 +125,30 @@ fun getBugSnagApiKey(
} }
return System.getenv("BUGSNAG_API_KEY") ?: bugsnagProps.getProperty("bugsnag.apikey") 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)")
}
}
}
}
}
+2 -2
View File
@@ -1,6 +1,6 @@
### Updated by release.sh ### ### Updated by release.sh ###
project.versioning.major=2 project.versioning.major=2
project.versioning.minor=7 project.versioning.minor=8
project.versioning.patch=2 project.versioning.patch=0
project.versioning.build=0 project.versioning.build=0
############################# #############################