mirror of
https://github.com/d4rken-org/capod.git
synced 2026-09-14 18:26:11 -04:00
Compare commits
34
Commits
v2.7.2-rc0
...
v2.8.3-rc0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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,8 @@ dependencies {
|
|||||||
|
|
||||||
addTesting()
|
addTesting()
|
||||||
|
|
||||||
implementation("com.bugsnag:bugsnag-android:5.9.2")
|
|
||||||
implementation("com.getkeepsafe.relinker:relinker:1.4.3")
|
|
||||||
|
|
||||||
"gplayImplementation"("com.android.billingclient:billing:4.0.0")
|
"gplayImplementation"("com.android.billingclient:billing:4.0.0")
|
||||||
|
|
||||||
|
"gplayImplementation"("com.bugsnag:bugsnag-android:5.9.2")
|
||||||
|
"gplayImplementation"("com.getkeepsafe.relinker:relinker:1.4.3")
|
||||||
}
|
}
|
||||||
@@ -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>
|
||||||
|
|||||||
+29
-29
@@ -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,36 @@ 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})"
|
||||||
|
}
|
||||||
|
|
||||||
|
val billingClientConnection = when (result.responseCode) {
|
||||||
|
BillingResponseCode.OK -> BillingClientConnection(client, purchasePublisher)
|
||||||
|
else -> throw BillingClientException(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
trySendBlocking(billingClientConnection)
|
||||||
|
|
||||||
|
launch {
|
||||||
|
try {
|
||||||
|
purchasePublisher.value = billingClientConnection.queryPurchases()
|
||||||
|
log(TAG) { "Initial IAP query successful." }
|
||||||
|
} catch (e: Exception) {
|
||||||
|
log(TAG, ERROR) { "Initial IAP query failed:\n${e.asLog()}" }
|
||||||
}
|
}
|
||||||
continuation.resume(result)
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onBillingServiceDisconnected() {
|
override fun onBillingServiceDisconnected() {
|
||||||
log(TAG, VERBOSE) { "onBillingServiceDisconnected() " }
|
log(TAG, VERBOSE) { "onBillingServiceDisconnected() " }
|
||||||
close(CancellationException("Billing service disconnected"))
|
error(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 {
|
||||||
|
|||||||
+1
-3
@@ -2,9 +2,7 @@ package eu.darken.capod.common.upgrade.core.client
|
|||||||
|
|
||||||
import com.android.billingclient.api.BillingResult
|
import com.android.billingclient.api.BillingResult
|
||||||
|
|
||||||
class BillingClientException(val result: BillingResult) : Exception() {
|
class BillingClientException(val result: BillingResult) : BillingException(result.debugMessage) {
|
||||||
override val message: String?
|
|
||||||
get() = result.debugMessage
|
|
||||||
|
|
||||||
override fun toString(): String =
|
override fun toString(): String =
|
||||||
"BillingClientException(code=${result.responseCode}, message=${result.debugMessage})"
|
"BillingClientException(code=${result.responseCode}, message=${result.debugMessage})"
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
package eu.darken.capod.common.upgrade.core.client
|
||||||
|
|
||||||
|
open class BillingException(override val message: String) : Exception()
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
+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)
|
||||||
|
|||||||
@@ -57,6 +57,7 @@
|
|||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:gravity="center_vertical"
|
android:gravity="center_vertical"
|
||||||
|
android:id="@+id/pod_case_container"
|
||||||
android:orientation="horizontal">
|
android:orientation="horizontal">
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
|
|||||||
@@ -7,9 +7,7 @@
|
|||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/pod_icon"
|
android:id="@+id/pod_icon"
|
||||||
style="@style/PodInfoItemIcon"
|
style="@style/PodInfoItemIcon.Popup"
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:src="@drawable/devic_earbuds_generic_both"
|
android:src="@drawable/devic_earbuds_generic_both"
|
||||||
app:layout_constraintBottom_toBottomOf="@id/pod_label"
|
app:layout_constraintBottom_toBottomOf="@id/pod_label"
|
||||||
app:layout_constraintEnd_toStartOf="@id/pod_label"
|
app:layout_constraintEnd_toStartOf="@id/pod_label"
|
||||||
@@ -55,9 +53,7 @@
|
|||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/pod_left_icon"
|
android:id="@+id/pod_left_icon"
|
||||||
style="@style/PodInfoItemIcon"
|
style="@style/PodInfoItemIcon.Popup"
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="center"
|
android:layout_gravity="center"
|
||||||
android:src="@drawable/devic_airpods_gen1_left"
|
android:src="@drawable/devic_airpods_gen1_left"
|
||||||
app:layout_constraintBottom_toBottomOf="@id/pod_left_battery_label"
|
app:layout_constraintBottom_toBottomOf="@id/pod_left_battery_label"
|
||||||
@@ -67,9 +63,7 @@
|
|||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@id/pod_left_battery_icon"
|
android:id="@id/pod_left_battery_icon"
|
||||||
style="@style/PodInfoItemIcon"
|
style="@style/PodInfoItemIcon.Popup"
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:src="@drawable/ic_baseline_battery_unknown_24"
|
android:src="@drawable/ic_baseline_battery_unknown_24"
|
||||||
app:layout_constraintBottom_toBottomOf="@id/pod_left_battery_label"
|
app:layout_constraintBottom_toBottomOf="@id/pod_left_battery_label"
|
||||||
app:layout_constraintEnd_toStartOf="@id/pod_left_battery_label"
|
app:layout_constraintEnd_toStartOf="@id/pod_left_battery_label"
|
||||||
@@ -103,9 +97,7 @@
|
|||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/pod_case_icon"
|
android:id="@+id/pod_case_icon"
|
||||||
style="@style/PodInfoItemIcon"
|
style="@style/PodInfoItemIcon.Popup"
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="center"
|
android:layout_gravity="center"
|
||||||
android:src="@drawable/devic_airpods_gen1_case"
|
android:src="@drawable/devic_airpods_gen1_case"
|
||||||
app:layout_constraintBottom_toBottomOf="@id/pod_case_battery_label"
|
app:layout_constraintBottom_toBottomOf="@id/pod_case_battery_label"
|
||||||
@@ -115,9 +107,7 @@
|
|||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@id/pod_case_battery_icon"
|
android:id="@id/pod_case_battery_icon"
|
||||||
style="@style/PodInfoItemIcon"
|
style="@style/PodInfoItemIcon.Popup"
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:src="@drawable/ic_baseline_battery_unknown_24"
|
android:src="@drawable/ic_baseline_battery_unknown_24"
|
||||||
app:layout_constraintBottom_toBottomOf="@id/pod_case_battery_label"
|
app:layout_constraintBottom_toBottomOf="@id/pod_case_battery_label"
|
||||||
app:layout_constraintEnd_toStartOf="@id/pod_case_battery_label"
|
app:layout_constraintEnd_toStartOf="@id/pod_case_battery_label"
|
||||||
@@ -150,9 +140,7 @@
|
|||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/pod_right_icon"
|
android:id="@+id/pod_right_icon"
|
||||||
style="@style/PodInfoItemIcon"
|
style="@style/PodInfoItemIcon.Popup"
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="center"
|
android:layout_gravity="center"
|
||||||
android:src="@drawable/devic_airpods_gen1_right"
|
android:src="@drawable/devic_airpods_gen1_right"
|
||||||
app:layout_constraintBottom_toBottomOf="@id/pod_right_battery_label"
|
app:layout_constraintBottom_toBottomOf="@id/pod_right_battery_label"
|
||||||
@@ -162,9 +150,7 @@
|
|||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@id/pod_right_battery_icon"
|
android:id="@id/pod_right_battery_icon"
|
||||||
style="@style/PodInfoItemIcon"
|
style="@style/PodInfoItemIcon.Popup"
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:src="@drawable/ic_baseline_battery_unknown_24"
|
android:src="@drawable/ic_baseline_battery_unknown_24"
|
||||||
app:layout_constraintBottom_toBottomOf="@id/pod_right_battery_label"
|
app:layout_constraintBottom_toBottomOf="@id/pod_right_battery_label"
|
||||||
app:layout_constraintEnd_toStartOf="@id/pod_right_battery_label"
|
app:layout_constraintEnd_toStartOf="@id/pod_right_battery_label"
|
||||||
|
|||||||
@@ -7,9 +7,7 @@
|
|||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/headphones_icon"
|
android:id="@+id/headphones_icon"
|
||||||
android:layout_width="wrap_content"
|
style="@style/PodInfoItemIcon.Popup"
|
||||||
style="@style/PodInfoItemIcon"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:src="@drawable/devic_earbuds_generic_both"
|
android:src="@drawable/devic_earbuds_generic_both"
|
||||||
app:layout_constraintBottom_toBottomOf="@id/headphones_label"
|
app:layout_constraintBottom_toBottomOf="@id/headphones_label"
|
||||||
app:layout_constraintEnd_toStartOf="@id/headphones_label"
|
app:layout_constraintEnd_toStartOf="@id/headphones_label"
|
||||||
@@ -56,9 +54,7 @@
|
|||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@id/headphones_battery_icon"
|
android:id="@id/headphones_battery_icon"
|
||||||
android:layout_width="wrap_content"
|
style="@style/PodInfoItemIcon.Popup"
|
||||||
style="@style/PodInfoItemIcon"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:src="@drawable/ic_baseline_battery_unknown_24"
|
android:src="@drawable/ic_baseline_battery_unknown_24"
|
||||||
app:layout_constraintBottom_toBottomOf="@id/headphones_battery_label"
|
app:layout_constraintBottom_toBottomOf="@id/headphones_battery_label"
|
||||||
app:layout_constraintEnd_toStartOf="@id/headphones_battery_label"
|
app:layout_constraintEnd_toStartOf="@id/headphones_battery_label"
|
||||||
|
|||||||
@@ -1,2 +1,79 @@
|
|||||||
<?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="general_share_action">Partilhar</string>
|
||||||
|
<string name="general_done_action">OK</string>
|
||||||
|
<string name="general_copy_action">Copiar</string>
|
||||||
|
<string name="general_thank_you_label">Obrigado</string>
|
||||||
|
<string name="general_upgrade_action">Melhorar</string>
|
||||||
|
<string name="general_check_action">Verificar</string>
|
||||||
|
<string name="general_close_action">Fechar</string>
|
||||||
|
<string name="upgrade_capod_label">Melhorar CAPod</string>
|
||||||
|
<string name="upgrade_capod_description">Obtenha funcionalidades adicionais e apoie o programador.</string>
|
||||||
|
<string name="settings_monitor_mode_label">Modo de monitorização</string>
|
||||||
|
<string name="settings_monitor_mode_description">Sob quais circunstâncias esta aplicação monitora dados Bluetooth.</string>
|
||||||
|
<string name="settings_scanner_mode_label">Modo de scanner</string>
|
||||||
|
<string name="settings_scanner_mode_description">O scanner de dados \'Bluetooth Low Energy\' deve priorizar desempenho ou economizar energia?</string>
|
||||||
|
<string name="settings_autopause_label">Pausa automática</string>
|
||||||
|
<string name="settings_autopause_description">Pause o áudio ao remover o dispositivo do ouvido.</string>
|
||||||
|
<string name="settings_showall_label">Mostrar todos os dispositivos</string>
|
||||||
|
<string name="settings_showall_description">Mostrar os dispositivos de outras pessoas que estão perto de você.</string>
|
||||||
|
<string name="settings_autopplay_label">Reprodução automática</string>
|
||||||
|
<string name="settings_autoplay_description">Iniciar a reprodução de áudio quando o dispositivo for usado.</string>
|
||||||
|
<string name="settings_fake_data_label">Dados falsos</string>
|
||||||
|
<string name="settings_fake_data_description">Mostrar dados falsos, por exemplo, mostrar dispositivos que não existem.</string>
|
||||||
|
<string name="settings_debug_label">Definições de debug</string>
|
||||||
|
<string name="settings_debug_description">Definições adicionais para ajudar a resolver problemas com a aplicação.</string>
|
||||||
|
<string name="settings_signal_minimum_label">Qualidade de sinal mínima</string>
|
||||||
|
<string name="settings_signal_minimum_description">A qualidade de sinal mínima para o dispositivo ser considerado seu.</string>
|
||||||
|
<string name="settings_autoconnect_label">AutoConectar</string>
|
||||||
|
<string name="settings_autoconnect_description">Se o Android não conectar automaticamente, nós podemos pedir também. Isto irá alterar o modo de monitorização para \'Sempre\'.</string>
|
||||||
|
<string name="settings_autoconnect_condition_label">Condição para AutoConectar</string>
|
||||||
|
<string name="settings_autoconnect_condition_description">Quando é que devemos tentar conectar ao seu dispositivo?</string>
|
||||||
|
<string name="settings_reaction_label">Reações</string>
|
||||||
|
<string name="settings_reaction_description">Reagir a eventos e comportamentos.</string>
|
||||||
|
<string name="settings_category_yourdevice_label">O seu dispositivo</string>
|
||||||
|
<string name="settings_maindevice_address_label">Endereço do seu dispositivo</string>
|
||||||
|
<string name="settings_maindevice_address_description">O endereço do seu dispositivo emparelhado. A aplicação utiliza isto para determinar quando é que está conectado ao seu telemóvel.</string>
|
||||||
|
<string name="settings_maindevice_address_none">Nenhum</string>
|
||||||
|
<string name="settings_maindevice_model_label">Modelo do seu dispositivo</string>
|
||||||
|
<string name="settings_maindevice_model_description">O modelo do seu dispositivo principal. Isto ajuda a aplicação a reconhecer o seu dispositivo quando não está conectado ao seu telemóvel.</string>
|
||||||
|
<string name="settings_popup_caseopen_label">Mostrar pop-up</string>
|
||||||
|
<string name="settings_popup_caseopen_description">Mostrar pop-up quando a caixa do dispositivo está aberta (experimental).</string>
|
||||||
|
<string name="settings_onepod_mode_label">Modo de um pod</string>
|
||||||
|
<string name="settings_onepod_mode_description">Usar os dois pods não é necessário, usar um único pod é suficiente para desencadear reações.</string>
|
||||||
|
<string name="notification_channel_device_status_label">Estado do dispositivo</string>
|
||||||
|
<string name="debug_debuglog_size_label">Tamanho</string>
|
||||||
|
<string name="debug_debuglog_size_compressed_label">Tamanho comprimido</string>
|
||||||
|
<string name="debug_notification_channel_label">Notificações de debug</string>
|
||||||
|
<string name="debug_debuglog_file_label">Ficheiro de registo gravado</string>
|
||||||
|
<string name="debug_debuglog_record_action">Gravar ficheiro de registo</string>
|
||||||
|
<string name="settings_support_installid_label">ID de instalação</string>
|
||||||
|
<string name="settings_support_installid_desc">Os relatórios de erros automáticos são anónimos. Compartilhe o seu ID de instalação se o programador precisar encontrar os seus relatórios de erro.</string>
|
||||||
|
<string name="settings_support_label">Suporte</string>
|
||||||
|
<string name="settings_support_description">Se precisar de ajuda.</string>
|
||||||
|
<string name="issue_tracker_label">Rastreador de problemas</string>
|
||||||
|
<string name="issue_tracker_description">Um rastreador de problemas públicos para relatórios de erros e solicitações de recursos (somente em inglês).</string>
|
||||||
|
<string name="discord_label">Discord</string>
|
||||||
|
<string name="discord_description">Um lugar para conviver e tirar dúvidas.</string>
|
||||||
|
<string name="changelog_label">Registo de alterações</string>
|
||||||
|
<string name="settings_label">Definições</string>
|
||||||
|
<string name="settings_privacy_policy_label">Política de privacidade</string>
|
||||||
|
<string name="settings_privacy_policy_desc">Manuseio de dados com responsabilidade.</string>
|
||||||
|
<string name="settings_licenses_label">Licenças</string>
|
||||||
|
<string name="settings_category_other_label">Outro</string>
|
||||||
|
<string name="settings_general_label">Definições</string>
|
||||||
|
<string name="settings_general_description">Ajustes gerais que afetam a aplicação toda.</string>
|
||||||
|
<string name="settings_acknowledgements_label">Reconhecimentos</string>
|
||||||
|
<string name="settings_debug_autoreports_label">Relatar de erros automaticamente</string>
|
||||||
|
<string name="settings_debug_autoreports_description">Relata problemas automaticamente, por exemplo, detalhes sobre uma falha na aplicação para eu poder perceber como corrigi-lo.</string>
|
||||||
|
<string name="settings_debug_mode_label">Modo de debug</string>
|
||||||
|
<string name="settings_debug_mode_description">Mostrar informações adicionais para resolver problemas.</string>
|
||||||
|
<string name="settings_blescanner_unfiltered_label">Dados BLE sem filtro</string>
|
||||||
|
<string name="settings_blescanner_unfiltered_description">Remova qualquer filtro do scanner BLE para mostrar todos os dados BLE transmitidos. Útil para adicionar suporte para novos headphones.</string>
|
||||||
|
<string name="settings_compatibility_mode_label">Modo de compatibilidade</string>
|
||||||
|
<string name="settings_compatibility_mode_description">Desative as otimizações para melhorar a compatibilidade. Tente isso se não estiver a ver nenhum dado.</string>
|
||||||
|
<string name="help_translate_label">Tradução</string>
|
||||||
|
<string name="help_translate_description">Ajude a traduzir esta aplicação para o seu idioma favorito.</string>
|
||||||
|
<string name="translators_thanks_title">Tradutores</string>
|
||||||
|
<string name="translators_thanks_description">darken</string>
|
||||||
|
</resources>
|
||||||
|
|||||||
@@ -53,6 +53,7 @@
|
|||||||
<string name="settings_support_description">Yardıma ihtiyacın olursa.</string>
|
<string name="settings_support_description">Yardıma ihtiyacın olursa.</string>
|
||||||
<string name="issue_tracker_label">Sorun izleyicisi</string>
|
<string name="issue_tracker_label">Sorun izleyicisi</string>
|
||||||
<string name="issue_tracker_description">Hata raporları ve özellik istekleri için genel bir sorun izleyicisi (yalnızca İngilizce).</string>
|
<string name="issue_tracker_description">Hata raporları ve özellik istekleri için genel bir sorun izleyicisi (yalnızca İngilizce).</string>
|
||||||
|
<string name="discord_label">Discord</string>
|
||||||
<string name="discord_description">Takılıp soru sormak için bir yer.</string>
|
<string name="discord_description">Takılıp soru sormak için bir yer.</string>
|
||||||
<string name="changelog_label">Değişiklik Günlüğü</string>
|
<string name="changelog_label">Değişiklik Günlüğü</string>
|
||||||
<string name="settings_label">Ayarlar</string>
|
<string name="settings_label">Ayarlar</string>
|
||||||
@@ -73,4 +74,6 @@
|
|||||||
<string name="settings_compatibility_mode_description">Uyumluluğu artırmak için iyileştirmeleri devre dışı bırakın. Herhangi bir veri görmüyorsanız bunu deneyin.</string>
|
<string name="settings_compatibility_mode_description">Uyumluluğu artırmak için iyileştirmeleri devre dışı bırakın. Herhangi bir veri görmüyorsanız bunu deneyin.</string>
|
||||||
<string name="help_translate_label">Tercüme</string>
|
<string name="help_translate_label">Tercüme</string>
|
||||||
<string name="help_translate_description">Bu uygulamayı en sevdiğiniz dile çevirmeye yardımcı olun.</string>
|
<string name="help_translate_description">Bu uygulamayı en sevdiğiniz dile çevirmeye yardımcı olun.</string>
|
||||||
|
<string name="translators_thanks_title">Çevirmenler</string>
|
||||||
|
<string name="translators_thanks_description">karanlık</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -38,6 +38,11 @@
|
|||||||
<item name="tint">?android:attr/colorControlNormal</item>
|
<item name="tint">?android:attr/colorControlNormal</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<style name="PodInfoItemIcon.Popup">
|
||||||
|
<item name="android:layout_height">24dp</item>
|
||||||
|
<item name="android:layout_width">24dp</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
<style name="PodInfoItemText" parent="TextAppearance.MaterialComponents.Body2">
|
<style name="PodInfoItemText" parent="TextAppearance.MaterialComponents.Body2">
|
||||||
<item name="android:singleLine">true</item>
|
<item name="android:singleLine">true</item>
|
||||||
<item name="android:ellipsize">end</item>
|
<item name="android:ellipsize">end</item>
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ buildscript {
|
|||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath("com.android.tools.build:gradle:7.2.2")
|
classpath("com.android.tools.build:gradle:7.3.1")
|
||||||
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${Versions.Kotlin.core}")
|
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${Versions.Kotlin.core}")
|
||||||
classpath("com.google.dagger:hilt-android-gradle-plugin:${Versions.Dagger.core}")
|
classpath("com.google.dagger:hilt-android-gradle-plugin:${Versions.Dagger.core}")
|
||||||
classpath("androidx.navigation:navigation-safe-args-gradle-plugin:${Versions.AndroidX.Navigation.core}")
|
classpath("androidx.navigation:navigation-safe-args-gradle-plugin:${Versions.AndroidX.Navigation.core}")
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ repositories {
|
|||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation("com.android.tools.build:gradle:7.2.2")
|
implementation("com.android.tools.build:gradle:7.3.1")
|
||||||
implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.7.10")
|
implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.0")
|
||||||
implementation("com.squareup:javapoet:1.13.0")
|
implementation("com.squareup:javapoet:1.13.0")
|
||||||
}
|
}
|
||||||
@@ -84,14 +84,14 @@ fun DependencyHandlerScope.addBaseWorkManager() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun DependencyHandlerScope.addBaseAndroid() {
|
fun DependencyHandlerScope.addBaseAndroid() {
|
||||||
implementation("androidx.core:core-ktx:1.8.0")
|
implementation("androidx.core:core-ktx:1.10.0-rc01")
|
||||||
implementation("androidx.annotation:annotation:1.4.0")
|
implementation("androidx.annotation:annotation:1.4.0")
|
||||||
implementation("androidx.collection:collection-ktx:1.2.0")
|
implementation("androidx.collection:collection-ktx:1.2.0")
|
||||||
implementation("androidx.preference:preference-ktx:1.2.0")
|
implementation("androidx.preference:preference-ktx:1.2.0")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun DependencyHandlerScope.addBaseAndroidUi() {
|
fun DependencyHandlerScope.addBaseAndroidUi() {
|
||||||
implementation("androidx.appcompat:appcompat:1.6.0-alpha04")
|
implementation("androidx.appcompat:appcompat:1.6.1")
|
||||||
implementation("androidx.constraintlayout:constraintlayout:2.1.3")
|
implementation("androidx.constraintlayout:constraintlayout:2.1.3")
|
||||||
implementation("androidx.fragment:fragment-ktx:1.4.1")
|
implementation("androidx.fragment:fragment-ktx:1.4.1")
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import com.android.build.gradle.LibraryExtension
|
import com.android.build.gradle.LibraryExtension
|
||||||
import org.gradle.api.Action
|
import org.gradle.api.Action
|
||||||
import org.gradle.api.JavaVersion
|
import org.gradle.api.JavaVersion
|
||||||
|
import org.gradle.api.Project
|
||||||
|
import org.gradle.kotlin.dsl.apply
|
||||||
|
import org.gradle.kotlin.dsl.extra
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileInputStream
|
import java.io.FileInputStream
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
@@ -122,4 +125,30 @@ fun getBugSnagApiKey(
|
|||||||
}
|
}
|
||||||
|
|
||||||
return System.getenv("BUGSNAG_API_KEY") ?: bugsnagProps.getProperty("bugsnag.apikey")
|
return System.getenv("BUGSNAG_API_KEY") ?: bugsnagProps.getProperty("bugsnag.apikey")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use extra["useBugsnag"] = true per flavor to enable
|
||||||
|
*/
|
||||||
|
fun <T> T.setupBugsnagPlugin() where T : Project {
|
||||||
|
fun Project.`android`(configure: Action<com.android.build.gradle.internal.dsl.BaseAppModuleExtension>): Unit =
|
||||||
|
(this as org.gradle.api.plugins.ExtensionAware).extensions.configure("android", configure)
|
||||||
|
|
||||||
|
val key = "useBugsnag"
|
||||||
|
|
||||||
|
android {
|
||||||
|
productFlavors {
|
||||||
|
forEach {
|
||||||
|
val paramString = gradle.startParameter.taskRequests.toString().toLowerCase(Locale.ROOT)
|
||||||
|
val useBugsnag = if (it.extra.has(key)) it.extra[key] as Boolean else false
|
||||||
|
|
||||||
|
if (paramString.contains(it.name) && useBugsnag) {
|
||||||
|
println("Bugsnag plugin (com.bugsnag.android.gradle) applied to ${it.name} ($paramString)")
|
||||||
|
apply(plugin = "com.bugsnag.android.gradle")
|
||||||
|
} else {
|
||||||
|
println("Bugsnag plugin (com.bugsnag.android.gradle) NOT applied to ${it.name} ($paramString)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ object Versions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
object Kotlin {
|
object Kotlin {
|
||||||
const val core = "1.7.10"
|
const val core = "1.8.0"
|
||||||
const val coroutines = "1.6.2"
|
const val coroutines = "1.6.2"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -13,11 +13,11 @@ object Versions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
object Dagger {
|
object Dagger {
|
||||||
const val core = "2.42"
|
const val core = "2.45"
|
||||||
}
|
}
|
||||||
|
|
||||||
object Moshi {
|
object Moshi {
|
||||||
const val core = "1.13.0"
|
const val core = "1.14.0"
|
||||||
}
|
}
|
||||||
|
|
||||||
object AndroidX {
|
object AndroidX {
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
Bugfixes and performance improvements.
|
||||||
|
¯\_(ツ)_/¯
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
Bugfixes and performance improvements.
|
||||||
|
¯\_(ツ)_/¯
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
Bugfixes and performance improvements.
|
||||||
|
¯\_(ツ)_/¯
|
||||||
@@ -1,3 +1,2 @@
|
|||||||
Now with more widgets!
|
Bugfixes and performance improvements.
|
||||||
But also bugfixes and performance improvements.
|
|
||||||
¯\_(ツ)_/¯
|
¯\_(ツ)_/¯
|
||||||
@@ -10,6 +10,9 @@ Features:
|
|||||||
* Automatically connect phone and AirPods.
|
* Automatically connect phone and AirPods.
|
||||||
* Show popup when case is opened.
|
* Show popup when case is opened.
|
||||||
|
|
||||||
|
CAPod has a Wear-OS version that shows the status of the pod device that is closest to you.
|
||||||
|
CAPod for Wear-OS is a stand-alone app and does not require a phone.
|
||||||
|
|
||||||
CAPod is ad-free. Some features require an in-app purchase.
|
CAPod is ad-free. Some features require an in-app purchase.
|
||||||
|
|
||||||
Most popular AirPods and Beats devices are supported.
|
Most popular AirPods and Beats devices are supported.
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
A CAPod az AirPods kísérőalkalmazása
|
Az AirPods kísérőalkalmazása
|
||||||
@@ -1 +1 @@
|
|||||||
CAPod - Companion app per le Airpods
|
Companion app per le Airpods
|
||||||
@@ -1 +1 @@
|
|||||||
CAPod - Complemento para AirPods
|
CAPod - Para AirPods
|
||||||
@@ -1,18 +1,18 @@
|
|||||||
CAPod is a companion app for AirPods.
|
CAPod é uma aplicação complementar aos AirPods.
|
||||||
|
|
||||||
Features:
|
Funcionalidades:
|
||||||
|
|
||||||
* Battery level for pods and cases.
|
* Nível de bateria para os pods e a caixa.
|
||||||
* Charging status for pods and case.
|
* Estado de carregamento para os pods e a caixa.
|
||||||
* Additional infos about connection, microphone and case.
|
* Informações adicionais sobre a conexão, o microfone e a caixa.
|
||||||
* Can receive and show all nearby devices.
|
* Pode detetar e mostrar todos os dispositivos por perto.
|
||||||
* Ear detection with automatic play/pause.
|
* Deteção do ouvido com reprodução/pausa automática.
|
||||||
* Automatically connect phone and AirPods.
|
* Automaticamente conectar telemóvel e AirPods.
|
||||||
* Show popup when case is opened.
|
* Mostrar pop-up quando a caixa é aberta.
|
||||||
|
|
||||||
CAPod is ad-free. Some features require an in-app purchase.
|
CAPods é livre de anúncios. Algumas funcionalidades exigem uma compra na aplicação.
|
||||||
|
|
||||||
Most popular AirPods and Beats devices are supported.
|
Os dispositivos AirPods e Beats mais populares são suportados.
|
||||||
If your device is similar to AirPods but not yet supported, send me a short mail.
|
Se o seu dispositivo é semelhante aos AirPods, mas ainda não é suportado, envie-me um pequeno email.
|
||||||
|
|
||||||
Got a cool idea for a new feature? Reach out!
|
Tem alguma ideia fixe para uma nova funcionalidade? Contacte-me!
|
||||||
@@ -1 +1 @@
|
|||||||
CAPod is a companion app for AirPods on Android.
|
CAPod é uma aplicação complementar aos AirPods no Android.
|
||||||
@@ -1 +1 @@
|
|||||||
CAPod - Companion for AirPods
|
CAPod - Para AirPods
|
||||||
@@ -1 +1 @@
|
|||||||
CAPod - Bạn đồng hành cho AirPods
|
Bạn đồng hành cho AirPods
|
||||||
+20
@@ -360,6 +360,25 @@ do-versionfile() {
|
|||||||
git add VERSION
|
git add VERSION
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Update a version file that can be parsed by third-parties, e.g. F-Droid
|
||||||
|
do-fastlane-changelog() {
|
||||||
|
CHANGELOGS_DIR="fastlane/metadata/android/en-US/changelogs"
|
||||||
|
NEW_CHANGELOG="$CHANGELOGS_DIR/$V_CODE.txt"
|
||||||
|
|
||||||
|
[ -f "$NEW_CHANGELOG" ] && return
|
||||||
|
|
||||||
|
echo -e "\n${S_NOTICE}Creating new default fastlane changelog file for $V_CODE...\n"
|
||||||
|
|
||||||
|
cp "$CHANGELOGS_DIR/default.txt" "$NEW_CHANGELOG"
|
||||||
|
|
||||||
|
cat "$NEW_CHANGELOG"
|
||||||
|
|
||||||
|
echo -e "\n\n${I_OK} ${S_NOTICE}${ACTION_MSG} [${S_NORM}$NEW_CHANGELOG${S_NOTICE}] file"
|
||||||
|
|
||||||
|
# Stage file for commit
|
||||||
|
git add "$CHANGELOGS_DIR/$V_CODE.txt"
|
||||||
|
}
|
||||||
|
|
||||||
# Does the release branch already exist?
|
# Does the release branch already exist?
|
||||||
check-branch-exist() {
|
check-branch-exist() {
|
||||||
[ "$FLAG_NOBRANCH" = true ] && return
|
[ "$FLAG_NOBRANCH" = true ] && return
|
||||||
@@ -448,6 +467,7 @@ echo -e "\n${S_LIGHT}––––––"
|
|||||||
# Update steps
|
# Update steps
|
||||||
do-version-properties
|
do-version-properties
|
||||||
do-versionfile
|
do-versionfile
|
||||||
|
do-fastlane-changelog
|
||||||
do-branch
|
do-branch
|
||||||
do-commit
|
do-commit
|
||||||
create-tag "${V_NAME}" "${REL_NOTE}"
|
create-tag "${V_NAME}" "${REL_NOTE}"
|
||||||
|
|||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
### Updated by release.sh ###
|
### Updated by release.sh ###
|
||||||
project.versioning.major=2
|
project.versioning.major=2
|
||||||
project.versioning.minor=7
|
project.versioning.minor=8
|
||||||
project.versioning.patch=2
|
project.versioning.patch=3
|
||||||
project.versioning.build=0
|
project.versioning.build=0
|
||||||
#############################
|
#############################
|
||||||
|
|||||||
Reference in New Issue
Block a user