Compare commits

...
34 Commits
Author SHA1 Message Date
darken 88b0069b94 Release: 2.8.3-rc0 2023-03-13 13:48:11 +01:00
Matthias Urhahn 3b8ddc9f06 Fix reversed AirPods Max wear-detection (#116)
* Fix reversed AirPods Max wear-detection
Fixes #115

* Remove unnecessary test
2023-03-13 13:47:32 +01:00
darken 708c7f38ec Merge remote-tracking branch 'origin/main' into main 2023-03-12 14:05:25 +01:00
darken 139daa1cc2 Update full gplay text 2023-03-12 14:05:20 +01:00
Matthias Urhahn b85ac778c0 Address that gplay api can emit multiple times (#114) 2023-03-12 02:58:37 +01:00
Matthias Urhahn a3e2c9e61d Bump dependencies and gradle (#113) 2023-03-11 18:16:24 +01:00
Matthias Urhahn ad78a0570c For some reason we can finish the widget update broadcast twice. (#112)
I can't yet determine why this race-condition occurs, due to the forced timeout, it shouldn't be due to exceeding the receiver ANR window...
2023-03-11 13:01:15 +01:00
Matthias Urhahn 7e6ee274d8 Quick data updates can cause concurrent access and modifying of the notification builder. (#111)
Internally building the notification can then throw a ConcurrentModificationException.
2023-03-11 13:01:05 +01:00
Matthias Urhahn daf7411c36 Show worker foreground notification early. (#110) 2023-03-11 13:00:55 +01:00
darken d3dedbe6cd Release: 2.8.2-rc0 2023-03-10 12:13:30 +01:00
Matthias Urhahn d5411586d5 Update translations (#109)
* Update translations, add Portuguese

* Update translations
2023-03-10 12:12:31 +01:00
Matthias Urhahn 94e2d40554 Fix AirPods Max wear detection. (#108)
Closes #106
2023-03-10 11:30:40 +01:00
Matthias Urhahn db80fa1b46 Wear: Remove unused code (#104)
* Wear: Remove unused code

* Remove more code
2023-03-02 16:39:11 +01:00
Matthias Urhahn 9b569b8d07 Update Turkish translation (#100) 2023-02-20 09:39:27 +01:00
darken 90288880fb Add f-droid as source 2023-02-20 09:14:20 +01:00
darken 870e518ff6 Release: 2.8.1-rc0 2023-02-09 22:01:02 +01:00
darken 6d89169bae Update release.sh script to create default changelogs for f-droid 2023-02-09 21:59:44 +01:00
darken ac77ee2703 Update default changelog for GPlay 2023-02-09 21:53:33 +01:00
darken 18dcf603e9 Release: 2.8.0-rc0 2023-02-03 22:32:01 +01:00
Matthias Urhahn 71aaba1b86 Fix data display for non dual apple pods (#98)
* Fix data display for fake pods and beats devices (with two pods)

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

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

* Setup bugsnag only in gplay builds.

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

* Only apply Bugsnag plugin for the right flavors.

* Run unit tests in debug, otherwise we run issues requiring api keys in the env
2023-02-01 11:17:50 +01:00
100 changed files with 670 additions and 455 deletions
+1
View File
@@ -12,3 +12,4 @@
*.jks *.jks
.local/* .local/*
/fastlane/report.xml /fastlane/report.xml
/fastlane/Appfile
+6 -5
View File
@@ -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://img.shields.io/endpoint?color=green&logo=google-play&logoColor=green&url=https%3A%2F%2Fplay.cuzi.workers.dev%2Fplay%3Fi%3Deu.darken.capod%26l%3DGoogle%2520Play%26m%3D%24version)](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://img.shields.io/endpoint?color=green&logo=google-play&logoColor=green&url=https%3A%2F%2Fplay.cuzi.workers.dev%2Fplay%3Fi%3Deu.darken.capod%26l%3DGoogle%2520Play%26m%3D%24version)](https://play.google.com/store/apps/details?id=eu.darken.capod) |
| [Google Play Beta](https://play.google.com/apps/testing/eu.darken.capod) | [![](https://img.shields.io/badge/Google%20Play-Beta-yellowgreen?style=flat&logo=google-play)](https://play.google.com/apps/testing/eu.darken.capod) | | | [Google Play Beta](https://play.google.com/apps/testing/eu.darken.capod) | [![](https://img.shields.io/badge/Google%20Play-Beta-yellowgreen?style=flat&logo=google-play)](https://play.google.com/apps/testing/eu.darken.capod) | |
| [Github Releases](https://github.com/d4rken-org/capod/releases) | [![GitHub release (latest SemVer including pre-releases)](https://img.shields.io/github/v/release/d4rken-org/capod?include_prereleases&label=GitHub)](https://github.com/d4rken-org/capod/releases/latest) | | [Github Releases](https://github.com/d4rken-org/capod/releases) | [![GitHub release (latest SemVer including pre-releases)](https://img.shields.io/github/v/release/d4rken-org/capod?include_prereleases&label=GitHub)](https://github.com/d4rken-org/capod/releases/latest) |
| [F-Droid (IzzyOnDroid)](https://apt.izzysoft.de/packages/eu.darken.capod/) | [![](https://img.shields.io/endpoint?url=https://apt.izzysoft.de/fdroid/api/v1/shield/eu.darken.capod)](https://apt.izzysoft.de/packages/eu.darken.capod/) | | [F-Droid](https://f-droid.org/en/packages/eu.darken.capod/) | [![](https://img.shields.io/f-droid/v/eu.darken.capod)](https://f-droid.org/en/packages/eu.darken.capod/) |
| [F-Droid (IzzyOnDroid)](https://apt.izzysoft.de/packages/eu.darken.capod/) | [![](https://img.shields.io/endpoint?url=https://apt.izzysoft.de/fdroid/api/v1/shield/eu.darken.capod)](https://apt.izzysoft.de/packages/eu.darken.capod/) |
## Support the project ## Support the project
+1 -1
View File
@@ -1 +1 @@
2.7.2-rc0 20702000 2.8.3-rc0 20803000
+6 -1
View File
@@ -21,6 +21,10 @@ android {
buildConfigField("String", "APPLICATION_ID", "\"${ProjectConfig.packageName}\"") buildConfigField("String", "APPLICATION_ID", "\"${ProjectConfig.packageName}\"")
buildConfigField("String", "GITSHA", "\"${lastCommitHash()}\"") buildConfigField("String", "GITSHA", "\"${lastCommitHash()}\"")
buildConfigField("String", "BUILDTIME", "\"${buildTime()}\"") buildConfigField("String", "BUILDTIME", "\"${buildTime()}\"")
manifestPlaceholders["bugsnagApiKey"] = getBugSnagApiKey(
File(System.getProperty("user.home"), ".appconfig/${ProjectConfig.packageName}/bugsnag.properties")
) ?: "bugsnag_apikey_placeholder"
} }
buildFeatures { buildFeatures {
@@ -95,6 +99,7 @@ dependencies {
addTesting() addTesting()
implementation("com.bugsnag:bugsnag-android:5.9.2") "gplayImplementation"("com.bugsnag:bugsnag-android:5.9.2")
"gplayImplementation"("com.getkeepsafe.relinker:relinker:1.4.3")
} }
+4
View File
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest>
</manifest>
@@ -0,0 +1,17 @@
package eu.darken.capod.debug.autoreport
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import eu.darken.capod.common.debug.autoreport.AutomaticBugReporter
import eu.darken.capod.debug.autoreport.FossAutoReporting
import javax.inject.Singleton
@InstallIn(SingletonComponent::class)
@Module
abstract class AutoReportingModule {
@Binds
@Singleton
abstract fun autoreporting(foss: FossAutoReporting): AutomaticBugReporter
}
@@ -0,0 +1,17 @@
package eu.darken.capod.debug.autoreport
import android.app.Application
import eu.darken.capod.common.debug.autoreport.AutomaticBugReporter
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class FossAutoReporting @Inject constructor() : AutomaticBugReporter {
override fun setup(application: Application) {
// NOOP
}
override fun notify(throwable: Throwable) {
throw IllegalStateException("Who initliazed this? Without setup no calls to here!")
}
}
+10
View File
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<meta-data
android:name="com.bugsnag.android.API_KEY"
android:value="${bugsnagApiKey}" />
</application>
</manifest>
@@ -0,0 +1,16 @@
package eu.darken.capod.debug.autoreport
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import eu.darken.capod.common.debug.autoreport.AutomaticBugReporter
import javax.inject.Singleton
@InstallIn(SingletonComponent::class)
@Module
abstract class AutoReportingModule {
@Binds
@Singleton
abstract fun autoreporting(foss: GplayAutoReporting): AutomaticBugReporter
}
@@ -1,4 +1,4 @@
package eu.darken.capod.common.debug.autoreport.bugsnag package eu.darken.capod.debug.autoreport
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context import android.content.Context
@@ -7,7 +7,7 @@ import com.bugsnag.android.Event
import com.bugsnag.android.OnErrorCallback import com.bugsnag.android.OnErrorCallback
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import eu.darken.capod.common.BuildConfigWrap import eu.darken.capod.common.BuildConfigWrap
import eu.darken.capod.common.debug.autoreport.DebugSettings import eu.darken.capod.common.debug.DebugSettings
import eu.darken.capod.common.debug.logging.Logging.Priority.WARN import eu.darken.capod.common.debug.logging.Logging.Priority.WARN
import eu.darken.capod.common.debug.logging.asLog import eu.darken.capod.common.debug.logging.asLog
import eu.darken.capod.common.debug.logging.log import eu.darken.capod.common.debug.logging.log
@@ -1,4 +1,4 @@
package eu.darken.capod.common.debug.autoreport.bugsnag package eu.darken.capod.debug.autoreport
import com.bugsnag.android.Event import com.bugsnag.android.Event
import eu.darken.capod.common.debug.logging.Logging import eu.darken.capod.common.debug.logging.Logging
@@ -1,15 +1,16 @@
package eu.darken.capod.common.debug.autoreport package eu.darken.capod.debug.autoreport
import android.app.Application
import android.content.Context import android.content.Context
import com.bugsnag.android.Bugsnag import com.bugsnag.android.Bugsnag
import com.bugsnag.android.Configuration import com.bugsnag.android.Configuration
import com.getkeepsafe.relinker.ReLinker
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import eu.darken.capod.common.BuildConfigWrap import eu.darken.capod.common.BuildConfigWrap
import eu.darken.capod.common.InstallId import eu.darken.capod.common.InstallId
import eu.darken.capod.common.debug.Bugs import eu.darken.capod.common.debug.Bugs
import eu.darken.capod.common.debug.autoreport.bugsnag.BugsnagErrorHandler import eu.darken.capod.common.debug.DebugSettings
import eu.darken.capod.common.debug.autoreport.bugsnag.BugsnagLogger import eu.darken.capod.common.debug.autoreport.AutomaticBugReporter
import eu.darken.capod.common.debug.autoreport.bugsnag.NOPBugsnagErrorHandler
import eu.darken.capod.common.debug.logging.Logging import eu.darken.capod.common.debug.logging.Logging
import eu.darken.capod.common.debug.logging.log import eu.darken.capod.common.debug.logging.log
import eu.darken.capod.common.debug.logging.logTag import eu.darken.capod.common.debug.logging.logTag
@@ -18,19 +19,25 @@ import javax.inject.Provider
import javax.inject.Singleton import javax.inject.Singleton
@Singleton @Singleton
class AutoReporting @Inject constructor( class GplayAutoReporting @Inject constructor(
@ApplicationContext private val context: Context, @ApplicationContext private val context: Context,
private val debugSettings: DebugSettings, private val debugSettings: DebugSettings,
private val installId: InstallId, private val installId: InstallId,
private val bugsnagLogger: Provider<BugsnagLogger>, private val bugsnagLogger: Provider<BugsnagLogger>,
private val bugsnagErrorHandler: Provider<BugsnagErrorHandler>, private val bugsnagErrorHandler: Provider<BugsnagErrorHandler>,
private val nopBugsnagErrorHandler: Provider<NOPBugsnagErrorHandler>, private val nopBugsnagErrorHandler: Provider<NOPBugsnagErrorHandler>,
) { ) : AutomaticBugReporter {
fun setup() { override fun setup(application: Application) {
val isEnabled = debugSettings.isAutoReportingEnabled.value val isEnabled = debugSettings.isAutoReportingEnabled.value
log(TAG) { "setup(): isEnabled=$isEnabled" } log(TAG) { "setup(): isEnabled=$isEnabled" }
if (!isEnabled) return
ReLinker
.log { message -> log(TAG) { "ReLinker: $message" } }
.loadLibrary(application, "bugsnag-plugin-android-anr")
try { try {
val bugsnagConfig = Configuration.load(context).apply { val bugsnagConfig = Configuration.load(context).apply {
if (debugSettings.isAutoReportingEnabled.value) { if (debugSettings.isAutoReportingEnabled.value) {
@@ -48,13 +55,18 @@ class AutoReporting @Inject constructor(
} }
Bugsnag.start(context, bugsnagConfig) Bugsnag.start(context, bugsnagConfig)
Bugs.ready = true Bugs.reporter = this
} catch (e: IllegalStateException) { } catch (e: IllegalStateException) {
log(TAG) { "Bugsnag API Key not configured." } log(TAG) { "Bugsnag API Key not configured." }
} }
} }
override fun notify(throwable: Throwable) {
Bugsnag.notify(throwable)
}
companion object { companion object {
private val TAG = logTag("Debug", "AutoReport") private val TAG = logTag("Debug", "AutoReport")
} }
} }
@@ -1,4 +1,4 @@
package eu.darken.capod.common.debug.autoreport.bugsnag package eu.darken.capod.debug.autoreport
import com.bugsnag.android.Event import com.bugsnag.android.Event
import com.bugsnag.android.OnErrorCallback import com.bugsnag.android.OnErrorCallback
@@ -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,4 +1,4 @@
package eu.darken.capod.common.debug.autoreport package eu.darken.capod.common.debug
import android.content.Context import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
@@ -0,0 +1,10 @@
package eu.darken.capod.common.debug.autoreport
import android.app.Application
interface AutomaticBugReporter {
fun setup(application: Application)
fun notify(throwable: Throwable)
}
@@ -6,7 +6,7 @@ import androidx.preference.PreferenceDataStore
import com.squareup.moshi.Moshi import com.squareup.moshi.Moshi
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import eu.darken.capod.common.bluetooth.ScannerMode import eu.darken.capod.common.bluetooth.ScannerMode
import eu.darken.capod.common.debug.autoreport.DebugSettings import eu.darken.capod.common.debug.DebugSettings
import eu.darken.capod.common.preferences.PreferenceStoreMapper import eu.darken.capod.common.preferences.PreferenceStoreMapper
import eu.darken.capod.common.preferences.Settings import eu.darken.capod.common.preferences.Settings
import eu.darken.capod.common.preferences.createFlowPreference import eu.darken.capod.common.preferences.createFlowPreference
@@ -26,7 +26,7 @@ class GeneralSettings @Inject constructor(
val monitorMode = preferences.createFlowPreference("core.monitor.mode", MonitorMode.AUTOMATIC, moshi) val monitorMode = preferences.createFlowPreference("core.monitor.mode", MonitorMode.AUTOMATIC, moshi)
val scannerMode = preferences.createFlowPreference("core.scanner.mode", ScannerMode.BALANCED, moshi) val scannerMode = preferences.createFlowPreference("core.scanner.mode", ScannerMode.BALANCED, moshi)
val showAll = preferences.createFlowPreference("core.showall.enabled", false) val showAll = preferences.createFlowPreference("core.showall.enabled", true)
val minimumSignalQuality = preferences.createFlowPreference("core.signal.minimum", 0.25f) val minimumSignalQuality = preferences.createFlowPreference("core.signal.minimum", 0.25f)
@@ -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
} }
} }
+60 -1
View File
@@ -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
}
}
} }
+7 -9
View File
@@ -6,7 +6,6 @@ plugins {
} }
apply(plugin = "dagger.hilt.android.plugin") apply(plugin = "dagger.hilt.android.plugin")
apply(plugin = "androidx.navigation.safeargs.kotlin") apply(plugin = "androidx.navigation.safeargs.kotlin")
apply(plugin = "com.bugsnag.android.gradle")
android { android {
compileSdk = ProjectConfig.compileSdk compileSdk = ProjectConfig.compileSdk
@@ -24,10 +23,6 @@ android {
buildConfigField("String", "GITSHA", "\"${lastCommitHash()}\"") buildConfigField("String", "GITSHA", "\"${lastCommitHash()}\"")
buildConfigField("String", "BUILDTIME", "\"${buildTime()}\"") buildConfigField("String", "BUILDTIME", "\"${buildTime()}\"")
manifestPlaceholders["bugsnagApiKey"] = getBugSnagApiKey(
File(System.getProperty("user.home"), ".appconfig/${ProjectConfig.packageName}/bugsnag.properties")
) ?: "fake"
} }
signingConfigs { signingConfigs {
@@ -49,9 +44,12 @@ android {
create("gplay") { create("gplay") {
dimension = "version" dimension = "version"
signingConfig = signingConfigs["releaseGplay"] signingConfig = signingConfigs["releaseGplay"]
extra["useBugsnag"] = true
} }
} }
setupBugsnagPlugin()
buildTypes { buildTypes {
val customProguardRules = fileTree(File("../proguard")) { val customProguardRules = fileTree(File("../proguard")) {
include("*.pro") include("*.pro")
@@ -92,7 +90,7 @@ android {
if (listOf("release", "beta").any { variantName.toLowerCase().contains(it) }) { if (listOf("release", "beta").any { variantName.toLowerCase().contains(it) }) {
val outputFileName = ProjectConfig.packageName + "-WEAROS" + val outputFileName = ProjectConfig.packageName + "-WEAROS" +
"-v${defaultConfig.versionName}-${defaultConfig.versionCode}" + "-v${defaultConfig.versionName}-${defaultConfig.versionCode}" +
"-${variantName.toUpperCase()}-${lastCommitHash()}.apk" "-${variantName.toUpperCase()}.apk"
variantOutputImpl.outputFileName = outputFileName variantOutputImpl.outputFileName = outputFileName
} }
@@ -138,9 +136,9 @@ dependencies {
addTesting() addTesting()
implementation("com.bugsnag:bugsnag-android:5.9.2")
implementation("com.getkeepsafe.relinker:relinker:1.4.3")
implementation("androidx.wear:wear:1.2.0") implementation("androidx.wear:wear:1.2.0")
implementation("androidx.wear.tiles:tiles-material:1.1.0") implementation("androidx.wear.tiles:tiles-material:1.1.0")
"gplayImplementation"("com.bugsnag:bugsnag-android:5.9.2")
"gplayImplementation"("com.getkeepsafe.relinker:relinker:1.4.3")
} }
-4
View File
@@ -39,10 +39,6 @@
android:name="androidx.startup.InitializationProvider" android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup" android:authorities="${applicationId}.androidx-startup"
tools:node="remove" /> tools:node="remove" />
<meta-data
android:name="com.bugsnag.android.API_KEY"
android:value="${bugsnagApiKey}" />
</application> </application>
</manifest> </manifest>
@@ -3,11 +3,10 @@ package eu.darken.capod
import android.app.Application import android.app.Application
import androidx.hilt.work.HiltWorkerFactory import androidx.hilt.work.HiltWorkerFactory
import androidx.work.* import androidx.work.*
import com.getkeepsafe.relinker.ReLinker
import dagger.hilt.android.HiltAndroidApp import dagger.hilt.android.HiltAndroidApp
import eu.darken.capod.common.BuildConfigWrap import eu.darken.capod.common.BuildConfigWrap
import eu.darken.capod.common.coroutine.AppScope import eu.darken.capod.common.coroutine.AppScope
import eu.darken.capod.common.debug.autoreport.AutoReporting import eu.darken.capod.common.debug.autoreport.AutomaticBugReporter
import eu.darken.capod.common.debug.logging.* import eu.darken.capod.common.debug.logging.*
import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE
import eu.darken.capod.main.core.GeneralSettings import eu.darken.capod.main.core.GeneralSettings
@@ -20,7 +19,7 @@ import javax.inject.Inject
open class App : Application(), Configuration.Provider { open class App : Application(), Configuration.Provider {
@Inject lateinit var workerFactory: HiltWorkerFactory @Inject lateinit var workerFactory: HiltWorkerFactory
@Inject lateinit var autoReporting: AutoReporting @Inject lateinit var autoReporting: AutomaticBugReporter
@Inject lateinit var generalSettings: GeneralSettings @Inject lateinit var generalSettings: GeneralSettings
@Inject lateinit var workManager: WorkManager @Inject lateinit var workManager: WorkManager
@Inject @AppScope lateinit var appScope: CoroutineScope @Inject @AppScope lateinit var appScope: CoroutineScope
@@ -29,11 +28,7 @@ open class App : Application(), Configuration.Provider {
super.onCreate() super.onCreate()
if (BuildConfig.DEBUG) Logging.install(LogCatLogger()) if (BuildConfig.DEBUG) Logging.install(LogCatLogger())
ReLinker autoReporting.setup(this)
.log { message -> log(TAG) { "ReLinker: $message" } }
.loadLibrary(this, "bugsnag-plugin-android-anr")
autoReporting.setup()
setupWorker() setupWorker()
@@ -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>()
+7 -9
View File
@@ -6,7 +6,6 @@ plugins {
} }
apply(plugin = "dagger.hilt.android.plugin") apply(plugin = "dagger.hilt.android.plugin")
apply(plugin = "androidx.navigation.safeargs.kotlin") apply(plugin = "androidx.navigation.safeargs.kotlin")
apply(plugin = "com.bugsnag.android.gradle")
android { android {
@@ -22,10 +21,6 @@ android {
versionName = ProjectConfig.Version.name versionName = ProjectConfig.Version.name
testInstrumentationRunner = "eu.darken.capod.HiltTestRunner" testInstrumentationRunner = "eu.darken.capod.HiltTestRunner"
manifestPlaceholders["bugsnagApiKey"] = getBugSnagApiKey(
File(System.getProperty("user.home"), ".appconfig/${ProjectConfig.packageName}/bugsnag.properties")
) ?: "fake"
} }
signingConfigs { signingConfigs {
@@ -47,9 +42,12 @@ android {
create("gplay") { create("gplay") {
dimension = "version" dimension = "version"
signingConfig = signingConfigs["releaseGplay"] signingConfig = signingConfigs["releaseGplay"]
extra["useBugsnag"] = true
} }
} }
setupBugsnagPlugin()
buildTypes { buildTypes {
val customProguardRules = fileTree(File(projectDir, "proguard")) { val customProguardRules = fileTree(File(projectDir, "proguard")) {
include("*.pro") include("*.pro")
@@ -90,7 +88,7 @@ android {
if (listOf("release", "beta").any { variantName.toLowerCase().contains(it) }) { if (listOf("release", "beta").any { variantName.toLowerCase().contains(it) }) {
val outputFileName = ProjectConfig.packageName + val outputFileName = ProjectConfig.packageName +
"-v${defaultConfig.versionName}-${defaultConfig.versionCode}" + "-v${defaultConfig.versionName}-${defaultConfig.versionCode}" +
"-${variantName.toUpperCase()}-${lastCommitHash()}.apk" "-${variantName.toUpperCase()}.apk"
variantOutputImpl.outputFileName = outputFileName variantOutputImpl.outputFileName = outputFileName
} }
@@ -148,8 +146,8 @@ dependencies {
addTesting() addTesting()
implementation("com.bugsnag:bugsnag-android:5.9.2")
implementation("com.getkeepsafe.relinker:relinker:1.4.3")
"gplayImplementation"("com.android.billingclient:billing:4.0.0") "gplayImplementation"("com.android.billingclient:billing:4.0.0")
"gplayImplementation"("com.bugsnag:bugsnag-android:5.9.2")
"gplayImplementation"("com.getkeepsafe.relinker:relinker:1.4.3")
} }
+5 -1
View File
@@ -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>
@@ -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 {
@@ -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()
+4 -1
View File
@@ -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>
-4
View File
@@ -86,10 +86,6 @@
android:name="androidx.startup.InitializationProvider" android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup" android:authorities="${applicationId}.androidx-startup"
tools:node="remove" /> tools:node="remove" />
<meta-data
android:name="com.bugsnag.android.API_KEY"
android:value="${bugsnagApiKey}" />
</application> </application>
</manifest> </manifest>
+3 -8
View File
@@ -3,10 +3,9 @@ package eu.darken.capod
import android.app.Application import android.app.Application
import androidx.hilt.work.HiltWorkerFactory import androidx.hilt.work.HiltWorkerFactory
import androidx.work.Configuration import androidx.work.Configuration
import com.getkeepsafe.relinker.ReLinker
import dagger.hilt.android.HiltAndroidApp import dagger.hilt.android.HiltAndroidApp
import eu.darken.capod.common.coroutine.AppScope import eu.darken.capod.common.coroutine.AppScope
import eu.darken.capod.common.debug.autoreport.AutoReporting import eu.darken.capod.common.debug.autoreport.AutomaticBugReporter
import eu.darken.capod.common.debug.logging.* import eu.darken.capod.common.debug.logging.*
import eu.darken.capod.common.flow.throttleLatest import eu.darken.capod.common.flow.throttleLatest
import eu.darken.capod.common.upgrade.UpgradeRepo import eu.darken.capod.common.upgrade.UpgradeRepo
@@ -25,7 +24,7 @@ import javax.inject.Inject
open class App : Application(), Configuration.Provider { open class App : Application(), Configuration.Provider {
@Inject lateinit var workerFactory: HiltWorkerFactory @Inject lateinit var workerFactory: HiltWorkerFactory
@Inject lateinit var autoReporting: AutoReporting @Inject lateinit var autoReporting: AutomaticBugReporter
@Inject lateinit var monitorControl: MonitorControl @Inject lateinit var monitorControl: MonitorControl
@Inject lateinit var podMonitor: PodMonitor @Inject lateinit var podMonitor: PodMonitor
@Inject lateinit var widgetManager: WidgetManager @Inject lateinit var widgetManager: WidgetManager
@@ -36,11 +35,7 @@ open class App : Application(), Configuration.Provider {
super.onCreate() super.onCreate()
if (BuildConfig.DEBUG) Logging.install(LogCatLogger()) if (BuildConfig.DEBUG) Logging.install(LogCatLogger())
ReLinker autoReporting.setup(this)
.log { message -> log(TAG) { "ReLinker: $message" } }
.loadLibrary(this, "bugsnag-plugin-android-anr")
autoReporting.setup()
log(TAG) { "onCreate() done! ${Exception().asLog()}" } log(TAG) { "onCreate() done! ${Exception().asLog()}" }
@@ -6,7 +6,7 @@ import androidx.lifecycle.SavedStateHandle
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import eu.darken.capod.common.bluetooth.BluetoothManager2 import eu.darken.capod.common.bluetooth.BluetoothManager2
import eu.darken.capod.common.coroutine.DispatcherProvider import eu.darken.capod.common.coroutine.DispatcherProvider
import eu.darken.capod.common.debug.autoreport.DebugSettings import eu.darken.capod.common.debug.DebugSettings
import eu.darken.capod.common.debug.logging.log import eu.darken.capod.common.debug.logging.log
import eu.darken.capod.common.flow.combine import eu.darken.capod.common.flow.combine
import eu.darken.capod.common.flow.throttleLatest import eu.darken.capod.common.flow.throttleLatest
@@ -7,7 +7,7 @@ import androidx.fragment.app.viewModels
import androidx.preference.Preference import androidx.preference.Preference
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import eu.darken.capod.R import eu.darken.capod.R
import eu.darken.capod.common.debug.autoreport.DebugSettings import eu.darken.capod.common.debug.DebugSettings
import eu.darken.capod.common.observe2 import eu.darken.capod.common.observe2
import eu.darken.capod.common.uix.PreferenceFragment3 import eu.darken.capod.common.uix.PreferenceFragment3
import javax.inject.Inject import javax.inject.Inject
@@ -3,7 +3,7 @@ package eu.darken.capod.main.ui.settings.general.debug
import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.SavedStateHandle
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import eu.darken.capod.common.coroutine.DispatcherProvider import eu.darken.capod.common.coroutine.DispatcherProvider
import eu.darken.capod.common.debug.autoreport.DebugSettings import eu.darken.capod.common.debug.DebugSettings
import eu.darken.capod.common.debug.logging.log import eu.darken.capod.common.debug.logging.log
import eu.darken.capod.common.debug.logging.logTag import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.common.debug.recording.core.RecorderModule import eu.darken.capod.common.debug.recording.core.RecorderModule
@@ -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"
+78 -1
View File
@@ -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>
+3
View File
@@ -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>
+5
View File
@@ -38,6 +38,11 @@
<item name="tint">?android:attr/colorControlNormal</item> <item name="tint">?android:attr/colorControlNormal</item>
</style> </style>
<style name="PodInfoItemIcon.Popup">
<item name="android:layout_height">24dp</item>
<item name="android:layout_width">24dp</item>
</style>
<style name="PodInfoItemText" parent="TextAppearance.MaterialComponents.Body2"> <style name="PodInfoItemText" parent="TextAppearance.MaterialComponents.Body2">
<item name="android:singleLine">true</item> <item name="android:singleLine">true</item>
<item name="android:ellipsize">end</item> <item name="android:ellipsize">end</item>
+1 -1
View File
@@ -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}")
+2 -2
View File
@@ -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")
} }
+2 -2
View File
@@ -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")
+29
View File
@@ -1,6 +1,9 @@
import com.android.build.gradle.LibraryExtension import com.android.build.gradle.LibraryExtension
import org.gradle.api.Action import org.gradle.api.Action
import org.gradle.api.JavaVersion import org.gradle.api.JavaVersion
import org.gradle.api.Project
import org.gradle.kotlin.dsl.apply
import org.gradle.kotlin.dsl.extra
import java.io.File import java.io.File
import java.io.FileInputStream import java.io.FileInputStream
import java.time.Instant import java.time.Instant
@@ -123,3 +126,29 @@ 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)")
}
}
}
}
}
+3 -3
View File
@@ -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
View File
@@ -1 +1 @@
A CAPod az AirPods kísérőalkalmazása Az AirPods kísérőalkalmazása
+1 -1
View File
@@ -1 +1 @@
CAPod - Companion app per le Airpods Companion app per le Airpods
+1 -1
View File
@@ -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
View File
@@ -1 +1 @@
CAPod - Companion for AirPods CAPod - Para AirPods
+1 -1
View File
@@ -1 +1 @@
CAPod - Bạn đồng hành cho AirPods Bạn đồng hành cho AirPods
+20
View File
@@ -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
View File
@@ -1,6 +1,6 @@
### Updated by release.sh ### ### Updated by release.sh ###
project.versioning.major=2 project.versioning.major=2
project.versioning.minor=7 project.versioning.minor=8
project.versioning.patch=2 project.versioning.patch=3
project.versioning.build=0 project.versioning.build=0
############################# #############################