mirror of
https://github.com/d4rken-org/capod.git
synced 2026-09-16 11:16:12 -04:00
Compare commits
43
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
71a060feb9 | ||
|
|
25d643257c | ||
|
|
85900e5bff | ||
|
|
d3257d6c5e | ||
|
|
afe1399f72 | ||
|
|
20ce2d3e22 | ||
|
|
cea8f2290d | ||
|
|
1b5b59c41b | ||
|
|
8df7c19e84 | ||
|
|
88b0069b94 | ||
|
|
3b8ddc9f06 | ||
|
|
708c7f38ec | ||
|
|
139daa1cc2 | ||
|
|
b85ac778c0 | ||
|
|
a3e2c9e61d | ||
|
|
ad78a0570c | ||
|
|
7e6ee274d8 | ||
|
|
daf7411c36 | ||
|
|
d3dedbe6cd | ||
|
|
d5411586d5 | ||
|
|
94e2d40554 | ||
|
|
db80fa1b46 | ||
|
|
9b569b8d07 | ||
|
|
90288880fb | ||
|
|
870e518ff6 | ||
|
|
6d89169bae | ||
|
|
ac77ee2703 | ||
|
|
18dcf603e9 | ||
|
|
71aaba1b86 | ||
|
|
64b9d5f17a | ||
|
|
a9f23c4bfe | ||
|
|
f454f46778 | ||
|
|
432936ea2c | ||
|
|
8211b0ac88 | ||
|
|
c314987e13 | ||
|
|
fc6cd74752 | ||
|
|
a47011e579 | ||
|
|
fbeeec6539 | ||
|
|
e1cd79cd9d | ||
|
|
692ced1ee7 | ||
|
|
c5cc127df4 | ||
|
|
5f00f10ae8 | ||
|
|
0fc9716f06 |
@@ -12,3 +12,4 @@
|
|||||||
*.jks
|
*.jks
|
||||||
.local/*
|
.local/*
|
||||||
/fastlane/report.xml
|
/fastlane/report.xml
|
||||||
|
/fastlane/Appfile
|
||||||
@@ -43,12 +43,13 @@ Currently supported models:
|
|||||||
|
|
||||||
## Download
|
## Download
|
||||||
|
|
||||||
| Source | Status |
|
| Source | Status |
|
||||||
|------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| [Google Play](https://play.google.com/store/apps/details?id=eu.darken.capod) | [](https://play.google.com/store/apps/details?id=eu.darken.capod) |
|
| [Google Play](https://play.google.com/store/apps/details?id=eu.darken.capod) | [](https://play.google.com/store/apps/details?id=eu.darken.capod) |
|
||||||
| [Google Play Beta](https://play.google.com/apps/testing/eu.darken.capod) | [](https://play.google.com/apps/testing/eu.darken.capod) | |
|
| [Google Play Beta](https://play.google.com/apps/testing/eu.darken.capod) | [](https://play.google.com/apps/testing/eu.darken.capod) | |
|
||||||
| [Github Releases](https://github.com/d4rken-org/capod/releases) | [](https://github.com/d4rken-org/capod/releases/latest) |
|
| [Github Releases](https://github.com/d4rken-org/capod/releases) | [](https://github.com/d4rken-org/capod/releases/latest) |
|
||||||
| [F-Droid (IzzyOnDroid)](https://apt.izzysoft.de/packages/eu.darken.capod/) | [](https://apt.izzysoft.de/packages/eu.darken.capod/) |
|
| [F-Droid](https://f-droid.org/en/packages/eu.darken.capod/) | [](https://f-droid.org/en/packages/eu.darken.capod/) |
|
||||||
|
| [F-Droid (IzzyOnDroid)](https://apt.izzysoft.de/packages/eu.darken.capod/) | [](https://apt.izzysoft.de/packages/eu.darken.capod/) |
|
||||||
|
|
||||||
|
|
||||||
## Support the project
|
## Support the project
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -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.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
-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.Event
|
||||||
import eu.darken.capod.common.debug.logging.Logging
|
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 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
-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.Event
|
||||||
import com.bugsnag.android.OnErrorCallback
|
import com.bugsnag.android.OnErrorCallback
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import eu.darken.capod.common.debug.logging.log
|
||||||
|
|
||||||
|
fun BroadcastReceiver.PendingResult.finish2(): Boolean = try {
|
||||||
|
finish()
|
||||||
|
true
|
||||||
|
} catch (e: IllegalStateException) {
|
||||||
|
log { "BroadcastReceiver.PendingResult.finish() failed: $e" }
|
||||||
|
false
|
||||||
|
}
|
||||||
@@ -32,9 +32,9 @@ class BleScanner @Inject constructor(
|
|||||||
@SuppressLint("MissingPermission") fun scan(
|
@SuppressLint("MissingPermission") fun scan(
|
||||||
filters: Set<ScanFilter>,
|
filters: Set<ScanFilter>,
|
||||||
scannerMode: ScannerMode = ScannerMode.BALANCED,
|
scannerMode: ScannerMode = ScannerMode.BALANCED,
|
||||||
disableOffloadFiltering: Boolean = true,
|
disableOffloadFiltering: Boolean = false,
|
||||||
disableOffloadBatching: Boolean = true,
|
disableOffloadBatching: Boolean = false,
|
||||||
disableDirectScanCallback: Boolean = true,
|
disableDirectScanCallback: Boolean = false,
|
||||||
): Flow<Collection<BleScanResult>> = callbackFlow {
|
): Flow<Collection<BleScanResult>> = callbackFlow {
|
||||||
log(TAG) { "scan(filters=$filters, scannerMode=$scannerMode)" }
|
log(TAG) { "scan(filters=$filters, scannerMode=$scannerMode)" }
|
||||||
|
|
||||||
|
|||||||
@@ -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
-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.Context
|
||||||
import android.content.SharedPreferences
|
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 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)
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
package eu.darken.capod.monitor.core
|
|
||||||
|
|
||||||
import dagger.BindsInstance
|
|
||||||
import dagger.hilt.DefineComponent
|
|
||||||
import dagger.hilt.components.SingletonComponent
|
|
||||||
|
|
||||||
@MonitorScope
|
|
||||||
@DefineComponent(parent = SingletonComponent::class)
|
|
||||||
interface MonitorComponent {
|
|
||||||
|
|
||||||
@DefineComponent.Builder
|
|
||||||
interface Builder {
|
|
||||||
|
|
||||||
fun coroutineScope(@BindsInstance coroutineScope: MonitorCoroutineScope): Builder
|
|
||||||
|
|
||||||
fun build(): MonitorComponent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
package eu.darken.capod.monitor.core
|
|
||||||
|
|
||||||
import dagger.Binds
|
|
||||||
import dagger.Module
|
|
||||||
import dagger.hilt.InstallIn
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
|
|
||||||
@InstallIn(MonitorComponent::class)
|
|
||||||
@Module()
|
|
||||||
abstract class ProcessorModule {
|
|
||||||
|
|
||||||
@Binds
|
|
||||||
@MonitorScope
|
|
||||||
abstract fun processorScope(scope: MonitorCoroutineScope): CoroutineScope
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
package eu.darken.capod.monitor.core
|
|
||||||
|
|
||||||
import javax.inject.Qualifier
|
|
||||||
|
|
||||||
@Qualifier
|
|
||||||
@MustBeDocumented
|
|
||||||
@Retention(AnnotationRetention.RUNTIME)
|
|
||||||
annotation class MonitorScope
|
|
||||||
@@ -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
|
||||||
@@ -14,6 +14,7 @@ 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.flow.replayingShare
|
import eu.darken.capod.common.flow.replayingShare
|
||||||
import eu.darken.capod.common.flow.setupCommonEventHandlers
|
import eu.darken.capod.common.flow.setupCommonEventHandlers
|
||||||
|
import eu.darken.capod.common.flow.throttleLatest
|
||||||
import eu.darken.capod.main.core.GeneralSettings
|
import eu.darken.capod.main.core.GeneralSettings
|
||||||
import eu.darken.capod.main.core.PermissionTool
|
import eu.darken.capod.main.core.PermissionTool
|
||||||
import eu.darken.capod.pods.core.PodDevice
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
@@ -113,6 +114,7 @@ class PodMonitor @Inject constructor(
|
|||||||
disableDirectCallback = useIndirectScanResultCallback,
|
disableDirectCallback = useIndirectScanResultCallback,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
.throttleLatest(1000)
|
||||||
.flatMapLatest { options ->
|
.flatMapLatest { options ->
|
||||||
val filters = when {
|
val filters = when {
|
||||||
options.showUnfiltered -> {
|
options.showUnfiltered -> {
|
||||||
|
|||||||
@@ -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)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package eu.darken.capod.pods.core.apple.airpods
|
|||||||
import eu.darken.capod.common.bluetooth.BleScanResult
|
import eu.darken.capod.common.bluetooth.BleScanResult
|
||||||
import eu.darken.capod.common.debug.logging.logTag
|
import eu.darken.capod.common.debug.logging.logTag
|
||||||
import eu.darken.capod.common.isBitSet
|
import eu.darken.capod.common.isBitSet
|
||||||
|
import eu.darken.capod.pods.core.HasChargeDetection
|
||||||
import eu.darken.capod.pods.core.HasEarDetection
|
import eu.darken.capod.pods.core.HasEarDetection
|
||||||
import eu.darken.capod.pods.core.PodDevice
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
import eu.darken.capod.pods.core.apple.ApplePods
|
import eu.darken.capod.pods.core.apple.ApplePods
|
||||||
@@ -20,23 +21,20 @@ 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, HasChargeDetection, HasAppleColor {
|
||||||
|
|
||||||
override val model: PodDevice.Model = PodDevice.Model.AIRPODS_MAX
|
override val model: PodDevice.Model = PodDevice.Model.AIRPODS_MAX
|
||||||
|
|
||||||
override val rssi: Int
|
override val rssi: Int
|
||||||
get() = rssiAverage ?: super<SingleApplePods>.rssi
|
get() = rssiAverage ?: super<SingleApplePods>.rssi
|
||||||
|
|
||||||
val isHeadphonesBeingWorn: Boolean
|
override val isHeadsetBeingCharged: Boolean
|
||||||
get() = rawStatus.isBitSet(1)
|
|
||||||
|
|
||||||
val isHeadsetBeingCharged: Boolean
|
|
||||||
get() = rawFlags.isBitSet(0)
|
get() = rawFlags.isBitSet(0)
|
||||||
|
|
||||||
override val isBeingWorn: Boolean
|
override val isBeingWorn: Boolean
|
||||||
get() = isHeadphonesBeingWorn
|
get() = rawStatus.isBitSet(5)
|
||||||
|
|
||||||
class Factory @Inject constructor() : SingleApplePodsFactory(TAG) {
|
class Factory @Inject constructor() : SingleApplePodsFactory(TAG) {
|
||||||
|
|
||||||
@@ -58,8 +56,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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -58,4 +58,6 @@
|
|||||||
<string name="pods_case_unknown_state">Невядомы стан</string>
|
<string name="pods_case_unknown_state">Невядомы стан</string>
|
||||||
<string name="last_seen_x">Апошні раз быў: %s</string>
|
<string name="last_seen_x">Апошні раз быў: %s</string>
|
||||||
<string name="first_seen_x">Упершыню быў: %s</string>
|
<string name="first_seen_x">Упершыню быў: %s</string>
|
||||||
|
<string name="permission_post_notifications_label">Адлюстраванне апавяшчэнняў</string>
|
||||||
|
<string name="permission_post_notifications_description">"Дазволіць CAPod адлюстроўваць вам апавяшчэнні аб вашых AirPods: напрыклад, іх бягучы стан пры падключэнні."</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -58,4 +58,6 @@
|
|||||||
<string name="pods_case_unknown_state">Estat desconegut</string>
|
<string name="pods_case_unknown_state">Estat desconegut</string>
|
||||||
<string name="last_seen_x">Últim cop vist: %s</string>
|
<string name="last_seen_x">Últim cop vist: %s</string>
|
||||||
<string name="first_seen_x">Primera vegada vist: %s</string>
|
<string name="first_seen_x">Primera vegada vist: %s</string>
|
||||||
|
<string name="permission_post_notifications_label">Publica notificacions</string>
|
||||||
|
<string name="permission_post_notifications_description">"Permet que el CAPod us mostri notificacions sobre els vostres AirPods, p. ex. el seu estat actual mentre estan connectats."</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -58,4 +58,6 @@
|
|||||||
<string name="pods_case_unknown_state">Neznámý stav</string>
|
<string name="pods_case_unknown_state">Neznámý stav</string>
|
||||||
<string name="last_seen_x">Naposledy spatřeno: %s</string>
|
<string name="last_seen_x">Naposledy spatřeno: %s</string>
|
||||||
<string name="first_seen_x">Poprvé spatřeno: %s</string>
|
<string name="first_seen_x">Poprvé spatřeno: %s</string>
|
||||||
|
<string name="permission_post_notifications_label">Posílat zprávy</string>
|
||||||
|
<string name="permission_post_notifications_description">"Povolte aplikaci CAPod zobrazovat oznámení o sluchátkách AirPods, např. o jejich aktuálním stavu během připojení."</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<string name="general_error_label">Fehler</string>
|
<string name="general_error_label">Fehler</string>
|
||||||
<string name="general_grant_permission_action">Berechtigung erteilen</string>
|
<string name="general_grant_permission_action">Berechtigung erteilen</string>
|
||||||
<string name="overview_nomaindevice_label">Kein primäres Gerät</string>
|
<string name="overview_nomaindevice_label">Kein primäres Gerät</string>
|
||||||
<string name="overview_nomaindevice_description">Alle gefundenen Geräte sind unwahrscheinlich ihnen zu gehören. Verbinden sie ihr Gerät oder passen sie die Einstellungen an.</string>
|
<string name="overview_nomaindevice_description">Alle gefundenen Geräte gehören wahrscheinlich nicht dir. Schalte deine Kopfhörer ein oder passe die Einstellungen von CAPod an.</string>
|
||||||
<string name="overview_bluetooth_disabled_label">Bluetooth ist deaktiviert</string>
|
<string name="overview_bluetooth_disabled_label">Bluetooth ist deaktiviert</string>
|
||||||
<string name="overview_bluetooth_disabled_description">Bluetooth ist deaktiviert, aktiviere es ;)</string>
|
<string name="overview_bluetooth_disabled_description">Bluetooth ist deaktiviert, aktiviere es ;)</string>
|
||||||
<string name="permission_bluetooth_connect_label">Bluetooth verbinden</string>
|
<string name="permission_bluetooth_connect_label">Bluetooth verbinden</string>
|
||||||
@@ -58,4 +58,6 @@
|
|||||||
<string name="pods_case_unknown_state">Unbekannter Zustand</string>
|
<string name="pods_case_unknown_state">Unbekannter Zustand</string>
|
||||||
<string name="last_seen_x">Zuletzt gesehen: %s</string>
|
<string name="last_seen_x">Zuletzt gesehen: %s</string>
|
||||||
<string name="first_seen_x">Zum ersten Mal gesehen: %s</string>
|
<string name="first_seen_x">Zum ersten Mal gesehen: %s</string>
|
||||||
|
<string name="permission_post_notifications_label">Benachrichtigungen posten</string>
|
||||||
|
<string name="permission_post_notifications_description">"Erlauben Sie CAPod, Ihnen Benachrichtigungen über Ihre AirPods anzuzeigen, z.b. ihren aktuellen Status, während sie verbunden sind."</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation">
|
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation">
|
||||||
|
<string name="app_name">CAPod</string>
|
||||||
|
<string name="app_name_pro">Tasuline CAPod</string>
|
||||||
|
<string name="app_name_foss">CAPod FOSS</string>
|
||||||
|
<string name="general_value_not_available_label">Pole saadaval</string>
|
||||||
<string name="pods_dual_left_label">Vasak klapp</string>
|
<string name="pods_dual_left_label">Vasak klapp</string>
|
||||||
<string name="pods_dual_right_label">Parem klapp</string>
|
<string name="pods_dual_right_label">Parem klapp</string>
|
||||||
|
<string name="permission_post_notifications_description">"Luba CAPodil näidata AirPodsi teateid, nt praegust olekut ühendatud olles."</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -55,4 +55,6 @@
|
|||||||
<string name="pods_case_unknown_state">Keadaan tidak diketahui</string>
|
<string name="pods_case_unknown_state">Keadaan tidak diketahui</string>
|
||||||
<string name="last_seen_x">Terakhir dilihat: %s</string>
|
<string name="last_seen_x">Terakhir dilihat: %s</string>
|
||||||
<string name="first_seen_x">Pertama dilihat: %s</string>
|
<string name="first_seen_x">Pertama dilihat: %s</string>
|
||||||
|
<string name="permission_post_notifications_label">Siarkan pemberitahuan</string>
|
||||||
|
<string name="permission_post_notifications_description">"Benarkan CAPod menunjukkan pemberitahuan kepada anda tentang AirPods anda, mis. status semasa mereka semasa disambungkan."</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -58,4 +58,6 @@
|
|||||||
<string name="pods_case_unknown_state">Onbekende staat</string>
|
<string name="pods_case_unknown_state">Onbekende staat</string>
|
||||||
<string name="last_seen_x">Laatst gezien: %s</string>
|
<string name="last_seen_x">Laatst gezien: %s</string>
|
||||||
<string name="first_seen_x">Eerst gezien: %s</string>
|
<string name="first_seen_x">Eerst gezien: %s</string>
|
||||||
|
<string name="permission_post_notifications_label">Post notificaties</string>
|
||||||
|
<string name="permission_post_notifications_description">"Sta CAPod toe notificaties over je AirPods te tonen, b.v. de huidige verbindingsstatus."</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -58,4 +58,6 @@
|
|||||||
<string name="pods_case_unknown_state">Ukjent status</string>
|
<string name="pods_case_unknown_state">Ukjent status</string>
|
||||||
<string name="last_seen_x">Sist sett: %s</string>
|
<string name="last_seen_x">Sist sett: %s</string>
|
||||||
<string name="first_seen_x">Først sett: %s</string>
|
<string name="first_seen_x">Først sett: %s</string>
|
||||||
|
<string name="permission_post_notifications_label">Vis varsler</string>
|
||||||
|
<string name="permission_post_notifications_description">"La CAPod vise varsler om dine AirPods, f.eks. nåværende status under tilkobling."</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -58,4 +58,6 @@
|
|||||||
<string name="pods_case_unknown_state">Nieznany stan</string>
|
<string name="pods_case_unknown_state">Nieznany stan</string>
|
||||||
<string name="last_seen_x">Ostatnio widziane: %s</string>
|
<string name="last_seen_x">Ostatnio widziane: %s</string>
|
||||||
<string name="first_seen_x">Widziane pierwszy raz: %s</string>
|
<string name="first_seen_x">Widziane pierwszy raz: %s</string>
|
||||||
|
<string name="permission_post_notifications_label">Wysyłanie powiadomień</string>
|
||||||
|
<string name="permission_post_notifications_description">"Zezwól CAPod na wyświetlanie powiadomień o słuchawkach AirPods, np. ich aktualnym statusie podczas połączenia."</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation">
|
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation">
|
||||||
<string name="app_name">CAPod</string>
|
<string name="app_name">CAPod</string>
|
||||||
<string name="app_name_pro">CAPod Pro</string>
|
<string name="app_name_pro">CAPod Pro</string>
|
||||||
|
<string name="app_name_foss">Fóssil CAPod</string>
|
||||||
|
<string name="general_value_not_available_label">N/A</string>
|
||||||
<string name="general_error_label">Erro</string>
|
<string name="general_error_label">Erro</string>
|
||||||
<string name="general_grant_permission_action">Conceder permissão</string>
|
<string name="general_grant_permission_action">Conceder permissão</string>
|
||||||
<string name="overview_nomaindevice_label">Nenhum dispositivo principal</string>
|
<string name="overview_nomaindevice_label">Nenhum dispositivo principal</string>
|
||||||
@@ -56,4 +58,6 @@
|
|||||||
<string name="pods_case_unknown_state">Estado desconhecido</string>
|
<string name="pods_case_unknown_state">Estado desconhecido</string>
|
||||||
<string name="last_seen_x">Visto pela última vez: %s</string>
|
<string name="last_seen_x">Visto pela última vez: %s</string>
|
||||||
<string name="first_seen_x">Visto pela primeira vez: %s</string>
|
<string name="first_seen_x">Visto pela primeira vez: %s</string>
|
||||||
|
<string name="permission_post_notifications_label">Post de notificações</string>
|
||||||
|
<string name="permission_post_notifications_description">"Permita que o CAPod mostre notificações sobre seus AirPods, por exemplo. seu status atual enquanto conectado."</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1,2 +1,61 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation"></resources>
|
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation">
|
||||||
|
<string name="app_name">CAPod</string>
|
||||||
|
<string name="app_name_pro">CAPod Pro</string>
|
||||||
|
<string name="app_name_foss">CAPod FOSS</string>
|
||||||
|
<string name="general_value_not_available_label">N/A</string>
|
||||||
|
<string name="general_error_label">Erro</string>
|
||||||
|
<string name="general_grant_permission_action">Conceder permissão</string>
|
||||||
|
<string name="overview_nomaindevice_label">Sem dispositivo primário</string>
|
||||||
|
<string name="overview_nomaindevice_description">É improvável que os dispositivos detetados sejam seus. Ligue e conecte o seu dispositivo ou ajuste as definições.</string>
|
||||||
|
<string name="overview_bluetooth_disabled_label">O Bluetooth está desligado</string>
|
||||||
|
<string name="overview_bluetooth_disabled_description">O Bluetooth está desligado, ativa-o ;)</string>
|
||||||
|
<string name="permission_bluetooth_connect_label">Conexão ao Bluetooth</string>
|
||||||
|
<string name="permission_bluetooth_connect_description">Esta aplicação requer permissão para conectar com o Bluetooth para interagir com dispositivos emparelhados e iniciar conexões.</string>
|
||||||
|
<string name="permission_bluetooth_scan_label">Explorar Bluetooth</string>
|
||||||
|
<string name="permission_bluetooth_scan_description">A permissão para explorar o Bluetooth autoriza esta aplicação para descobrir e detetar dados de Bluetooth de dispositivos por perto como os seus AirPods.</string>
|
||||||
|
<string name="permission_bluetooth_label">Bluetooth</string>
|
||||||
|
<string name="permission_bluetooth_description">Esta aplicação requer a permissão do Bluetooth para conectar e emparelhar a dispositivos bluetooth.</string>
|
||||||
|
<string name="permission_access_fine_location_label">Aceder a localização exata</string>
|
||||||
|
<string name="permission_access_fine_location_description">CAPod utiliza a localização exata para receber dados de \'Bluetooth Low Energy\'. Os seus headphones utilizam tecnologia \'Bluetooth Low Energy\' para transmitir o seu estado. Esta aplicação NÃO irá utilizar dados de Bluetooth para determinar a sua localização.</string>
|
||||||
|
<string name="permission_background_location_label">Acesso à localização de fundo</string>
|
||||||
|
<string name="permission_background_location_description">CAPod utiliza o acesso à localização de fundo para habilitar funcionalidades como \"Mostrar pop-up\" e \"AutoConectar\" enquanto a aplicação está fechada. Acesso à localização de fundo autoriza à aplicação receber dados de \'Bluetooth Low Energy\' enquanto está de fundo. Esta aplicação NÃO irá utilizar dados de Bluetooth para determinar a sua localização.</string>
|
||||||
|
<string name="permission_ignore_battery_optimizations_label">Desativar otimizações de bateria</string>
|
||||||
|
<string name="permission_ignore_battery_optimizations_description">Otimizações de bateria impedem que esta aplicação receba dados de Bluetooth fidedignos enquanto a aplicação está de fundo.</string>
|
||||||
|
<string name="permission_required_title">A seguinte permissão é necessária:</string>
|
||||||
|
<string name="permission_system_alert_window_label">Janela de Alerta do Sistema</string>
|
||||||
|
<string name="permission_system_alert_window_description">Autorizar CAPod para aparecer por cima de outras aplicações para tornar possível a funcionalidade \"Mostrar pop-up\".</string>
|
||||||
|
<string name="settings_scanner_mode_lowpower_label">Poupar bateria</string>
|
||||||
|
<string name="settings_scanner_mode_balanced_label">Equilibrado</string>
|
||||||
|
<string name="settings_scanner_mode_lowlatency_label">Baixa latência</string>
|
||||||
|
<string name="settings_monitor_mode_manual_label">Quando a aplicação está aberta</string>
|
||||||
|
<string name="settings_monitor_mode_automatic_label">Quando o dispositivo é conectado</string>
|
||||||
|
<string name="settings_monitor_mode_always_label">Sempre</string>
|
||||||
|
<string name="settings_reaction_autoconnect_whenseen_label">Quando visto</string>
|
||||||
|
<string name="settings_reaction_autoconnect_caseopen_label">Caixa está aberta</string>
|
||||||
|
<string name="settings_reaction_autoconnect_inear_label">No ouvido</string>
|
||||||
|
<string name="pods_dual_left_label">Pod esquerdo</string>
|
||||||
|
<string name="pods_dual_right_label">Pod direito</string>
|
||||||
|
<string name="pods_case_label">Caixa</string>
|
||||||
|
<string name="pods_case_status_open_label">Aberta</string>
|
||||||
|
<string name="pods_case_status_closed_label">Fechada</string>
|
||||||
|
<string name="pods_connection_state_disconnected_label">Não está conectado a um dispositivo</string>
|
||||||
|
<string name="pods_connection_state_idle_label">Conectado a um dispositivo, mas inativo</string>
|
||||||
|
<string name="pods_connection_state_music_label">Em modo de música</string>
|
||||||
|
<string name="pods_connection_state_call_label">Em modo de chamada</string>
|
||||||
|
<string name="pods_connection_state_ringing_label">A tocar</string>
|
||||||
|
<string name="pods_connection_state_hanging_up_label">A terminar a chamada</string>
|
||||||
|
<string name="pods_connection_state_unknown_label">Estado de conexão desconhecido</string>
|
||||||
|
<string name="pods_unknown_raw_data_label">Dados não tratados</string>
|
||||||
|
<string name="pods_unknown_label">Dispositivo desconhecido</string>
|
||||||
|
<string name="pods_unknown_contact_dev">Este é um dispositivo desconhecido, mas usa um formato de mensagem semelhante. Vamos adicionar suporte para isso, entre em contacto comigo :)</string>
|
||||||
|
<string name="pods_none_label_short">Sem dispositivo</string>
|
||||||
|
<string name="pods_charging_label">A carregar</string>
|
||||||
|
<string name="pods_inear_label">No ouvido</string>
|
||||||
|
<string name="pods_microphone_label">Microfone</string>
|
||||||
|
<string name="pods_yours">Seu</string>
|
||||||
|
<string name="headset_being_worn_label">A ser usado</string>
|
||||||
|
<string name="pods_case_unknown_state">Estado desconhecido</string>
|
||||||
|
<string name="last_seen_x">Última vez visto: %s</string>
|
||||||
|
<string name="first_seen_x">Primeira vez visto: %s</string>
|
||||||
|
</resources>
|
||||||
|
|||||||
@@ -58,4 +58,5 @@
|
|||||||
<string name="pods_case_unknown_state">Неизвестное состояние</string>
|
<string name="pods_case_unknown_state">Неизвестное состояние</string>
|
||||||
<string name="last_seen_x">Последние: %s</string>
|
<string name="last_seen_x">Последние: %s</string>
|
||||||
<string name="first_seen_x">Первые: %s</string>
|
<string name="first_seen_x">Первые: %s</string>
|
||||||
|
<string name="permission_post_notifications_description">"Разрешить CAPod показывать уведомления о ваших AirPods, например текущий статус при подключении."</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -58,4 +58,6 @@
|
|||||||
<string name="pods_case_unknown_state">Neznámy stav</string>
|
<string name="pods_case_unknown_state">Neznámy stav</string>
|
||||||
<string name="last_seen_x">Naposledy videné: %s</string>
|
<string name="last_seen_x">Naposledy videné: %s</string>
|
||||||
<string name="first_seen_x">Prvýkrát videné: %s</string>
|
<string name="first_seen_x">Prvýkrát videné: %s</string>
|
||||||
|
<string name="permission_post_notifications_label">Zobraziť oznámenia</string>
|
||||||
|
<string name="permission_post_notifications_description">"Povoľte CAPod, aby vám zobrazoval upozornenia týkajúce sa vašich slúchadiel AirPods, napr. ich aktuálny stav počas pripojenia."</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation">
|
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation">
|
||||||
|
<string name="app_name">CAPod</string>
|
||||||
|
<string name="app_name_pro">CAPod Pro</string>
|
||||||
|
<string name="app_name_foss">CAPod FOSS</string>
|
||||||
<string name="general_value_not_available_label">Yok</string>
|
<string name="general_value_not_available_label">Yok</string>
|
||||||
<string name="general_error_label">Hata</string>
|
<string name="general_error_label">Hata</string>
|
||||||
<string name="general_grant_permission_action">İzin ver</string>
|
<string name="general_grant_permission_action">İzin ver</string>
|
||||||
@@ -11,6 +14,7 @@
|
|||||||
<string name="permission_bluetooth_connect_description">Bu uygulama, eşleştirilmiş cihazlarla etkileşim kurmak ve bağlantıları başlatmak için \"Bluetooth bağlantısı\" izni gerektirir.</string>
|
<string name="permission_bluetooth_connect_description">Bu uygulama, eşleştirilmiş cihazlarla etkileşim kurmak ve bağlantıları başlatmak için \"Bluetooth bağlantısı\" izni gerektirir.</string>
|
||||||
<string name="permission_bluetooth_scan_label">Bluetooth taraması</string>
|
<string name="permission_bluetooth_scan_label">Bluetooth taraması</string>
|
||||||
<string name="permission_bluetooth_scan_description">\"Bluetooth tarama\" izni, bu uygulamanın AirPods\'unuz gibi yakındaki cihazlardan Bluetooth verilerini keşfetmesine ve almasına izin verir.</string>
|
<string name="permission_bluetooth_scan_description">\"Bluetooth tarama\" izni, bu uygulamanın AirPods\'unuz gibi yakındaki cihazlardan Bluetooth verilerini keşfetmesine ve almasına izin verir.</string>
|
||||||
|
<string name="permission_bluetooth_label">Bluetooth</string>
|
||||||
<string name="permission_bluetooth_description">Bu uygulama, eşleştirilmiş bluetooth cihazlarına bağlanmak için \"Bluetooth\" izni gerektirir.</string>
|
<string name="permission_bluetooth_description">Bu uygulama, eşleştirilmiş bluetooth cihazlarına bağlanmak için \"Bluetooth\" izni gerektirir.</string>
|
||||||
<string name="permission_access_fine_location_label">Hassas konuma erişin</string>
|
<string name="permission_access_fine_location_label">Hassas konuma erişin</string>
|
||||||
<string name="permission_access_fine_location_description">CAPod, Bluetooth Düşük Enerji verilerini almak için \"iyi konum\" iznini kullanır. Kulaklıklarınız durumlarını yayınlamak için Bluetooth Düşük Enerji teknolojisini kullanır. Bu uygulama konumunuzu belirlemek için Bluetooth verilerini KULLANMAYACAKTIR.</string>
|
<string name="permission_access_fine_location_description">CAPod, Bluetooth Düşük Enerji verilerini almak için \"iyi konum\" iznini kullanır. Kulaklıklarınız durumlarını yayınlamak için Bluetooth Düşük Enerji teknolojisini kullanır. Bu uygulama konumunuzu belirlemek için Bluetooth verilerini KULLANMAYACAKTIR.</string>
|
||||||
|
|||||||
@@ -3,11 +3,11 @@
|
|||||||
<string name="app_name">CAPod</string>
|
<string name="app_name">CAPod</string>
|
||||||
<string name="app_name_pro">CAPod Pro</string>
|
<string name="app_name_pro">CAPod Pro</string>
|
||||||
<string name="app_name_foss">CAPod FOSS</string>
|
<string name="app_name_foss">CAPod FOSS</string>
|
||||||
<string name="general_value_not_available_label">不可用</string>
|
<string name="general_value_not_available_label">不適用</string>
|
||||||
<string name="general_error_label">錯誤</string>
|
<string name="general_error_label">錯誤</string>
|
||||||
<string name="general_grant_permission_action">授予權限</string>
|
<string name="general_grant_permission_action">授予權限</string>
|
||||||
<string name="overview_nomaindevice_label">無主要裝置</string>
|
<string name="overview_nomaindevice_label">無主要裝置</string>
|
||||||
<string name="overview_nomaindevice_description">所有偵測到的裝置似乎並不是您的。開啟並連線您的裝置,也可以調整設定。</string>
|
<string name="overview_nomaindevice_description">所有偵測到的裝置似乎並不是您的。開啟並連線至您的裝置,也可以調整設定。</string>
|
||||||
<string name="overview_bluetooth_disabled_label">藍牙已停用</string>
|
<string name="overview_bluetooth_disabled_label">藍牙已停用</string>
|
||||||
<string name="overview_bluetooth_disabled_description">藍牙已停用,啟用它 ;)</string>
|
<string name="overview_bluetooth_disabled_description">藍牙已停用,啟用它 ;)</string>
|
||||||
<string name="permission_bluetooth_connect_label">藍牙連線</string>
|
<string name="permission_bluetooth_connect_label">藍牙連線</string>
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
<string name="permission_required_title">可能要求下列權限:</string>
|
<string name="permission_required_title">可能要求下列權限:</string>
|
||||||
<string name="permission_system_alert_window_label">系統警報視窗</string>
|
<string name="permission_system_alert_window_label">系統警報視窗</string>
|
||||||
<string name="permission_system_alert_window_description">允許 CAPod 在其他應用程式上繪圖,使「顯示彈出式視窗」功能成為可能。</string>
|
<string name="permission_system_alert_window_description">允許 CAPod 在其他應用程式上繪圖,使「顯示彈出式視窗」功能成為可能。</string>
|
||||||
<string name="settings_scanner_mode_lowpower_label">節電</string>
|
<string name="settings_scanner_mode_lowpower_label">低電源</string>
|
||||||
<string name="settings_scanner_mode_balanced_label">平衡</string>
|
<string name="settings_scanner_mode_balanced_label">平衡</string>
|
||||||
<string name="settings_scanner_mode_lowlatency_label">低延遲</string>
|
<string name="settings_scanner_mode_lowlatency_label">低延遲</string>
|
||||||
<string name="settings_monitor_mode_manual_label">應用程式開啟時</string>
|
<string name="settings_monitor_mode_manual_label">應用程式開啟時</string>
|
||||||
@@ -58,4 +58,6 @@
|
|||||||
<string name="pods_case_unknown_state">未知狀態</string>
|
<string name="pods_case_unknown_state">未知狀態</string>
|
||||||
<string name="last_seen_x">最後連線:%s</string>
|
<string name="last_seen_x">最後連線:%s</string>
|
||||||
<string name="first_seen_x">首次連線:%s</string>
|
<string name="first_seen_x">首次連線:%s</string>
|
||||||
|
<string name="permission_post_notifications_label">張貼通知</string>
|
||||||
|
<string name="permission_post_notifications_description">"允許 CAPod 為您顯示關於您 AirPods 的通知,例如在連線時顯示它們的目前狀態。"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package eu.darken.capod.pods.core.apple
|
package eu.darken.capod.pods.core.apple
|
||||||
|
|
||||||
import eu.darken.capod.pods.core.apple.airpods.AirPodsMax
|
|
||||||
import io.kotest.matchers.shouldBe
|
import io.kotest.matchers.shouldBe
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
@@ -21,15 +20,4 @@ class SingleApplePodsTest : BaseAirPodsTest() {
|
|||||||
rawSuffix shouldBe 0x40.toUByte()
|
rawSuffix shouldBe 0x40.toUByte()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `test values based on AirPodMax`() = runTest {
|
|
||||||
create<AirPodsMax>("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") {
|
|
||||||
batteryHeadsetPercent shouldBe 0.5f
|
|
||||||
|
|
||||||
isHeadsetBeingCharged shouldBe false
|
|
||||||
|
|
||||||
isHeadphonesBeingWorn shouldBe true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package eu.darken.capod.pods.core.apple.airpods
|
package eu.darken.capod.pods.core.apple.airpods
|
||||||
|
|
||||||
|
import eu.darken.capod.common.isBitSet
|
||||||
import eu.darken.capod.pods.core.PodDevice
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
import eu.darken.capod.pods.core.apple.BaseAirPodsTest
|
import eu.darken.capod.pods.core.apple.BaseAirPodsTest
|
||||||
import eu.darken.capod.pods.core.apple.HasAppleColor
|
import eu.darken.capod.pods.core.apple.HasAppleColor
|
||||||
@@ -27,8 +28,6 @@ class AirPodsMaxTest : BaseAirPodsTest() {
|
|||||||
|
|
||||||
isHeadsetBeingCharged shouldBe false
|
isHeadsetBeingCharged shouldBe false
|
||||||
|
|
||||||
isHeadphonesBeingWorn shouldBe true
|
|
||||||
|
|
||||||
model shouldBe PodDevice.Model.AIRPODS_MAX
|
model shouldBe PodDevice.Model.AIRPODS_MAX
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -50,8 +49,6 @@ class AirPodsMaxTest : BaseAirPodsTest() {
|
|||||||
batteryHeadsetPercent shouldBe 0.5f
|
batteryHeadsetPercent shouldBe 0.5f
|
||||||
|
|
||||||
isHeadsetBeingCharged shouldBe false
|
isHeadsetBeingCharged shouldBe false
|
||||||
|
|
||||||
isHeadphonesBeingWorn shouldBe true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,9 +69,25 @@ class AirPodsMaxTest : BaseAirPodsTest() {
|
|||||||
|
|
||||||
isHeadsetBeingCharged shouldBe false
|
isHeadsetBeingCharged shouldBe false
|
||||||
|
|
||||||
isHeadphonesBeingWorn shouldBe true
|
rawStatus.isBitSet(5) shouldBe true
|
||||||
|
|
||||||
podStyle shouldBe HasAppleColor.DeviceColor.BLUE
|
podStyle shouldBe HasAppleColor.DeviceColor.BLUE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `wear status`() = runTest {
|
||||||
|
create<AirPodsMax>("07 19 01 0A 20 03 07 80 03 03 65 1F 28 32 D0 D9 71 43 00 9A 40 E7 6B EA 6C 2C FB") {
|
||||||
|
rawStatus shouldBe 0x03.toUByte()
|
||||||
|
|
||||||
|
rawStatus.isBitSet(5) shouldBe false
|
||||||
|
isBeingWorn shouldBe false
|
||||||
|
}
|
||||||
|
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") {
|
||||||
|
rawStatus shouldBe 0x23.toUByte()
|
||||||
|
|
||||||
|
rawStatus.isBitSet(5) shouldBe true
|
||||||
|
isBeingWorn shouldBe true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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")
|
||||||
}
|
}
|
||||||
@@ -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()
|
||||||
|
|
||||||
|
|||||||
@@ -6,8 +6,6 @@ import androidx.work.CoroutineWorker
|
|||||||
import androidx.work.WorkerParameters
|
import androidx.work.WorkerParameters
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import dagger.hilt.EntryPoints
|
|
||||||
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.Bugs
|
import eu.darken.capod.common.debug.Bugs
|
||||||
import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE
|
import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE
|
||||||
@@ -15,9 +13,7 @@ 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
|
||||||
import eu.darken.capod.common.debug.logging.logTag
|
import eu.darken.capod.common.debug.logging.logTag
|
||||||
import eu.darken.capod.common.flow.setupCommonEventHandlers
|
import eu.darken.capod.common.flow.setupCommonEventHandlers
|
||||||
import eu.darken.capod.main.core.GeneralSettings
|
|
||||||
import eu.darken.capod.main.core.PermissionTool
|
import eu.darken.capod.main.core.PermissionTool
|
||||||
import eu.darken.capod.monitor.core.MonitorComponent
|
|
||||||
import eu.darken.capod.monitor.core.MonitorCoroutineScope
|
import eu.darken.capod.monitor.core.MonitorCoroutineScope
|
||||||
import eu.darken.capod.monitor.core.PodMonitor
|
import eu.darken.capod.monitor.core.PodMonitor
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
@@ -31,22 +27,12 @@ import kotlinx.coroutines.flow.take
|
|||||||
class MonitorWorker @AssistedInject constructor(
|
class MonitorWorker @AssistedInject constructor(
|
||||||
@Assisted private val context: Context,
|
@Assisted private val context: Context,
|
||||||
@Assisted private val params: WorkerParameters,
|
@Assisted private val params: WorkerParameters,
|
||||||
monitorComponentBuilder: MonitorComponent.Builder,
|
|
||||||
private val dispatcherProvider: DispatcherProvider,
|
private val dispatcherProvider: DispatcherProvider,
|
||||||
private val generalSettings: GeneralSettings,
|
|
||||||
private val permissionTool: PermissionTool,
|
private val permissionTool: PermissionTool,
|
||||||
private val podMonitor: PodMonitor,
|
private val podMonitor: PodMonitor,
|
||||||
private val bluetoothManager: BluetoothManager2,
|
|
||||||
) : CoroutineWorker(context, params) {
|
) : CoroutineWorker(context, params) {
|
||||||
|
|
||||||
private val workerScope = MonitorCoroutineScope()
|
private val workerScope = MonitorCoroutineScope()
|
||||||
private val monitorComponent = monitorComponentBuilder
|
|
||||||
.coroutineScope(workerScope)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
private val entryPoint by lazy {
|
|
||||||
EntryPoints.get(monitorComponent, MonitorWorkerEntryPoint::class.java)
|
|
||||||
}
|
|
||||||
|
|
||||||
private var finishedWithError = false
|
private var finishedWithError = false
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
package eu.darken.capod.wear.core
|
|
||||||
|
|
||||||
import dagger.hilt.EntryPoint
|
|
||||||
import dagger.hilt.InstallIn
|
|
||||||
import eu.darken.capod.monitor.core.MonitorComponent
|
|
||||||
|
|
||||||
@InstallIn(MonitorComponent::class)
|
|
||||||
@EntryPoint
|
|
||||||
interface MonitorWorkerEntryPoint
|
|
||||||
@@ -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
|
||||||
@@ -13,13 +13,9 @@ import eu.darken.capod.common.flow.throttleLatest
|
|||||||
import eu.darken.capod.common.livedata.SingleLiveEvent
|
import eu.darken.capod.common.livedata.SingleLiveEvent
|
||||||
import eu.darken.capod.common.permissions.Permission
|
import eu.darken.capod.common.permissions.Permission
|
||||||
import eu.darken.capod.common.uix.ViewModel3
|
import eu.darken.capod.common.uix.ViewModel3
|
||||||
import eu.darken.capod.main.core.GeneralSettings
|
|
||||||
import eu.darken.capod.main.core.PermissionTool
|
import eu.darken.capod.main.core.PermissionTool
|
||||||
import eu.darken.capod.monitor.core.PodDeviceCache
|
|
||||||
import eu.darken.capod.monitor.core.PodMonitor
|
import eu.darken.capod.monitor.core.PodMonitor
|
||||||
import eu.darken.capod.pods.core.DualPodDevice
|
import eu.darken.capod.pods.core.DualPodDevice
|
||||||
import eu.darken.capod.pods.core.PodDevice
|
|
||||||
import eu.darken.capod.pods.core.PodFactory
|
|
||||||
import eu.darken.capod.pods.core.SinglePodDevice
|
import eu.darken.capod.pods.core.SinglePodDevice
|
||||||
import eu.darken.capod.wear.ui.overview.cards.BluetoothDisabledVH
|
import eu.darken.capod.wear.ui.overview.cards.BluetoothDisabledVH
|
||||||
import eu.darken.capod.wear.ui.overview.cards.MissingMainDeviceVH
|
import eu.darken.capod.wear.ui.overview.cards.MissingMainDeviceVH
|
||||||
@@ -38,11 +34,8 @@ class OverviewFragmentVM @Inject constructor(
|
|||||||
dispatcherProvider: DispatcherProvider,
|
dispatcherProvider: DispatcherProvider,
|
||||||
private val podMonitor: PodMonitor,
|
private val podMonitor: PodMonitor,
|
||||||
private val permissionTool: PermissionTool,
|
private val permissionTool: PermissionTool,
|
||||||
private val generalSettings: GeneralSettings,
|
|
||||||
debugSettings: DebugSettings,
|
debugSettings: DebugSettings,
|
||||||
private val bluetoothManager: BluetoothManager2,
|
private val bluetoothManager: BluetoothManager2,
|
||||||
private val podDeviceCache: PodDeviceCache,
|
|
||||||
private val podFactory: PodFactory
|
|
||||||
) : ViewModel3(dispatcherProvider = dispatcherProvider) {
|
) : ViewModel3(dispatcherProvider = dispatcherProvider) {
|
||||||
|
|
||||||
private val updateTicker = channelFlow<Unit> {
|
private val updateTicker = channelFlow<Unit> {
|
||||||
@@ -54,30 +47,14 @@ class OverviewFragmentVM @Inject constructor(
|
|||||||
|
|
||||||
val requestPermissionEvent = SingleLiveEvent<Permission>()
|
val requestPermissionEvent = SingleLiveEvent<Permission>()
|
||||||
|
|
||||||
private val pods: Flow<List<PodDevice>> = permissionTool.missingPermissions
|
private val mainDevice = podMonitor.mainDevice.throttleLatest(1000)
|
||||||
.flatMapLatest { permissions ->
|
|
||||||
if (permissions.isNotEmpty()) {
|
|
||||||
return@flatMapLatest flowOf(emptyList())
|
|
||||||
}
|
|
||||||
|
|
||||||
generalSettings.showAll.flow.flatMapLatest { showAll ->
|
|
||||||
if (showAll) {
|
|
||||||
podMonitor.devices
|
|
||||||
} else {
|
|
||||||
podMonitor.mainDevice.map { mainDevice ->
|
|
||||||
mainDevice?.let { listOf(it) } ?: emptyList()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.catch { errorEvents.postValue(it) }
|
|
||||||
|
|
||||||
val listItems: LiveData<List<OverviewAdapter.Item>> = combine(
|
val listItems: LiveData<List<OverviewAdapter.Item>> = combine(
|
||||||
updateTicker,
|
updateTicker,
|
||||||
permissionTool.missingPermissions,
|
permissionTool.missingPermissions,
|
||||||
debugSettings.isDebugModeEnabled.flow,
|
debugSettings.isDebugModeEnabled.flow,
|
||||||
bluetoothManager.isBluetoothEnabled,
|
bluetoothManager.isBluetoothEnabled,
|
||||||
podMonitor.mainDevice.throttleLatest(1000),
|
mainDevice,
|
||||||
) { _, permissions, isDebugMode, isBluetoothEnabled, _ ->
|
) { _, permissions, isDebugMode, isBluetoothEnabled, _ ->
|
||||||
val items = mutableListOf<OverviewAdapter.Item>()
|
val items = mutableListOf<OverviewAdapter.Item>()
|
||||||
|
|
||||||
|
|||||||
@@ -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,9 @@ dependencies {
|
|||||||
|
|
||||||
addTesting()
|
addTesting()
|
||||||
|
|
||||||
implementation("com.bugsnag:bugsnag-android:5.9.2")
|
"gplayImplementation"("com.android.billingclient:billing:5.1.0")
|
||||||
implementation("com.getkeepsafe.relinker:relinker:1.4.3")
|
"gplayImplementation"("com.android.billingclient:billing-ktx:5.1.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")
|
||||||
}
|
}
|
||||||
@@ -1,2 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources></resources>
|
<resources>
|
||||||
|
<string name="foss_upgrade_donate_label">Doar</string>
|
||||||
|
<string name="foss_upgrade_alreadydonated_label">Já doei</string>
|
||||||
|
<string name="foss_upgrade_no_money_label">Gastei todo o meu dinheiro em AirPods</string>
|
||||||
|
</resources>
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
package eu.darken.capod.common.upgrade.core
|
|
||||||
|
|
||||||
import com.android.billingclient.api.Purchase
|
|
||||||
import eu.darken.capod.common.debug.logging.logTag
|
|
||||||
import eu.darken.capod.common.upgrade.core.data.PurchasedSku
|
|
||||||
import eu.darken.capod.common.upgrade.core.data.Sku
|
|
||||||
|
|
||||||
|
|
||||||
fun Purchase.toPurchasedSku(): Collection<PurchasedSku> = skus.map {
|
|
||||||
PurchasedSku(Sku(it), this)
|
|
||||||
}
|
|
||||||
|
|
||||||
private val TAG: String = logTag("Upgrade", "Gplay", "Billing", "Extensions")
|
|
||||||
@@ -49,7 +49,7 @@ class UpgradeRepoGplay @Inject constructor(
|
|||||||
lastProStateAt = now
|
lastProStateAt = now
|
||||||
Info(billingData = data)
|
Info(billingData = data)
|
||||||
}
|
}
|
||||||
(now - lastProStateAt) < 6 * 60 * 1000L -> { // 6 hours
|
(now - lastProStateAt) < 6 * 60 * 60 * 1000L -> { // 6 hours
|
||||||
log(TAG, VERBOSE) { "We are not pro, but were recently, did GPlay try annoy us again?" }
|
log(TAG, VERBOSE) { "We are not pro, but were recently, did GPlay try annoy us again?" }
|
||||||
Info(gracePeriod = true, billingData = null)
|
Info(gracePeriod = true, billingData = null)
|
||||||
}
|
}
|
||||||
@@ -62,7 +62,7 @@ class UpgradeRepoGplay @Inject constructor(
|
|||||||
// Ignore Google Play errors if the last pro state was recent
|
// Ignore Google Play errors if the last pro state was recent
|
||||||
val now = System.currentTimeMillis()
|
val now = System.currentTimeMillis()
|
||||||
log(TAG) { "now=$now, lastProStateAt=$lastProStateAt, error=$it" }
|
log(TAG) { "now=$now, lastProStateAt=$lastProStateAt, error=$it" }
|
||||||
if ((now - lastProStateAt) < 6 * 60 * 60 * 1000L) { // 6 hours
|
if ((now - lastProStateAt) < 24 * 60 * 60 * 1000L) { // 24 hours
|
||||||
log(TAG, VERBOSE) { "We are not pro, but were recently, and just and an error, what is GPlay doing???" }
|
log(TAG, VERBOSE) { "We are not pro, but were recently, and just and an error, what is GPlay doing???" }
|
||||||
emit(Info(gracePeriod = true, billingData = null))
|
emit(Info(gracePeriod = true, billingData = null))
|
||||||
} else {
|
} else {
|
||||||
@@ -123,7 +123,7 @@ class UpgradeRepoGplay @Inject constructor(
|
|||||||
get() = UpgradeRepo.Type.GPLAY
|
get() = UpgradeRepo.Type.GPLAY
|
||||||
|
|
||||||
override val isPro: Boolean
|
override val isPro: Boolean
|
||||||
get() = billingData?.getProSku() != null
|
get() = billingData?.getProSku() != null || gracePeriod
|
||||||
|
|
||||||
override val upgradedAt: Instant?
|
override val upgradedAt: Instant?
|
||||||
get() = billingData
|
get() = billingData
|
||||||
|
|||||||
+25
-21
@@ -2,6 +2,7 @@ package eu.darken.capod.common.upgrade.core.client
|
|||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import com.android.billingclient.api.*
|
import com.android.billingclient.api.*
|
||||||
|
import eu.darken.capod.common.debug.logging.Logging.Priority.INFO
|
||||||
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
|
||||||
import eu.darken.capod.common.debug.logging.logTag
|
import eu.darken.capod.common.debug.logging.logTag
|
||||||
@@ -38,7 +39,7 @@ data class BillingClientConnection(
|
|||||||
|
|
||||||
if (!result.isSuccess) {
|
if (!result.isSuccess) {
|
||||||
log(TAG, WARN) { "queryPurchases() failed" }
|
log(TAG, WARN) { "queryPurchases() failed" }
|
||||||
throw BillingClientException(result)
|
throw BillingResultException(result)
|
||||||
} else {
|
} else {
|
||||||
requireNotNull(purchases)
|
requireNotNull(purchases)
|
||||||
}
|
}
|
||||||
@@ -47,34 +48,31 @@ data class BillingClientConnection(
|
|||||||
return purchases
|
return purchases
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun acknowledgePurchase(purchase: Purchase): BillingResult {
|
suspend fun acknowledgePurchase(purchase: Purchase) {
|
||||||
val ack = AcknowledgePurchaseParams.newBuilder().apply {
|
val ack = AcknowledgePurchaseParams.newBuilder().apply {
|
||||||
setPurchaseToken(purchase.purchaseToken)
|
setPurchaseToken(purchase.purchaseToken)
|
||||||
}.build()
|
}.build()
|
||||||
|
|
||||||
val ackResult = suspendCoroutine<BillingResult> { continuation ->
|
val result = suspendCoroutine<BillingResult> { continuation ->
|
||||||
client.acknowledgePurchase(ack) { continuation.resume(it) }
|
client.acknowledgePurchase(ack) { continuation.resume(it) }
|
||||||
}
|
}
|
||||||
log(TAG) {
|
|
||||||
"acknowledgePurchase(purchase=$purchase): code=${ackResult.responseCode}, message=${ackResult.debugMessage})"
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!ackResult.isSuccess) {
|
log(TAG, INFO) { "acknowledgePurchase($purchase): code=${result.responseCode} (${result.debugMessage})" }
|
||||||
throw BillingClientException(ackResult)
|
|
||||||
}
|
|
||||||
|
|
||||||
return ackResult
|
if (!result.isSuccess) throw BillingResultException(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun querySku(sku: Sku): Sku.Details {
|
suspend fun querySku(sku: Sku): Sku.Details {
|
||||||
val skuParams = SkuDetailsParams.newBuilder().apply {
|
val productDetails = QueryProductDetailsParams.Product.newBuilder().apply {
|
||||||
setType(BillingClient.SkuType.INAPP)
|
setProductType(BillingClient.ProductType.INAPP)
|
||||||
setSkusList(listOf(sku.id))
|
setProductId(sku.id)
|
||||||
}.build()
|
}.build()
|
||||||
|
|
||||||
val (result, details) = suspendCoroutine<Pair<BillingResult, Collection<SkuDetails>?>> { continuation ->
|
val params = QueryProductDetailsParams.newBuilder().setProductList(listOf(productDetails)).build()
|
||||||
client.querySkuDetailsAsync(skuParams) { skuResult, skuDetails ->
|
|
||||||
continuation.resume(skuResult to skuDetails)
|
val (result, details) = suspendCoroutine<Pair<BillingResult, Collection<ProductDetails>?>> { continuation ->
|
||||||
|
client.queryProductDetailsAsync(params) { result, skuDetails ->
|
||||||
|
continuation.resume(result to skuDetails)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,7 +80,7 @@ data class BillingClientConnection(
|
|||||||
"querySku(sku=$sku): code=${result.responseCode}, debug=${result.debugMessage}), skuDetails=$details"
|
"querySku(sku=$sku): code=${result.responseCode}, debug=${result.debugMessage}), skuDetails=$details"
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!result.isSuccess) throw BillingClientException(result)
|
if (!result.isSuccess) throw BillingResultException(result)
|
||||||
|
|
||||||
if (details.isNullOrEmpty()) throw IllegalStateException("Unknown SKU, no details available.")
|
if (details.isNullOrEmpty()) throw IllegalStateException("Unknown SKU, no details available.")
|
||||||
|
|
||||||
@@ -97,10 +95,16 @@ data class BillingClientConnection(
|
|||||||
|
|
||||||
suspend fun launchBillingFlow(activity: Activity, skuDetails: Sku.Details): BillingResult {
|
suspend fun launchBillingFlow(activity: Activity, skuDetails: Sku.Details): BillingResult {
|
||||||
log(TAG) { "launchBillingFlow(activity=$activity, skuDetails=$skuDetails)" }
|
log(TAG) { "launchBillingFlow(activity=$activity, skuDetails=$skuDetails)" }
|
||||||
return client.launchBillingFlow(
|
|
||||||
activity,
|
val productParams = BillingFlowParams.ProductDetailsParams.newBuilder().apply {
|
||||||
BillingFlowParams.newBuilder().setSkuDetails(skuDetails.details.single()).build()
|
setProductDetails(skuDetails.details.first())
|
||||||
)
|
}.build()
|
||||||
|
|
||||||
|
val billingFlowParams = BillingFlowParams.newBuilder().apply {
|
||||||
|
setProductDetailsParamsList(listOf(productParams))
|
||||||
|
}.build()
|
||||||
|
|
||||||
|
return client.launchBillingFlow(activity, billingFlowParams)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
+44
-38
@@ -14,15 +14,15 @@ import eu.darken.capod.common.debug.logging.logTag
|
|||||||
import eu.darken.capod.common.flow.setupCommonEventHandlers
|
import eu.darken.capod.common.flow.setupCommonEventHandlers
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.channels.awaitClose
|
import kotlinx.coroutines.channels.awaitClose
|
||||||
|
import kotlinx.coroutines.channels.trySendBlocking
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.callbackFlow
|
import kotlinx.coroutines.flow.callbackFlow
|
||||||
import kotlinx.coroutines.flow.retryWhen
|
import kotlinx.coroutines.flow.retryWhen
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
import kotlin.coroutines.resume
|
|
||||||
import kotlin.coroutines.suspendCoroutine
|
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
class BillingClientConnectionProvider @Inject constructor(
|
class BillingClientConnectionProvider @Inject constructor(
|
||||||
@@ -48,36 +48,40 @@ class BillingClientConnectionProvider @Inject constructor(
|
|||||||
}
|
}
|
||||||
}.build()
|
}.build()
|
||||||
|
|
||||||
val connectionResult = suspendCoroutine<BillingResult> { continuation ->
|
|
||||||
log(TAG, VERBOSE) { "startConnection(...)" }
|
log(TAG, VERBOSE) { "startConnection(...)" }
|
||||||
client.startConnection(object : BillingClientStateListener {
|
client.startConnection(object : BillingClientStateListener {
|
||||||
override fun onBillingSetupFinished(result: BillingResult) {
|
override fun onBillingSetupFinished(result: BillingResult) {
|
||||||
log(TAG, VERBOSE) {
|
log(TAG, VERBOSE) {
|
||||||
"onBillingSetupFinished(code=${result.responseCode}, message=${result.debugMessage})"
|
"onBillingSetupFinished(code=${result.responseCode}, message=${result.debugMessage})"
|
||||||
|
}
|
||||||
|
|
||||||
|
when (result.responseCode) {
|
||||||
|
BillingResponseCode.OK -> {
|
||||||
|
val connection = BillingClientConnection(client, purchasePublisher)
|
||||||
|
|
||||||
|
trySendBlocking(connection)
|
||||||
|
|
||||||
|
launch {
|
||||||
|
try {
|
||||||
|
purchasePublisher.value = connection.queryPurchases()
|
||||||
|
log(TAG) { "Initial IAP query successful." }
|
||||||
|
} catch (e: Exception) {
|
||||||
|
log(TAG, ERROR) { "Initial IAP query failed:\n${e.asLog()}" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
close(BillingResultException(result))
|
||||||
}
|
}
|
||||||
continuation.resume(result)
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onBillingServiceDisconnected() {
|
override fun onBillingServiceDisconnected() {
|
||||||
log(TAG, VERBOSE) { "onBillingServiceDisconnected() " }
|
log(TAG, VERBOSE) { "onBillingServiceDisconnected() " }
|
||||||
close(CancellationException("Billing service disconnected"))
|
close(BillingException("Billing service disconnected"))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
|
||||||
val billingClientConnection = when (connectionResult.responseCode) {
|
|
||||||
BillingResponseCode.OK -> BillingClientConnection(client, purchasePublisher)
|
|
||||||
else -> throw BillingClientException(connectionResult)
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
purchasePublisher.value = billingClientConnection.queryPurchases()
|
|
||||||
log(TAG) { "Initial IAP query successful." }
|
|
||||||
} catch (e: Exception) {
|
|
||||||
log(TAG, ERROR) { "Initial IAP query failed:\n${e.asLog()}" }
|
|
||||||
}
|
|
||||||
|
|
||||||
send(billingClientConnection)
|
|
||||||
|
|
||||||
log(TAG) { "Awaiting close." }
|
log(TAG) { "Awaiting close." }
|
||||||
awaitClose {
|
awaitClose {
|
||||||
@@ -89,26 +93,28 @@ class BillingClientConnectionProvider @Inject constructor(
|
|||||||
val connection: Flow<BillingClientConnection> = connectionProvider
|
val connection: Flow<BillingClientConnection> = connectionProvider
|
||||||
.setupCommonEventHandlers(TAG) { "connection" }
|
.setupCommonEventHandlers(TAG) { "connection" }
|
||||||
.retryWhen { cause, attempt ->
|
.retryWhen { cause, attempt ->
|
||||||
|
log(TAG) { "Billing client connection error: ${cause.asLog()}" }
|
||||||
|
|
||||||
if (cause is CancellationException) {
|
if (cause is CancellationException) {
|
||||||
log(TAG) { "BillingClient connection cancelled." }
|
log(TAG) { "BillingClient connection cancelled." }
|
||||||
return@retryWhen false
|
return@retryWhen false
|
||||||
}
|
}
|
||||||
if (attempt > 5) {
|
|
||||||
log(TAG, WARN) { "Reached attempt limit: $attempt due to $cause" }
|
if (cause !is BillingException) {
|
||||||
|
log(TAG, WARN) { "Unknown exception type: $cause" }
|
||||||
return@retryWhen false
|
return@retryWhen false
|
||||||
}
|
}
|
||||||
if (cause !is BillingClientException) {
|
|
||||||
log(TAG, WARN) { "Unknown BillingClient exception type: $cause" }
|
|
||||||
return@retryWhen false
|
|
||||||
} else {
|
|
||||||
log(TAG) { "BillingClient exception: $cause; ${cause.result}" }
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cause.result.responseCode == BillingResponseCode.BILLING_UNAVAILABLE) {
|
if (cause is BillingResultException && cause.result.isGplayUnavailablePermanent) {
|
||||||
log(TAG) { "Got BILLING_UNAVAILABLE while trying to connect client." }
|
log(TAG) { "Got BILLING_UNAVAILABLE while trying to connect client." }
|
||||||
return@retryWhen false
|
return@retryWhen false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (attempt > 5) {
|
||||||
|
log(TAG, WARN) { "Reached attempt limit: $attempt due to $cause" }
|
||||||
|
return@retryWhen false
|
||||||
|
}
|
||||||
|
|
||||||
log(TAG) { "Will retry BillingClient connection... *sigh*" }
|
log(TAG) { "Will retry BillingClient connection... *sigh*" }
|
||||||
delay(3000 * attempt)
|
delay(3000 * attempt)
|
||||||
true
|
true
|
||||||
|
|||||||
-11
@@ -1,11 +0,0 @@
|
|||||||
package eu.darken.capod.common.upgrade.core.client
|
|
||||||
|
|
||||||
import com.android.billingclient.api.BillingResult
|
|
||||||
|
|
||||||
class BillingClientException(val result: BillingResult) : Exception() {
|
|
||||||
override val message: String?
|
|
||||||
get() = result.debugMessage
|
|
||||||
|
|
||||||
override fun toString(): String =
|
|
||||||
"BillingClientException(code=${result.responseCode}, message=${result.debugMessage})"
|
|
||||||
}
|
|
||||||
+11
-1
@@ -4,4 +4,14 @@ import com.android.billingclient.api.BillingClient
|
|||||||
import com.android.billingclient.api.BillingResult
|
import com.android.billingclient.api.BillingResult
|
||||||
|
|
||||||
internal val BillingResult.isSuccess: Boolean
|
internal val BillingResult.isSuccess: Boolean
|
||||||
get() = responseCode == BillingClient.BillingResponseCode.OK
|
get() = responseCode == BillingClient.BillingResponseCode.OK
|
||||||
|
|
||||||
|
internal val BillingResult.isGplayUnavailableTemporary: Boolean
|
||||||
|
get() = setOf(
|
||||||
|
BillingClient.BillingResponseCode.SERVICE_UNAVAILABLE,
|
||||||
|
BillingClient.BillingResponseCode.SERVICE_DISCONNECTED,
|
||||||
|
BillingClient.BillingResponseCode.SERVICE_TIMEOUT
|
||||||
|
).contains(responseCode)
|
||||||
|
|
||||||
|
internal val BillingResult.isGplayUnavailablePermanent: Boolean
|
||||||
|
get() = responseCode == BillingClient.BillingResponseCode.BILLING_UNAVAILABLE
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
package eu.darken.capod.common.upgrade.core.client
|
||||||
|
|
||||||
|
open class BillingException(override val message: String) : Exception()
|
||||||
+9
@@ -0,0 +1,9 @@
|
|||||||
|
package eu.darken.capod.common.upgrade.core.client
|
||||||
|
|
||||||
|
import com.android.billingclient.api.BillingResult
|
||||||
|
|
||||||
|
class BillingResultException(val result: BillingResult) : BillingException(result.debugMessage) {
|
||||||
|
|
||||||
|
override fun toString(): String =
|
||||||
|
"BillingResultException(code=${result.responseCode}, message=${result.debugMessage})"
|
||||||
|
}
|
||||||
+5
-9
@@ -7,13 +7,9 @@ import eu.darken.capod.common.error.LocalizedError
|
|||||||
|
|
||||||
class GplayServiceUnavailableException(cause: Throwable) : Exception("Google Play services are unavailable.", cause),
|
class GplayServiceUnavailableException(cause: Throwable) : Exception("Google Play services are unavailable.", cause),
|
||||||
HasLocalizedError {
|
HasLocalizedError {
|
||||||
override fun getLocalizedError(context: Context): LocalizedError {
|
override fun getLocalizedError(context: Context): LocalizedError = LocalizedError(
|
||||||
return LocalizedError(
|
throwable = this,
|
||||||
throwable = this,
|
label = "Google Play Services Unavailable",
|
||||||
label = "Google Play Services Unavailable",
|
description = context.getString(R.string.upgrades_gplay_unavailable_error)
|
||||||
description = context.getString(R.string.upgrades_gplay_unavailable_error)
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,11 +1,14 @@
|
|||||||
package eu.darken.capod.common.upgrade.core.data
|
package eu.darken.capod.common.upgrade.core.data
|
||||||
|
|
||||||
import com.android.billingclient.api.Purchase
|
import com.android.billingclient.api.Purchase
|
||||||
import eu.darken.capod.common.upgrade.core.toPurchasedSku
|
|
||||||
|
|
||||||
data class BillingData(
|
data class BillingData(
|
||||||
val purchases: Collection<Purchase>
|
val purchases: Collection<Purchase>
|
||||||
) {
|
) {
|
||||||
val purchasedSkus: Collection<PurchasedSku>
|
val purchasedSkus: Collection<PurchasedSku>
|
||||||
get() = purchases.map { it.toPurchasedSku() }.flatten()
|
get() = purchases.map { it.toPurchasedSku() }.flatten()
|
||||||
|
|
||||||
|
private fun Purchase.toPurchasedSku(): Collection<PurchasedSku> = skus.map {
|
||||||
|
PurchasedSku(Sku(it), this)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
package eu.darken.capod.common.upgrade.core.data
|
package eu.darken.capod.common.upgrade.core.data
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import com.android.billingclient.api.BillingClient.BillingResponseCode
|
|
||||||
import eu.darken.capod.common.coroutine.AppScope
|
import eu.darken.capod.common.coroutine.AppScope
|
||||||
import eu.darken.capod.common.debug.Bugs
|
import eu.darken.capod.common.debug.Bugs
|
||||||
import eu.darken.capod.common.debug.logging.Logging.Priority.*
|
import eu.darken.capod.common.debug.logging.Logging.Priority.*
|
||||||
@@ -10,9 +9,7 @@ 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.flow.replayingShare
|
import eu.darken.capod.common.flow.replayingShare
|
||||||
import eu.darken.capod.common.flow.setupCommonEventHandlers
|
import eu.darken.capod.common.flow.setupCommonEventHandlers
|
||||||
import eu.darken.capod.common.upgrade.core.client.BillingClientConnectionProvider
|
import eu.darken.capod.common.upgrade.core.client.*
|
||||||
import eu.darken.capod.common.upgrade.core.client.BillingClientException
|
|
||||||
import eu.darken.capod.common.upgrade.core.client.GplayServiceUnavailableException
|
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
@@ -53,37 +50,34 @@ class BillingDataRepo @Inject constructor(
|
|||||||
}
|
}
|
||||||
.forEach {
|
.forEach {
|
||||||
log(TAG, INFO) { "Acknowledging purchase: $it" }
|
log(TAG, INFO) { "Acknowledging purchase: $it" }
|
||||||
|
client.acknowledgePurchase(it)
|
||||||
try {
|
|
||||||
client.acknowledgePurchase(it)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
log(TAG, ERROR) { "Failed to ancknowledge purchase: $it\n${e.asLog()}" }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.setupCommonEventHandlers(TAG) { "connection-acks" }
|
.setupCommonEventHandlers(TAG) { "connection-acks" }
|
||||||
.retryWhen { cause, attempt ->
|
.retryWhen { cause, attempt ->
|
||||||
|
log(TAG, ERROR) { "Failed to acknowledge purchase: ${cause.asLog()}" }
|
||||||
|
|
||||||
if (cause is CancellationException) {
|
if (cause is CancellationException) {
|
||||||
log(TAG) { "Ack was cancelled (appScope?) cancelled." }
|
log(TAG) { "Ack was cancelled (appScope?) cancelled." }
|
||||||
return@retryWhen false
|
return@retryWhen false
|
||||||
}
|
}
|
||||||
|
|
||||||
if (attempt > 5) {
|
if (attempt > 5) {
|
||||||
log(TAG, WARN) { "Reached attempt limit: $attempt due to $cause" }
|
log(TAG, WARN) { "Reached attempt limit: $attempt due to $cause" }
|
||||||
return@retryWhen false
|
return@retryWhen false
|
||||||
}
|
}
|
||||||
if (cause !is BillingClientException) {
|
|
||||||
log(TAG, WARN) { "Unknown BillingClient exception type: $cause" }
|
if (cause !is BillingException) {
|
||||||
|
log(TAG, WARN) { "Unknown exception type: $cause" }
|
||||||
return@retryWhen false
|
return@retryWhen false
|
||||||
} else {
|
|
||||||
log(TAG) { "BillingClient exception: $cause; ${cause.result}" }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cause.result.responseCode == BillingResponseCode.BILLING_UNAVAILABLE) {
|
if (cause is BillingResultException && cause.result.isGplayUnavailablePermanent) {
|
||||||
log(TAG) { "Got BILLING_UNAVAILABLE while trying to ACK purchase." }
|
log(TAG) { "Got BILLING_UNAVAILABLE while trying to ACK purchase." }
|
||||||
return@retryWhen false
|
return@retryWhen false
|
||||||
}
|
}
|
||||||
|
|
||||||
log(TAG) { "Will retry ACK" }
|
log(TAG) { "Will retry ACK (attempt=$attempt)" }
|
||||||
delay(3000 * attempt)
|
delay(3000 * attempt)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
@@ -108,7 +102,7 @@ class BillingDataRepo @Inject constructor(
|
|||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
log(TAG, WARN) { "Failed to start IAP flow:\n${e.asLog()}" }
|
log(TAG, WARN) { "Failed to start IAP flow:\n${e.asLog()}" }
|
||||||
val ignoredCodes = listOf(3, 6)
|
val ignoredCodes = listOf(3, 6)
|
||||||
if (e !is BillingClientException || !e.result.responseCode.let { ignoredCodes.contains(it) }) {
|
if (e !is BillingResultException || !e.result.responseCode.let { ignoredCodes.contains(it) }) {
|
||||||
Bugs.report(TAG, "IAP flow failed for $sku", e)
|
Bugs.report(TAG, "IAP flow failed for $sku", e)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,16 +113,14 @@ class BillingDataRepo @Inject constructor(
|
|||||||
companion object {
|
companion object {
|
||||||
val TAG: String = logTag("Upgrade", "Gplay", "Billing", "DataRepo")
|
val TAG: String = logTag("Upgrade", "Gplay", "Billing", "DataRepo")
|
||||||
|
|
||||||
internal fun Throwable.tryMapUserFriendly(): Throwable {
|
internal fun Throwable.tryMapUserFriendly(): Throwable = when {
|
||||||
if (this !is BillingClientException) return this
|
this is BillingResultException && this.result.isGplayUnavailableTemporary -> {
|
||||||
|
GplayServiceUnavailableException(this)
|
||||||
return when (result.responseCode) {
|
|
||||||
BillingResponseCode.BILLING_UNAVAILABLE,
|
|
||||||
BillingResponseCode.SERVICE_UNAVAILABLE,
|
|
||||||
BillingResponseCode.SERVICE_DISCONNECTED,
|
|
||||||
BillingResponseCode.SERVICE_TIMEOUT -> GplayServiceUnavailableException(this)
|
|
||||||
else -> this
|
|
||||||
}
|
}
|
||||||
|
this is BillingResultException && this.result.isGplayUnavailablePermanent -> {
|
||||||
|
GplayServiceUnavailableException(this)
|
||||||
|
}
|
||||||
|
else -> this
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
package eu.darken.capod.common.upgrade.core.data
|
package eu.darken.capod.common.upgrade.core.data
|
||||||
|
|
||||||
import com.android.billingclient.api.SkuDetails
|
import com.android.billingclient.api.ProductDetails
|
||||||
|
|
||||||
data class Sku(
|
data class Sku(
|
||||||
val id: String
|
val id: String
|
||||||
) {
|
) {
|
||||||
data class Details(
|
data class Details(
|
||||||
val sku: Sku,
|
val sku: Sku,
|
||||||
val details: Collection<SkuDetails>,
|
val details: Collection<ProductDetails>,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1,2 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources></resources>
|
<resources>
|
||||||
|
<string name="upgrades_gplay_unavailable_error">Os serviços do Google Play estão indisponíveis.</string>
|
||||||
|
<string name="upgrades_no_purchases_found_check_account">Nenhuma compra encontrada. Está a usar a conta correta?</string>
|
||||||
|
</resources>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<string name="upgrades_gplay_unavailable_error">Google Play 服務不可用。</string>
|
<string name="upgrades_gplay_unavailable_error">Google Play 服務無法使用。</string>
|
||||||
<string name="upgrades_no_purchases_found_check_account">未找到訂單,確定帳戶無誤?</string>
|
<string name="upgrades_no_purchases_found_check_account">未找到訂單,確定帳戶無誤?</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -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,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
|
||||||
@@ -123,7 +123,9 @@ class OverviewFragmentVM @Inject constructor(
|
|||||||
if (!isBluetoothEnabled) {
|
if (!isBluetoothEnabled) {
|
||||||
items.add(0, BluetoothDisabledVH.Item)
|
items.add(0, BluetoothDisabledVH.Item)
|
||||||
} else if (mainPod == null) {
|
} else if (mainPod == null) {
|
||||||
items.add(0, MissingMainDeviceVH.Item)
|
items.add(0, MissingMainDeviceVH.Item {
|
||||||
|
OverviewFragmentDirections.actionOverviewFragmentToTroubleShooterFragment().navVia(this)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,10 +21,12 @@ class MissingMainDeviceVH(parent: ViewGroup) :
|
|||||||
item: Item,
|
item: Item,
|
||||||
payloads: List<Any>
|
payloads: List<Any>
|
||||||
) -> Unit = binding(payload = true) { item ->
|
) -> Unit = binding(payload = true) { item ->
|
||||||
|
troubleshootAction.setOnClickListener { item.onTroubleShoot() }
|
||||||
}
|
}
|
||||||
|
|
||||||
object Item : OverviewAdapter.Item {
|
data class Item(
|
||||||
|
val onTroubleShoot: () -> Unit,
|
||||||
|
) : OverviewAdapter.Item {
|
||||||
override val stableId: Long = Item::class.hashCode().toLong()
|
override val stableId: Long = Item::class.hashCode().toLong()
|
||||||
|
|
||||||
override val payloadProvider: ((DifferItem, DifferItem) -> DifferItem?)
|
override val payloadProvider: ((DifferItem, DifferItem) -> DifferItem?)
|
||||||
|
|||||||
+1
-1
@@ -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
|
||||||
|
|||||||
+1
-1
@@ -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
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import eu.darken.capod.main.ui.MainActivity
|
|||||||
import eu.darken.capod.monitor.core.PodDeviceCache
|
import eu.darken.capod.monitor.core.PodDeviceCache
|
||||||
import eu.darken.capod.monitor.core.PodMonitor
|
import eu.darken.capod.monitor.core.PodMonitor
|
||||||
import eu.darken.capod.pods.core.*
|
import eu.darken.capod.pods.core.*
|
||||||
|
import finish2
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withTimeout
|
import kotlinx.coroutines.withTimeout
|
||||||
@@ -37,15 +38,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(7),
|
||||||
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 +53,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.finish2()
|
||||||
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) " }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,10 +5,10 @@ import android.bluetooth.BluetoothDevice
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.hilt.work.HiltWorker
|
import androidx.hilt.work.HiltWorker
|
||||||
import androidx.work.CoroutineWorker
|
import androidx.work.CoroutineWorker
|
||||||
|
import androidx.work.ForegroundInfo
|
||||||
import androidx.work.WorkerParameters
|
import androidx.work.WorkerParameters
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import dagger.hilt.EntryPoints
|
|
||||||
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.Bugs
|
import eu.darken.capod.common.debug.Bugs
|
||||||
@@ -23,7 +23,6 @@ import eu.darken.capod.main.core.GeneralSettings
|
|||||||
import eu.darken.capod.main.core.MonitorMode
|
import eu.darken.capod.main.core.MonitorMode
|
||||||
import eu.darken.capod.main.core.PermissionTool
|
import eu.darken.capod.main.core.PermissionTool
|
||||||
import eu.darken.capod.main.ui.widget.WidgetManager
|
import eu.darken.capod.main.ui.widget.WidgetManager
|
||||||
import eu.darken.capod.monitor.core.MonitorComponent
|
|
||||||
import eu.darken.capod.monitor.core.MonitorCoroutineScope
|
import eu.darken.capod.monitor.core.MonitorCoroutineScope
|
||||||
import eu.darken.capod.monitor.core.PodMonitor
|
import eu.darken.capod.monitor.core.PodMonitor
|
||||||
import eu.darken.capod.monitor.ui.MonitorNotifications
|
import eu.darken.capod.monitor.ui.MonitorNotifications
|
||||||
@@ -39,7 +38,6 @@ import kotlinx.coroutines.flow.*
|
|||||||
class MonitorWorker @AssistedInject constructor(
|
class MonitorWorker @AssistedInject constructor(
|
||||||
@Assisted private val context: Context,
|
@Assisted private val context: Context,
|
||||||
@Assisted private val params: WorkerParameters,
|
@Assisted private val params: WorkerParameters,
|
||||||
monitorComponentBuilder: MonitorComponent.Builder,
|
|
||||||
private val dispatcherProvider: DispatcherProvider,
|
private val dispatcherProvider: DispatcherProvider,
|
||||||
private val monitorNotifications: MonitorNotifications,
|
private val monitorNotifications: MonitorNotifications,
|
||||||
private val notificationManager: NotificationManager,
|
private val notificationManager: NotificationManager,
|
||||||
@@ -55,13 +53,6 @@ class MonitorWorker @AssistedInject constructor(
|
|||||||
) : CoroutineWorker(context, params) {
|
) : CoroutineWorker(context, params) {
|
||||||
|
|
||||||
private val workerScope = MonitorCoroutineScope()
|
private val workerScope = MonitorCoroutineScope()
|
||||||
private val monitorComponent = monitorComponentBuilder
|
|
||||||
.coroutineScope(workerScope)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
private val entryPoint by lazy {
|
|
||||||
EntryPoints.get(monitorComponent, MonitorWorkerEntryPoint::class.java)
|
|
||||||
}
|
|
||||||
|
|
||||||
private var finishedWithError = false
|
private var finishedWithError = false
|
||||||
|
|
||||||
@@ -69,10 +60,16 @@ class MonitorWorker @AssistedInject constructor(
|
|||||||
log(TAG, VERBOSE) { "init(): workerId=$id" }
|
log(TAG, VERBOSE) { "init(): workerId=$id" }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun getForegroundInfo(): ForegroundInfo {
|
||||||
|
return monitorNotifications.getForegroundInfo(null)
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun doWork(): Result = try {
|
override suspend fun doWork(): Result = try {
|
||||||
val start = System.currentTimeMillis()
|
val start = System.currentTimeMillis()
|
||||||
log(TAG, VERBOSE) { "Executing $inputData now (runAttemptCount=$runAttemptCount)" }
|
log(TAG, VERBOSE) { "Executing $inputData now (runAttemptCount=$runAttemptCount)" }
|
||||||
|
|
||||||
|
setForeground(monitorNotifications.getForegroundInfo(null))
|
||||||
|
|
||||||
doDoWork()
|
doDoWork()
|
||||||
|
|
||||||
val duration = System.currentTimeMillis() - start
|
val duration = System.currentTimeMillis() - start
|
||||||
@@ -101,7 +98,6 @@ class MonitorWorker @AssistedInject constructor(
|
|||||||
|
|
||||||
val monitorJob = podMonitor.mainDevice
|
val monitorJob = podMonitor.mainDevice
|
||||||
.setupCommonEventHandlers(TAG) { "PodMonitor" }
|
.setupCommonEventHandlers(TAG) { "PodMonitor" }
|
||||||
.onStart { setForeground(monitorNotifications.getForegroundInfo(null)) }
|
|
||||||
.distinctUntilChanged()
|
.distinctUntilChanged()
|
||||||
.throttleLatest(1000)
|
.throttleLatest(1000)
|
||||||
.onEach { currentDevice ->
|
.onEach { currentDevice ->
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
package eu.darken.capod.monitor.core.worker
|
|
||||||
|
|
||||||
import dagger.hilt.EntryPoint
|
|
||||||
import dagger.hilt.InstallIn
|
|
||||||
import eu.darken.capod.monitor.core.MonitorComponent
|
|
||||||
|
|
||||||
@InstallIn(MonitorComponent::class)
|
|
||||||
@EntryPoint
|
|
||||||
interface MonitorWorkerEntryPoint
|
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ import eu.darken.capod.common.hasApiLevel
|
|||||||
import eu.darken.capod.common.notifications.PendingIntentCompat
|
import eu.darken.capod.common.notifications.PendingIntentCompat
|
||||||
import eu.darken.capod.main.ui.MainActivity
|
import eu.darken.capod.main.ui.MainActivity
|
||||||
import eu.darken.capod.pods.core.PodDevice
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
|
||||||
@@ -29,6 +31,7 @@ class MonitorNotifications @Inject constructor(
|
|||||||
private val notificationViewFactory: MonitorNotificationViewFactory
|
private val notificationViewFactory: MonitorNotificationViewFactory
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
private val builderLock = Mutex()
|
||||||
private val builder: NotificationCompat.Builder
|
private val builder: NotificationCompat.Builder
|
||||||
|
|
||||||
init {
|
init {
|
||||||
@@ -56,7 +59,7 @@ class MonitorNotifications @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getBuilder(device: PodDevice?): NotificationCompat.Builder {
|
private fun getBuilder(device: PodDevice?): NotificationCompat.Builder {
|
||||||
if (device == null) {
|
if (device == null) {
|
||||||
return builder.apply {
|
return builder.apply {
|
||||||
setCustomContentView(null)
|
setCustomContentView(null)
|
||||||
@@ -76,9 +79,13 @@ class MonitorNotifications @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getNotification(podDevice: PodDevice?): Notification = getBuilder(podDevice).build()
|
suspend fun getNotification(podDevice: PodDevice?): Notification = builderLock.withLock {
|
||||||
|
getBuilder(podDevice).build()
|
||||||
|
}
|
||||||
|
|
||||||
fun getForegroundInfo(podDevice: PodDevice?): ForegroundInfo = getBuilder(podDevice).toForegroundInfo()
|
suspend fun getForegroundInfo(podDevice: PodDevice?): ForegroundInfo = builderLock.withLock {
|
||||||
|
getBuilder(podDevice).toForegroundInfo()
|
||||||
|
}
|
||||||
|
|
||||||
@SuppressLint("InlinedApi")
|
@SuppressLint("InlinedApi")
|
||||||
private fun NotificationCompat.Builder.toForegroundInfo(): ForegroundInfo = if (hasApiLevel(29)) {
|
private fun NotificationCompat.Builder.toForegroundInfo(): ForegroundInfo = if (hasApiLevel(29)) {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user