mirror of
https://github.com/d4rken-org/capod.git
synced 2026-09-14 18:26:11 -04:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4e258e65c1 | ||
|
|
9f49b470fe | ||
|
|
4a71cfe65a | ||
|
|
d06959f964 | ||
|
|
8c339ca2eb | ||
|
|
3aab53566d | ||
|
|
749cfed137 | ||
|
|
ce25ad6711 | ||
|
|
568aeec0d5 | ||
|
|
6b02fd66e6 | ||
|
|
0e313004ac | ||
|
|
270de8702b | ||
|
|
324878d68d | ||
|
|
9b39240016 | ||
|
|
4d6f185b58 | ||
|
|
5979081928 | ||
|
|
0d852675cd | ||
|
|
29f9d9e4be | ||
|
|
59c0e818e1 | ||
|
|
6d6647e511 | ||
|
|
612acd6d72 | ||
|
|
425a9f321b | ||
|
|
55ccfa27b2 | ||
|
|
7029263622 | ||
|
|
6153cae3fd | ||
|
|
7f78f2599e | ||
|
|
3f05802483 | ||
|
|
b5d0b47c77 |
+2
-1
@@ -1,3 +1,4 @@
|
||||
.idea
|
||||
.gradle
|
||||
build/
|
||||
build/
|
||||
/fastlane/report.xml
|
||||
|
||||
+218
@@ -0,0 +1,218 @@
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
CFPropertyList (3.0.5)
|
||||
rexml
|
||||
addressable (2.8.0)
|
||||
public_suffix (>= 2.0.2, < 5.0)
|
||||
artifactory (3.0.15)
|
||||
atomos (0.1.3)
|
||||
aws-eventstream (1.2.0)
|
||||
aws-partitions (1.554.0)
|
||||
aws-sdk-core (3.126.0)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
aws-partitions (~> 1, >= 1.525.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
jmespath (~> 1.0)
|
||||
aws-sdk-kms (1.54.0)
|
||||
aws-sdk-core (~> 3, >= 3.126.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
aws-sdk-s3 (1.112.0)
|
||||
aws-sdk-core (~> 3, >= 3.126.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.4)
|
||||
aws-sigv4 (1.4.0)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
babosa (1.0.4)
|
||||
claide (1.1.0)
|
||||
colored (1.2)
|
||||
colored2 (3.1.2)
|
||||
commander (4.6.0)
|
||||
highline (~> 2.0.0)
|
||||
declarative (0.0.20)
|
||||
digest-crc (0.6.4)
|
||||
rake (>= 12.0.0, < 14.0.0)
|
||||
domain_name (0.5.20190701)
|
||||
unf (>= 0.0.5, < 1.0.0)
|
||||
dotenv (2.7.6)
|
||||
emoji_regex (3.2.3)
|
||||
excon (0.91.0)
|
||||
faraday (1.9.3)
|
||||
faraday-em_http (~> 1.0)
|
||||
faraday-em_synchrony (~> 1.0)
|
||||
faraday-excon (~> 1.1)
|
||||
faraday-httpclient (~> 1.0)
|
||||
faraday-multipart (~> 1.0)
|
||||
faraday-net_http (~> 1.0)
|
||||
faraday-net_http_persistent (~> 1.0)
|
||||
faraday-patron (~> 1.0)
|
||||
faraday-rack (~> 1.0)
|
||||
faraday-retry (~> 1.0)
|
||||
ruby2_keywords (>= 0.0.4)
|
||||
faraday-cookie_jar (0.0.7)
|
||||
faraday (>= 0.8.0)
|
||||
http-cookie (~> 1.0.0)
|
||||
faraday-em_http (1.0.0)
|
||||
faraday-em_synchrony (1.0.0)
|
||||
faraday-excon (1.1.0)
|
||||
faraday-httpclient (1.0.1)
|
||||
faraday-multipart (1.0.3)
|
||||
multipart-post (>= 1.2, < 3)
|
||||
faraday-net_http (1.0.1)
|
||||
faraday-net_http_persistent (1.2.0)
|
||||
faraday-patron (1.0.0)
|
||||
faraday-rack (1.0.0)
|
||||
faraday-retry (1.0.3)
|
||||
faraday_middleware (1.2.0)
|
||||
faraday (~> 1.0)
|
||||
fastimage (2.2.6)
|
||||
fastlane (2.204.3)
|
||||
CFPropertyList (>= 2.3, < 4.0.0)
|
||||
addressable (>= 2.8, < 3.0.0)
|
||||
artifactory (~> 3.0)
|
||||
aws-sdk-s3 (~> 1.0)
|
||||
babosa (>= 1.0.3, < 2.0.0)
|
||||
bundler (>= 1.12.0, < 3.0.0)
|
||||
colored
|
||||
commander (~> 4.6)
|
||||
dotenv (>= 2.1.1, < 3.0.0)
|
||||
emoji_regex (>= 0.1, < 4.0)
|
||||
excon (>= 0.71.0, < 1.0.0)
|
||||
faraday (~> 1.0)
|
||||
faraday-cookie_jar (~> 0.0.6)
|
||||
faraday_middleware (~> 1.0)
|
||||
fastimage (>= 2.1.0, < 3.0.0)
|
||||
gh_inspector (>= 1.1.2, < 2.0.0)
|
||||
google-apis-androidpublisher_v3 (~> 0.3)
|
||||
google-apis-playcustomapp_v1 (~> 0.1)
|
||||
google-cloud-storage (~> 1.31)
|
||||
highline (~> 2.0)
|
||||
json (< 3.0.0)
|
||||
jwt (>= 2.1.0, < 3)
|
||||
mini_magick (>= 4.9.4, < 5.0.0)
|
||||
multipart-post (~> 2.0.0)
|
||||
naturally (~> 2.2)
|
||||
optparse (~> 0.1.1)
|
||||
plist (>= 3.1.0, < 4.0.0)
|
||||
rubyzip (>= 2.0.0, < 3.0.0)
|
||||
security (= 0.1.3)
|
||||
simctl (~> 1.6.3)
|
||||
terminal-notifier (>= 2.0.0, < 3.0.0)
|
||||
terminal-table (>= 1.4.5, < 2.0.0)
|
||||
tty-screen (>= 0.6.3, < 1.0.0)
|
||||
tty-spinner (>= 0.8.0, < 1.0.0)
|
||||
word_wrap (~> 1.0.0)
|
||||
xcodeproj (>= 1.13.0, < 2.0.0)
|
||||
xcpretty (~> 0.3.0)
|
||||
xcpretty-travis-formatter (>= 0.0.3)
|
||||
gh_inspector (1.1.3)
|
||||
google-apis-androidpublisher_v3 (0.16.0)
|
||||
google-apis-core (>= 0.4, < 2.a)
|
||||
google-apis-core (0.4.2)
|
||||
addressable (~> 2.5, >= 2.5.1)
|
||||
googleauth (>= 0.16.2, < 2.a)
|
||||
httpclient (>= 2.8.1, < 3.a)
|
||||
mini_mime (~> 1.0)
|
||||
representable (~> 3.0)
|
||||
retriable (>= 2.0, < 4.a)
|
||||
rexml
|
||||
webrick
|
||||
google-apis-iamcredentials_v1 (0.10.0)
|
||||
google-apis-core (>= 0.4, < 2.a)
|
||||
google-apis-playcustomapp_v1 (0.7.0)
|
||||
google-apis-core (>= 0.4, < 2.a)
|
||||
google-apis-storage_v1 (0.11.0)
|
||||
google-apis-core (>= 0.4, < 2.a)
|
||||
google-cloud-core (1.6.0)
|
||||
google-cloud-env (~> 1.0)
|
||||
google-cloud-errors (~> 1.0)
|
||||
google-cloud-env (1.5.0)
|
||||
faraday (>= 0.17.3, < 2.0)
|
||||
google-cloud-errors (1.2.0)
|
||||
google-cloud-storage (1.36.1)
|
||||
addressable (~> 2.8)
|
||||
digest-crc (~> 0.4)
|
||||
google-apis-iamcredentials_v1 (~> 0.1)
|
||||
google-apis-storage_v1 (~> 0.1)
|
||||
google-cloud-core (~> 1.6)
|
||||
googleauth (>= 0.16.2, < 2.a)
|
||||
mini_mime (~> 1.0)
|
||||
googleauth (1.1.0)
|
||||
faraday (>= 0.17.3, < 2.0)
|
||||
jwt (>= 1.4, < 3.0)
|
||||
memoist (~> 0.16)
|
||||
multi_json (~> 1.11)
|
||||
os (>= 0.9, < 2.0)
|
||||
signet (>= 0.16, < 2.a)
|
||||
highline (2.0.3)
|
||||
http-cookie (1.0.4)
|
||||
domain_name (~> 0.5)
|
||||
httpclient (2.8.3)
|
||||
jmespath (1.5.0)
|
||||
json (2.6.1)
|
||||
jwt (2.3.0)
|
||||
memoist (0.16.2)
|
||||
mini_magick (4.11.0)
|
||||
mini_mime (1.1.2)
|
||||
multi_json (1.15.0)
|
||||
multipart-post (2.0.0)
|
||||
nanaimo (0.3.0)
|
||||
naturally (2.2.1)
|
||||
optparse (0.1.1)
|
||||
os (1.1.4)
|
||||
plist (3.6.0)
|
||||
public_suffix (4.0.6)
|
||||
rake (13.0.6)
|
||||
representable (3.1.1)
|
||||
declarative (< 0.1.0)
|
||||
trailblazer-option (>= 0.1.1, < 0.2.0)
|
||||
uber (< 0.2.0)
|
||||
retriable (3.1.2)
|
||||
rexml (3.2.5)
|
||||
rouge (2.0.7)
|
||||
ruby2_keywords (0.0.5)
|
||||
rubyzip (2.3.2)
|
||||
security (0.1.3)
|
||||
signet (0.16.0)
|
||||
addressable (~> 2.8)
|
||||
faraday (>= 0.17.3, < 2.0)
|
||||
jwt (>= 1.5, < 3.0)
|
||||
multi_json (~> 1.10)
|
||||
simctl (1.6.8)
|
||||
CFPropertyList
|
||||
naturally
|
||||
terminal-notifier (2.0.0)
|
||||
terminal-table (1.8.0)
|
||||
unicode-display_width (~> 1.1, >= 1.1.1)
|
||||
trailblazer-option (0.1.2)
|
||||
tty-cursor (0.7.1)
|
||||
tty-screen (0.8.1)
|
||||
tty-spinner (0.9.3)
|
||||
tty-cursor (~> 0.7)
|
||||
uber (0.1.0)
|
||||
unf (0.1.4)
|
||||
unf_ext
|
||||
unf_ext (0.0.8)
|
||||
unicode-display_width (1.8.0)
|
||||
webrick (1.7.0)
|
||||
word_wrap (1.0.0)
|
||||
xcodeproj (1.21.0)
|
||||
CFPropertyList (>= 2.3.3, < 4.0)
|
||||
atomos (~> 0.1.3)
|
||||
claide (>= 1.0.2, < 2.0)
|
||||
colored2 (~> 3.1)
|
||||
nanaimo (~> 0.3.0)
|
||||
rexml (~> 3.2.4)
|
||||
xcpretty (0.3.0)
|
||||
rouge (~> 2.0.7)
|
||||
xcpretty-travis-formatter (1.0.1)
|
||||
xcpretty (~> 0.2, >= 0.0.7)
|
||||
|
||||
PLATFORMS
|
||||
x86_64-linux
|
||||
|
||||
DEPENDENCIES
|
||||
fastlane
|
||||
|
||||
BUNDLED WITH
|
||||
2.2.8
|
||||
@@ -12,7 +12,7 @@ A companion app that adds support for AirPod specific features to Android:
|
||||
* Additional infos about connection, microphone & case.
|
||||
* Can receive and show all nearby devices.
|
||||
* Ear detection with automatic play/pause.
|
||||
* Automatically connect phone & airpods.
|
||||
* Automatically connect phone & AirPods.
|
||||
* Show popup when case is opened.
|
||||
|
||||
CAPod is ad-free. Some additional features require an in-app purchase.
|
||||
|
||||
@@ -1,2 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
||||
<resources>
|
||||
<string name="foss_upgrade_donate_label">İanə ver</string>
|
||||
<string name="foss_upgrade_alreadydonated_label">Artıq ianə vermişəm</string>
|
||||
<string name="foss_upgrade_no_money_label">Bütün pulumu AirPods-lara xərcləyirəm</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,2 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
||||
<resources>
|
||||
<string name="foss_upgrade_donate_label">Donacions</string>
|
||||
<string name="foss_upgrade_alreadydonated_label">Ja he donat</string>
|
||||
<string name="foss_upgrade_no_money_label">Gasto tots els meus diners en AirPods</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,2 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
||||
<resources>
|
||||
<string name="foss_upgrade_donate_label">Přispět</string>
|
||||
<string name="foss_upgrade_alreadydonated_label">Již jsem přispěl/a</string>
|
||||
<string name="foss_upgrade_no_money_label">Všechny peníze jsem utratil/a za AirPods</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,2 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
||||
<resources>
|
||||
<string name="foss_upgrade_donate_label">Spenden</string>
|
||||
<string name="foss_upgrade_alreadydonated_label">Ich habe schon gespendet</string>
|
||||
<string name="foss_upgrade_no_money_label">Ich gebe mein ganzes Geld für AirPods aus</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,2 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
||||
<resources>
|
||||
<string name="foss_upgrade_donate_label">Δωρεά</string>
|
||||
<string name="foss_upgrade_alreadydonated_label">Ήδη δώρισα</string>
|
||||
<string name="foss_upgrade_no_money_label">Ξόδεψα όλα τα χρήματά μου στα AirPods</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,2 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
||||
<resources>
|
||||
<string name="foss_upgrade_donate_label">Donar</string>
|
||||
<string name="foss_upgrade_alreadydonated_label">Ya he donado</string>
|
||||
<string name="foss_upgrade_no_money_label">Gasto todo mi dinero en AirPods</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,2 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
||||
<resources>
|
||||
<string name="foss_upgrade_donate_label">Faire un don</string>
|
||||
<string name="foss_upgrade_alreadydonated_label">J’ai déjà fait un don</string>
|
||||
<string name="foss_upgrade_no_money_label">J’ai dépensé tout mon argent sur les AirPods</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,2 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
||||
<resources>
|
||||
<string name="foss_upgrade_donate_label">Adomány</string>
|
||||
<string name="foss_upgrade_alreadydonated_label">Már adományoztam</string>
|
||||
<string name="foss_upgrade_no_money_label">Minden pénzemet AirPodokra költöm</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,2 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
||||
<resources>
|
||||
<string name="foss_upgrade_donate_label">Menyumbang</string>
|
||||
<string name="foss_upgrade_alreadydonated_label">Saya telah berdonasi</string>
|
||||
<string name="foss_upgrade_no_money_label">Saya menghabiskan semua uang saya untuk AirPods</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,2 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
||||
<resources>
|
||||
<string name="foss_upgrade_donate_label">Dona</string>
|
||||
<string name="foss_upgrade_alreadydonated_label">Ho già donato</string>
|
||||
<string name="foss_upgrade_no_money_label">Spendo tutti i miei soldi per gli AirPods</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,2 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
||||
<resources>
|
||||
<string name="foss_upgrade_donate_label">תרומה</string>
|
||||
<string name="foss_upgrade_alreadydonated_label">כבר תרמתי</string>
|
||||
<string name="foss_upgrade_no_money_label">מיטב כספי מושקע ב-AirPods</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,2 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
||||
<resources>
|
||||
<string name="foss_upgrade_donate_label">후원하기</string>
|
||||
<string name="foss_upgrade_alreadydonated_label">이미 후원했어요</string>
|
||||
<string name="foss_upgrade_no_money_label">AirPods에 돈을 다 썼어요</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,2 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
||||
<resources>
|
||||
<string name="foss_upgrade_donate_label">Derma</string>
|
||||
<string name="foss_upgrade_alreadydonated_label">Saya sudah menderma</string>
|
||||
<string name="foss_upgrade_no_money_label">Saya belanjakan semua wang saya untuk AirPods</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,2 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
||||
<resources>
|
||||
<string name="foss_upgrade_donate_label">Doneren</string>
|
||||
<string name="foss_upgrade_alreadydonated_label">Ik heb al gedoneerd</string>
|
||||
<string name="foss_upgrade_no_money_label">Ik besteed al mijn geld aan AirPods</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,2 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
||||
<resources>
|
||||
<string name="foss_upgrade_donate_label">Doner</string>
|
||||
<string name="foss_upgrade_alreadydonated_label">Jeg har allerede donert</string>
|
||||
<string name="foss_upgrade_no_money_label">Jeg bruker alle pengene mine på AirPods</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,2 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
||||
<resources>
|
||||
<string name="foss_upgrade_donate_label">Dotacja</string>
|
||||
<string name="foss_upgrade_alreadydonated_label">Już się zrzuciłem</string>
|
||||
<string name="foss_upgrade_no_money_label">Wydałem wszystkie pieniądze na AirPods</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,2 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
||||
<resources>
|
||||
<string name="foss_upgrade_donate_label">Doar</string>
|
||||
<string name="foss_upgrade_alreadydonated_label">Eu já doei</string>
|
||||
<string name="foss_upgrade_no_money_label">Eu gasto todo o meu dinheiro em AirPods</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,2 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
||||
<resources>
|
||||
<string name="foss_upgrade_donate_label">Пожертвовать</string>
|
||||
<string name="foss_upgrade_alreadydonated_label">Я уже пожертвовал</string>
|
||||
<string name="foss_upgrade_no_money_label">Я потратил все свои деньги на AirPods</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,2 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
||||
<resources>
|
||||
<string name="foss_upgrade_donate_label">පරිත්යාග</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,2 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
||||
<resources>
|
||||
<string name="foss_upgrade_donate_label">Prispieť</string>
|
||||
<string name="foss_upgrade_alreadydonated_label">Už som prispel/a</string>
|
||||
<string name="foss_upgrade_no_money_label">Všetky peniaze som minul na AirPods</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,2 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
||||
<resources>
|
||||
<string name="foss_upgrade_donate_label">Bağış</string>
|
||||
<string name="foss_upgrade_alreadydonated_label">Ben zaten bağışladım</string>
|
||||
<string name="foss_upgrade_no_money_label">Tüm paramı AirPod\'lara harcıyorum</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,2 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
||||
<resources>
|
||||
<string name="foss_upgrade_donate_label">Пожертва</string>
|
||||
<string name="foss_upgrade_alreadydonated_label">Вже пожертвував</string>
|
||||
<string name="foss_upgrade_no_money_label">Витратив усі гроші на AirPods</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,2 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
||||
<resources>
|
||||
<string name="foss_upgrade_donate_label">Quyên tặng</string>
|
||||
<string name="foss_upgrade_alreadydonated_label">Tôi đã quyên góp</string>
|
||||
<string name="foss_upgrade_no_money_label">Tôi tiêu hết tiền cho AirPods</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,2 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
||||
<resources>
|
||||
<string name="foss_upgrade_donate_label">捐赠</string>
|
||||
<string name="foss_upgrade_alreadydonated_label">已捐赠过了</string>
|
||||
<string name="foss_upgrade_no_money_label">钱都用来买 AirPods 了</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,2 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
||||
<resources>
|
||||
<string name="foss_upgrade_donate_label">抖內</string>
|
||||
<string name="foss_upgrade_alreadydonated_label">我已經抖內了</string>
|
||||
<string name="foss_upgrade_no_money_label">我已經為 AirPods 傾家蕩產了</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,2 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
||||
<resources>
|
||||
<string name="upgrades_gplay_unavailable_error">Google Play xidmətləri əlçatmazdır.</string>
|
||||
<string name="upgrades_no_purchases_found_check_account">Heç bir satın alma tapılmadı. Doğru hesabı istifadə edirsiniz?</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,2 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
||||
<resources>
|
||||
<string name="upgrades_gplay_unavailable_error">Els serveis de Google Play no estan disponibles.</string>
|
||||
<string name="upgrades_no_purchases_found_check_account">No s\'ha trobat cap compra. Esteu utilitzant el compte correcte?</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,2 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
||||
<resources>
|
||||
<string name="upgrades_gplay_unavailable_error">Služby Google Play nejsou k dispozici.</string>
|
||||
<string name="upgrades_no_purchases_found_check_account">Nebyly nalezeny žádné nákupy. Používáte správný účet?</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,2 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
||||
<resources>
|
||||
<string name="upgrades_gplay_unavailable_error">Google Play-Dienste sind nicht verfügbar.</string>
|
||||
<string name="upgrades_no_purchases_found_check_account">Keine Käufe gefunden. Verwenden Sie das richtige Konto?</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,2 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
||||
<resources>
|
||||
<string name="upgrades_gplay_unavailable_error">Οι υπηρεσίες Google Play δεν είναι διαθέσιμες.</string>
|
||||
<string name="upgrades_no_purchases_found_check_account">Δε βρέθηκαν αγορές. Χρησιμοποιείτε τον σωστό λογαριασμό;</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,2 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
||||
<resources>
|
||||
<string name="upgrades_gplay_unavailable_error">Los servicios de Google Play no están disponibles.</string>
|
||||
<string name="upgrades_no_purchases_found_check_account">No se encontraron compras. ¿Está usando la cuenta correcta?</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,2 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
||||
<resources>
|
||||
<string name="upgrades_gplay_unavailable_error">Les services Google Play ne sont pas accessibles.</string>
|
||||
<string name="upgrades_no_purchases_found_check_account">Aucun achat n’a été trouvé. Utilisez-vous le bon compte ?</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,2 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
||||
<resources>
|
||||
<string name="upgrades_gplay_unavailable_error">A Google Play szolgáltatások nem érhetők el.</string>
|
||||
<string name="upgrades_no_purchases_found_check_account">Nem található vásárlás. A megfelelő fiókot használod?</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,2 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
||||
<resources>
|
||||
<string name="upgrades_gplay_unavailable_error">Layanan Google Play tidak tersedia.</string>
|
||||
<string name="upgrades_no_purchases_found_check_account">Tidak ada pembelian yang ditemukan. Apakah Anda menggunakan akun yang benar?</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,2 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
||||
<resources>
|
||||
<string name="upgrades_gplay_unavailable_error">I servizi di Google Play non sono disponibili.</string>
|
||||
<string name="upgrades_no_purchases_found_check_account">Nessun acquisto trovato. Stai usando l\'account giusto?</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,2 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
||||
<resources>
|
||||
<string name="upgrades_gplay_unavailable_error">שירותי Google Play אינם זמינים.</string>
|
||||
<string name="upgrades_no_purchases_found_check_account">לא נמצאו רכישות. האם אתה משתמש בחשבון הנכון?</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,2 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
||||
<resources>
|
||||
<string name="upgrades_gplay_unavailable_error">구글 플레이 서비스는 제공되지 않습니다.</string>
|
||||
<string name="upgrades_no_purchases_found_check_account">구매 내역을 찾지 못했습니다. 결제했던 계정이 맞나요?</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,2 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
||||
<resources>
|
||||
<string name="upgrades_gplay_unavailable_error">Perkhidmatan Google Play tidak tersedia.</string>
|
||||
<string name="upgrades_no_purchases_found_check_account">Tiada pembelian ditemui. Adakah anda menggunakan akaun yang betul?</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,2 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
||||
<resources>
|
||||
<string name="upgrades_gplay_unavailable_error">De Google Play-diensten zijn niet beschikbaar.</string>
|
||||
<string name="upgrades_no_purchases_found_check_account">Geen aankopen gevonden. Gebruikt u de juiste rekening?</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,2 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
||||
<resources>
|
||||
<string name="upgrades_gplay_unavailable_error">Google Play-tjenester er utilgjengelige.</string>
|
||||
<string name="upgrades_no_purchases_found_check_account">Ingen kjøp funnet. Bruker du riktig konto?</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,2 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
||||
<resources>
|
||||
<string name="upgrades_gplay_unavailable_error">Usługi Google Play nie są dostępne.</string>
|
||||
<string name="upgrades_no_purchases_found_check_account">Nie znaleziono zakupu. Czy na pewno korzystasz z poprawnego konta?</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,2 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
||||
<resources>
|
||||
<string name="upgrades_gplay_unavailable_error">Os serviços do Google Play não estão disponíveis.</string>
|
||||
<string name="upgrades_no_purchases_found_check_account">Nenhuma compra encontrada. Você está usando a conta certa?</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,2 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
||||
<resources>
|
||||
<string name="upgrades_gplay_unavailable_error">Сервисы Google Play недоступны.</string>
|
||||
<string name="upgrades_no_purchases_found_check_account">Покупки не найдены. Вы используете правильный аккаунт?</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,2 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
||||
<resources>
|
||||
<string name="upgrades_gplay_unavailable_error">ගූගල් ප්ලේ සේවා නොතිබේ.</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,2 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
||||
<resources>
|
||||
<string name="upgrades_gplay_unavailable_error">Služby Google Play nie sú k dispozícii.</string>
|
||||
<string name="upgrades_no_purchases_found_check_account">Nenašli sa žiadne nákupy. Používate správny účet?</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,2 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
||||
<resources>
|
||||
<string name="upgrades_gplay_unavailable_error">Google Play hizmetleri kullanılamıyor.</string>
|
||||
<string name="upgrades_no_purchases_found_check_account">Satın alma bulunamadı. Doğru hesabı mı kullanıyorsunuz?</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,2 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
||||
<resources>
|
||||
<string name="upgrades_gplay_unavailable_error">Сервіси Google Play недоступні.</string>
|
||||
<string name="upgrades_no_purchases_found_check_account">Покупок не знайдено. Ви справді використовуєте правильний обліковий запис?</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,2 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
||||
<resources>
|
||||
<string name="upgrades_gplay_unavailable_error">Các dịch vụ của Google Play không khả dụng.</string>
|
||||
<string name="upgrades_no_purchases_found_check_account">Không tìm thấy giao dịch mua nào. Bạn có đang sử dụng đúng tài khoản không?</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,2 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
||||
<resources>
|
||||
<string name="upgrades_gplay_unavailable_error">Google Play 服务不可用</string>
|
||||
<string name="upgrades_no_purchases_found_check_account">未找到购买记录,您使用的帐户正确吗?</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,2 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
||||
<resources>
|
||||
<string name="upgrades_gplay_unavailable_error">Google Play 服務不可用。</string>
|
||||
<string name="upgrades_no_purchases_found_check_account">未找到訂單,確定帳戶無誤?</string>
|
||||
</resources>
|
||||
|
||||
@@ -31,8 +31,21 @@ class BleScanner @Inject constructor(
|
||||
@SuppressLint("MissingPermission") fun scan(
|
||||
filters: Set<ScanFilter>,
|
||||
scannerMode: ScannerMode,
|
||||
compatMode: Boolean,
|
||||
): Flow<List<BleScanResult>> = callbackFlow {
|
||||
log(TAG, VERBOSE) { "scan(filters=$filters, scannerMode=$scannerMode, compatMode=$compatMode)" }
|
||||
if (compatMode) log(TAG, WARN) { "Using compatibilityMode!" }
|
||||
|
||||
val adapter = bluetoothManager.adapter
|
||||
|
||||
val supportsOffloadFiltering = adapter.isOffloadedFilteringSupported.also {
|
||||
log(TAG, if (it) DEBUG else WARN) { "isOffloadedFilteringSupported=$it" }
|
||||
} && !compatMode
|
||||
|
||||
val supportsOffloadBatching = adapter.isOffloadedScanBatchingSupported.also {
|
||||
log(TAG, if (it) DEBUG else WARN) { "isOffloadedScanBatchingSupported=$it" }
|
||||
} && !compatMode
|
||||
|
||||
val scanner = bluetoothManager.scanner
|
||||
|
||||
val callback = object : ScanCallback() {
|
||||
@@ -43,7 +56,13 @@ class BleScanner @Inject constructor(
|
||||
lastScanAt = System.currentTimeMillis()
|
||||
"onScanResult(delay=${delay}ms, callbackType=$callbackType, result=$result)"
|
||||
}
|
||||
trySend(listOf(BleScanResult.fromScanResult(result)))
|
||||
val toSend = if (supportsOffloadFiltering || filters.isEmpty() || filters.any { it.matches(result) }) {
|
||||
listOf(BleScanResult.fromScanResult(result))
|
||||
} else {
|
||||
log(TAG, VERBOSE) { "Manual filtering: No match for $result" }
|
||||
emptyList()
|
||||
}
|
||||
trySend(toSend)
|
||||
}
|
||||
|
||||
override fun onBatchScanResults(results: MutableList<ScanResult>) {
|
||||
@@ -52,7 +71,19 @@ class BleScanner @Inject constructor(
|
||||
lastScanAt = System.currentTimeMillis()
|
||||
"onBatchScanResults(delay=${delay}ms, results=$results)"
|
||||
}
|
||||
trySend(results.map { BleScanResult.fromScanResult(it) })
|
||||
|
||||
val toSend = results
|
||||
.filter { result ->
|
||||
val passed = when {
|
||||
supportsOffloadFiltering -> true
|
||||
filters.isEmpty() -> true
|
||||
else -> filters.any { it.matches(result) }
|
||||
}
|
||||
if (!passed) log(TAG, VERBOSE) { "Manually filtered $result" }
|
||||
passed
|
||||
}
|
||||
.map { BleScanResult.fromScanResult(it) }
|
||||
trySend(toSend)
|
||||
}
|
||||
|
||||
override fun onScanFailed(errorCode: Int) {
|
||||
@@ -60,6 +91,7 @@ class BleScanner @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
val settings = ScanSettings.Builder().apply {
|
||||
setScanMode(
|
||||
when (scannerMode) {
|
||||
@@ -68,20 +100,24 @@ class BleScanner @Inject constructor(
|
||||
ScannerMode.LOW_LATENCY -> ScanSettings.SCAN_MODE_LOW_LATENCY
|
||||
}
|
||||
)
|
||||
if (adapter.isOffloadedScanBatchingSupported) {
|
||||
when (scannerMode) {
|
||||
ScannerMode.LOW_POWER -> setReportDelay(2000)
|
||||
ScannerMode.BALANCED -> setReportDelay(1000)
|
||||
ScannerMode.LOW_LATENCY -> setReportDelay(500)
|
||||
}
|
||||
} else {
|
||||
log(TAG, WARN) { "isOffloadedScanBatchingSupported=false" }
|
||||
if (supportsOffloadBatching) {
|
||||
setReportDelay(
|
||||
when (scannerMode) {
|
||||
ScannerMode.LOW_POWER -> 2000L
|
||||
ScannerMode.BALANCED -> 1000L
|
||||
ScannerMode.LOW_LATENCY -> 500L
|
||||
}
|
||||
)
|
||||
}
|
||||
}.build()
|
||||
|
||||
log(TAG, VERBOSE) { "Settings created for offloaded filtering: $settings" }
|
||||
|
||||
val flushJob = launch {
|
||||
log(TAG) { "Flush job launched" }
|
||||
while (isActive) {
|
||||
// Can undercut the minimum setReportDelay(), e.g. 5000ms on a Pixel5@12
|
||||
log(TAG, VERBOSE) { "Flushing scan results." }
|
||||
adapter.bluetoothLeScanner.flushPendingScanResults(callback)
|
||||
when (scannerMode) {
|
||||
ScannerMode.LOW_POWER -> break
|
||||
@@ -91,19 +127,17 @@ class BleScanner @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
if (adapter.isOffloadedFilteringSupported) {
|
||||
scanner.startScan(filters.toList(), settings, callback)
|
||||
log(TAG, VERBOSE) { "BleScanner started (filters=$filters, settings=$settings)" }
|
||||
} else {
|
||||
log(TAG, WARN) { "isOffloadedFilteringSupported=false" }
|
||||
scanner.startScan(callback)
|
||||
log(TAG, VERBOSE) { "BleScanner started" }
|
||||
}
|
||||
scanner.startScan(
|
||||
if (supportsOffloadFiltering) filters.toList() else listOf(ScanFilter.Builder().build()),
|
||||
settings,
|
||||
callback
|
||||
)
|
||||
log(TAG) { "BleScanner started (filters=$filters, settings=$settings)" }
|
||||
|
||||
awaitClose {
|
||||
flushJob.cancel()
|
||||
scanner.stopScan(callback)
|
||||
log(TAG, INFO) { "BleScanner stopped" }
|
||||
log(TAG) { "BleScanner stopped" }
|
||||
}
|
||||
}
|
||||
.map { fakeBleData.maybeAddfakeData(it) }
|
||||
|
||||
@@ -75,6 +75,19 @@ class FakeBleData @Inject constructor(
|
||||
fakeDevices.add(this)
|
||||
}
|
||||
}
|
||||
|
||||
// Tws i99999
|
||||
BleScanResult(
|
||||
address = "5E:9E:D1:29:D2:6D",
|
||||
rssi = Random.nextInt(15, 75) * -1,
|
||||
generatedAtNanos = SystemClockWrap.elapsedRealtimeNanos + 400,
|
||||
manufacturerSpecificData = mapOf(76 to "07 13 01 02 20 71 AA 37 32 00 10 00 64 64 FF 00 00 00 00 00 00".hexToByteArray())
|
||||
).run {
|
||||
if (Random.nextBoolean()) {
|
||||
fakeDevices.add(this)
|
||||
}
|
||||
}
|
||||
|
||||
// Unknown Device
|
||||
BleScanResult(
|
||||
address = "6E:9E:D1:49:D2:6D",
|
||||
|
||||
@@ -4,8 +4,8 @@ import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import androidx.preference.PreferenceDataStore
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import eu.darken.androidstarter.common.preferences.Settings
|
||||
import eu.darken.capod.common.preferences.PreferenceStoreMapper
|
||||
import eu.darken.capod.common.preferences.Settings
|
||||
import eu.darken.capod.common.preferences.createFlowPreference
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package eu.darken.androidstarter.common.preferences
|
||||
package eu.darken.capod.common.preferences
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import androidx.preference.PreferenceDataStore
|
||||
|
||||
@@ -13,7 +13,7 @@ import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import androidx.viewbinding.ViewBinding
|
||||
import eu.darken.androidstarter.common.preferences.Settings
|
||||
import eu.darken.capod.common.preferences.Settings
|
||||
import eu.darken.capod.main.ui.settings.SettingsFragment
|
||||
|
||||
abstract class PreferenceFragment2
|
||||
|
||||
@@ -5,9 +5,9 @@ import android.content.SharedPreferences
|
||||
import androidx.preference.PreferenceDataStore
|
||||
import com.squareup.moshi.Moshi
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import eu.darken.androidstarter.common.preferences.Settings
|
||||
import eu.darken.capod.common.debug.autoreport.DebugSettings
|
||||
import eu.darken.capod.common.preferences.PreferenceStoreMapper
|
||||
import eu.darken.capod.common.preferences.Settings
|
||||
import eu.darken.capod.common.preferences.createFlowPreference
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import javax.inject.Inject
|
||||
@@ -34,6 +34,11 @@ class GeneralSettings @Inject constructor(
|
||||
moshi
|
||||
)
|
||||
|
||||
val compatibilityMode = preferences.createFlowPreference(
|
||||
"core.compatibility.enabled",
|
||||
false
|
||||
)
|
||||
|
||||
val showAll = preferences.createFlowPreference(
|
||||
"core.showall.enabled",
|
||||
false
|
||||
@@ -58,6 +63,7 @@ class GeneralSettings @Inject constructor(
|
||||
override val preferenceDataStore: PreferenceDataStore = PreferenceStoreMapper(
|
||||
monitorMode,
|
||||
scannerMode,
|
||||
compatibilityMode,
|
||||
showAll,
|
||||
minimumSignalQuality,
|
||||
mainDeviceAddress,
|
||||
|
||||
@@ -14,9 +14,8 @@ import eu.darken.capod.common.lists.modular.mods.TypedVHCreatorMod
|
||||
import eu.darken.capod.main.ui.overview.cards.BluetoothDisabledVH
|
||||
import eu.darken.capod.main.ui.overview.cards.MissingMainDeviceVH
|
||||
import eu.darken.capod.main.ui.overview.cards.PermissionCardVH
|
||||
import eu.darken.capod.main.ui.overview.cards.pods.BasicSingleApplePodsCardVH
|
||||
import eu.darken.capod.main.ui.overview.cards.pods.DualApplePodsCardVH
|
||||
import eu.darken.capod.main.ui.overview.cards.pods.SingleApplePodsCardVH
|
||||
import eu.darken.capod.main.ui.overview.cards.pods.DualPodsCardVH
|
||||
import eu.darken.capod.main.ui.overview.cards.pods.SinglePodsCardVH
|
||||
import eu.darken.capod.main.ui.overview.cards.pods.UnknownPodDeviceCardVH
|
||||
import javax.inject.Inject
|
||||
|
||||
@@ -29,9 +28,8 @@ class OverviewAdapter @Inject constructor() :
|
||||
init {
|
||||
modules.add(DataBinderMod(data))
|
||||
modules.add(TypedVHCreatorMod({ data[it] is PermissionCardVH.Item }) { PermissionCardVH(it) })
|
||||
modules.add(TypedVHCreatorMod({ data[it] is DualApplePodsCardVH.Item }) { DualApplePodsCardVH(it) })
|
||||
modules.add(TypedVHCreatorMod({ data[it] is SingleApplePodsCardVH.Item }) { SingleApplePodsCardVH(it) })
|
||||
modules.add(TypedVHCreatorMod({ data[it] is BasicSingleApplePodsCardVH.Item }) { BasicSingleApplePodsCardVH(it) })
|
||||
modules.add(TypedVHCreatorMod({ data[it] is DualPodsCardVH.Item }) { DualPodsCardVH(it) })
|
||||
modules.add(TypedVHCreatorMod({ data[it] is SinglePodsCardVH.Item }) { SinglePodsCardVH(it) })
|
||||
modules.add(TypedVHCreatorMod({ data[it] is MissingMainDeviceVH.Item }) { MissingMainDeviceVH(it) })
|
||||
modules.add(TypedVHCreatorMod({ data[it] is BluetoothDisabledVH.Item }) { BluetoothDisabledVH(it) })
|
||||
modules.add(TypedVHCreatorMod({ data[it] is UnknownPodDeviceCardVH.Item }) { UnknownPodDeviceCardVH(it) })
|
||||
|
||||
@@ -10,6 +10,7 @@ import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.fragment.app.viewModels
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import eu.darken.capod.BuildConfig
|
||||
import eu.darken.capod.R
|
||||
import eu.darken.capod.common.colorString
|
||||
import eu.darken.capod.common.debug.logging.log
|
||||
@@ -71,6 +72,7 @@ class OverviewFragment : Fragment3(R.layout.main_fragment) {
|
||||
}
|
||||
|
||||
vm.listItems.observe2(ui) {
|
||||
if (BuildConfig.DEBUG) toolbar.subtitle = "${it.size} items"
|
||||
adapter.update(it)
|
||||
}
|
||||
|
||||
|
||||
@@ -20,23 +20,23 @@ import eu.darken.capod.main.core.PermissionTool
|
||||
import eu.darken.capod.main.ui.overview.cards.BluetoothDisabledVH
|
||||
import eu.darken.capod.main.ui.overview.cards.MissingMainDeviceVH
|
||||
import eu.darken.capod.main.ui.overview.cards.PermissionCardVH
|
||||
import eu.darken.capod.main.ui.overview.cards.pods.*
|
||||
import eu.darken.capod.main.ui.overview.cards.pods.DualPodsCardVH
|
||||
import eu.darken.capod.main.ui.overview.cards.pods.SinglePodsCardVH
|
||||
import eu.darken.capod.main.ui.overview.cards.pods.UnknownPodDeviceCardVH
|
||||
import eu.darken.capod.monitor.core.PodMonitor
|
||||
import eu.darken.capod.monitor.core.worker.MonitorControl
|
||||
import eu.darken.capod.pods.core.DualPodDevice
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.apple.BasicSingleApplePods
|
||||
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||
import eu.darken.capod.pods.core.apple.SingleApplePods
|
||||
import eu.darken.capod.pods.core.SinglePodDevice
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.isActive
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class OverviewFragmentVM @Inject constructor(
|
||||
handle: SavedStateHandle,
|
||||
@Suppress("UNUSED_PARAMETER") handle: SavedStateHandle,
|
||||
dispatcherProvider: DispatcherProvider,
|
||||
private val monitorControl: MonitorControl,
|
||||
private val podMonitor: PodMonitor,
|
||||
@@ -74,7 +74,7 @@ class OverviewFragmentVM @Inject constructor(
|
||||
monitorControl.startMonitor()
|
||||
}
|
||||
}
|
||||
.map { Unit }
|
||||
.map { }
|
||||
.asLiveData2()
|
||||
|
||||
val requestPermissionEvent = SingleLiveEvent<Permission>()
|
||||
@@ -129,19 +129,13 @@ class OverviewFragmentVM @Inject constructor(
|
||||
pods.map {
|
||||
val now = Instant.now()
|
||||
when (it) {
|
||||
is DualApplePods -> DualApplePodsCardVH.Item(
|
||||
is DualPodDevice -> DualPodsCardVH.Item(
|
||||
now = now,
|
||||
device = it,
|
||||
showDebug = isDebugMode,
|
||||
isMainPod = it == mainPod,
|
||||
)
|
||||
is SingleApplePods -> SingleApplePodsCardVH.Item(
|
||||
now = now,
|
||||
device = it,
|
||||
showDebug = isDebugMode,
|
||||
isMainPod = it == mainPod,
|
||||
)
|
||||
is BasicSingleApplePods -> BasicSingleApplePodsCardVH.Item(
|
||||
is SinglePodDevice -> SinglePodsCardVH.Item(
|
||||
now = now,
|
||||
device = it,
|
||||
showDebug = isDebugMode,
|
||||
|
||||
-58
@@ -1,58 +0,0 @@
|
||||
package eu.darken.capod.main.ui.overview.cards.pods
|
||||
|
||||
import android.graphics.Typeface
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.isGone
|
||||
import eu.darken.capod.R
|
||||
import eu.darken.capod.common.lists.binding
|
||||
import eu.darken.capod.databinding.OverviewPodsAppleSingleBasicItemBinding
|
||||
import eu.darken.capod.pods.core.apple.BasicSingleApplePods
|
||||
import eu.darken.capod.pods.core.getBatteryDrawable
|
||||
import eu.darken.capod.pods.core.getBatteryLevelHeadset
|
||||
import eu.darken.capod.pods.core.lastSeenFormatted
|
||||
import java.time.Instant
|
||||
|
||||
class BasicSingleApplePodsCardVH(parent: ViewGroup) :
|
||||
PodDeviceVH<BasicSingleApplePodsCardVH.Item, OverviewPodsAppleSingleBasicItemBinding>(
|
||||
R.layout.overview_pods_apple_single_basic_item,
|
||||
parent
|
||||
) {
|
||||
|
||||
override val viewBinding = lazy { OverviewPodsAppleSingleBasicItemBinding.bind(itemView) }
|
||||
|
||||
override val onBindData = binding(payload = true) { item: Item ->
|
||||
val device = item.device
|
||||
|
||||
name.apply {
|
||||
text = device.getLabel(context)
|
||||
if (item.isMainPod) setTypeface(typeface, Typeface.BOLD)
|
||||
else setTypeface(typeface, Typeface.NORMAL)
|
||||
}
|
||||
|
||||
deviceIcon.setImageResource(device.iconRes)
|
||||
|
||||
lastSeen.text = device.lastSeenFormatted(item.now)
|
||||
|
||||
reception.text = item.getReceptionText()
|
||||
|
||||
batteryLabel.text = device.getBatteryLevelHeadset(context)
|
||||
batteryIcon.setImageResource(getBatteryDrawable(device.batteryHeadsetPercent))
|
||||
|
||||
status.apply {
|
||||
val sb = StringBuilder()
|
||||
if (item.showDebug) {
|
||||
sb.append("--- Debug ---")
|
||||
sb.append("\n").append(device.rawDataHex)
|
||||
}
|
||||
text = sb
|
||||
isGone = !item.showDebug
|
||||
}
|
||||
}
|
||||
|
||||
data class Item(
|
||||
override val now: Instant,
|
||||
override val device: BasicSingleApplePods,
|
||||
override val showDebug: Boolean,
|
||||
override val isMainPod: Boolean,
|
||||
) : PodDeviceVH.Item
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
package eu.darken.capod.main.ui.overview.cards.pods
|
||||
|
||||
import android.graphics.Typeface
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isInvisible
|
||||
import eu.darken.capod.R
|
||||
import eu.darken.capod.common.lists.binding
|
||||
import eu.darken.capod.databinding.OverviewPodsAppleDualItemBinding
|
||||
import eu.darken.capod.pods.core.*
|
||||
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||
import eu.darken.capod.pods.core.apple.DualApplePods.DeviceColor
|
||||
import eu.darken.capod.pods.core.apple.DualApplePods.LidState
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
|
||||
class DualApplePodsCardVH(parent: ViewGroup) :
|
||||
PodDeviceVH<DualApplePodsCardVH.Item, OverviewPodsAppleDualItemBinding>(
|
||||
R.layout.overview_pods_apple_dual_item,
|
||||
parent
|
||||
) {
|
||||
|
||||
override val viewBinding = lazy { OverviewPodsAppleDualItemBinding.bind(itemView) }
|
||||
|
||||
override val onBindData = binding(payload = true) { item: Item ->
|
||||
val device = item.device
|
||||
name.apply {
|
||||
val sb = StringBuilder(device.getLabel(context))
|
||||
if (!listOf(DeviceColor.WHITE, DeviceColor.UNKNOWN).contains(device.deviceColor)) {
|
||||
sb.append(" (${device.deviceColor.name})")
|
||||
}
|
||||
text = sb
|
||||
if (item.isMainPod) setTypeface(typeface, Typeface.BOLD)
|
||||
else setTypeface(typeface, Typeface.NORMAL)
|
||||
if (item.showDebug) {
|
||||
append(" [${device.primaryPod.name}]")
|
||||
}
|
||||
}
|
||||
deviceIcon.setImageResource(device.iconRes)
|
||||
|
||||
lastSeen.text = context.getString(R.string.last_seen_x, device.lastSeenFormatted(item.now))
|
||||
firstSeen.text = context.getString(R.string.first_seen_x, device.firstSeenFormatted(item.now))
|
||||
firstSeen.isGone = Duration.between(device.seenFirstAt, device.seenLastAt).toMinutes() < 1
|
||||
|
||||
reception.text = item.getReceptionText()
|
||||
|
||||
// Left Pod
|
||||
device.apply {
|
||||
podLeftBatteryIcon.setImageResource(getBatteryDrawable(batteryLeftPodPercent))
|
||||
podLeftBatteryLabel.text = getBatteryLevelLeftPod(context)
|
||||
|
||||
podLeftChargingIcon.isInvisible = !isLeftPodCharging
|
||||
podLeftChargingLabel.isInvisible = !isLeftPodCharging
|
||||
|
||||
podLeftMicrophoneIcon.isInvisible = !isLeftPodMicrophone
|
||||
podLeftMicrophoneLabel.isInvisible = !isLeftPodMicrophone
|
||||
|
||||
podLeftWearIcon.isInvisible = !isLeftPodInEar
|
||||
podLeftWearLabel.isInvisible = !isLeftPodInEar
|
||||
}
|
||||
|
||||
// Right Pod
|
||||
device.apply {
|
||||
podRightBatteryIcon.setImageResource(getBatteryDrawable(batteryRightPodPercent))
|
||||
podRightBatteryLabel.text = getBatteryLevelRightPod(context)
|
||||
|
||||
podRightChargingIcon.isInvisible = !isRightPodCharging
|
||||
podRightChargingLabel.isInvisible = !isRightPodCharging
|
||||
|
||||
podRightMicrophoneIcon.isInvisible = !isRightPodMicrophone
|
||||
podRightMicrophoneLabel.isInvisible = !isRightPodMicrophone
|
||||
|
||||
podRightWearIcon.isInvisible = !isRightPodInEar
|
||||
podRightWearLabel.isInvisible = !isRightPodInEar
|
||||
}
|
||||
|
||||
// Case
|
||||
device.apply {
|
||||
podCaseBatteryIcon.setImageResource(getBatteryDrawable(batteryCasePercent))
|
||||
podCaseBatteryLabel.text = getBatteryLevelCase(context)
|
||||
|
||||
podCaseChargingIcon.isInvisible = !isCaseCharging
|
||||
podCaseChargingLabel.isInvisible = !isCaseCharging
|
||||
|
||||
podCaseLidLabel.text = when (caseLidState) {
|
||||
LidState.OPEN -> context.getString(R.string.pods_case_status_open_label)
|
||||
LidState.CLOSED -> context.getString(R.string.pods_case_status_closed_label)
|
||||
else -> context.getString(R.string.general_value_unknown_label)
|
||||
}
|
||||
|
||||
val hideInfo = !listOf(LidState.OPEN, LidState.CLOSED).contains(caseLidState)
|
||||
podCaseLidIcon.isInvisible = hideInfo
|
||||
podCaseLidLabel.isInvisible = hideInfo
|
||||
}
|
||||
|
||||
status.apply {
|
||||
val sb = StringBuilder(device.getConnectionStateLabel(context))
|
||||
if (item.showDebug) {
|
||||
sb.append("\n\n").append("---Debug---")
|
||||
sb.append("\n").append(device.rawDataHex)
|
||||
}
|
||||
text = sb
|
||||
}
|
||||
}
|
||||
|
||||
data class Item(
|
||||
override val now: Instant,
|
||||
override val device: DualApplePods,
|
||||
override val showDebug: Boolean,
|
||||
override val isMainPod: Boolean,
|
||||
) : PodDeviceVH.Item
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
package eu.darken.capod.main.ui.overview.cards.pods
|
||||
|
||||
import android.graphics.Typeface
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isInvisible
|
||||
import eu.darken.capod.R
|
||||
import eu.darken.capod.common.lists.binding
|
||||
import eu.darken.capod.databinding.OverviewPodsDualItemBinding
|
||||
import eu.darken.capod.pods.core.*
|
||||
import eu.darken.capod.pods.core.apple.DualAirPods
|
||||
import eu.darken.capod.pods.core.apple.DualAirPods.LidState
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
|
||||
class DualPodsCardVH(parent: ViewGroup) :
|
||||
PodDeviceVH<DualPodsCardVH.Item, OverviewPodsDualItemBinding>(
|
||||
R.layout.overview_pods_dual_item,
|
||||
parent
|
||||
) {
|
||||
|
||||
override val viewBinding = lazy { OverviewPodsDualItemBinding.bind(itemView) }
|
||||
|
||||
override val onBindData = binding(payload = true) { item: Item ->
|
||||
val device = item.device
|
||||
name.apply {
|
||||
val sb = StringBuilder(device.getLabel(context))
|
||||
if (device is HasPodStyle && item.showDebug) {
|
||||
val style = device.podStyle
|
||||
sb.append(" (${style.getColor(context)})")
|
||||
}
|
||||
text = sb
|
||||
|
||||
if (item.isMainPod) setTypeface(typeface, Typeface.BOLD)
|
||||
else setTypeface(typeface, Typeface.NORMAL)
|
||||
|
||||
if (device is DualAirPods && item.showDebug) {
|
||||
append(" [${device.primaryPod.name}]")
|
||||
}
|
||||
}
|
||||
deviceIcon.setImageResource(device.iconRes)
|
||||
|
||||
lastSeen.text = context.getString(R.string.last_seen_x, device.lastSeenFormatted(item.now))
|
||||
firstSeen.text = context.getString(R.string.first_seen_x, device.firstSeenFormatted(item.now))
|
||||
firstSeen.isGone = Duration.between(device.seenFirstAt, device.seenLastAt).toMinutes() < 1
|
||||
|
||||
reception.text = item.getReceptionText()
|
||||
|
||||
// Pods battery state
|
||||
device.apply {
|
||||
podLeftBatteryIcon.setImageResource(getBatteryDrawable(batteryLeftPodPercent))
|
||||
podLeftBatteryLabel.text = getBatteryLevelLeftPod(context)
|
||||
|
||||
podRightBatteryIcon.setImageResource(getBatteryDrawable(batteryRightPodPercent))
|
||||
podRightBatteryLabel.text = getBatteryLevelRightPod(context)
|
||||
}
|
||||
|
||||
// Pods charging state
|
||||
device.apply {
|
||||
if (this is HasChargeDetectionDual) {
|
||||
podLeftChargingIcon.isInvisible = !isLeftPodCharging
|
||||
podLeftChargingLabel.isInvisible = !isLeftPodCharging
|
||||
|
||||
podRightChargingIcon.isInvisible = !isRightPodCharging
|
||||
podRightChargingLabel.isInvisible = !isRightPodCharging
|
||||
} else {
|
||||
podLeftChargingIcon.isGone = true
|
||||
podLeftChargingLabel.isGone = true
|
||||
|
||||
podRightChargingIcon.isGone = true
|
||||
podRightChargingLabel.isGone = true
|
||||
}
|
||||
}
|
||||
|
||||
// Microphone state
|
||||
device.apply {
|
||||
if (this is HasDualMicrophone) {
|
||||
podLeftMicrophoneIcon.isInvisible = !isLeftPodMicrophone
|
||||
podLeftMicrophoneLabel.isInvisible = !isLeftPodMicrophone
|
||||
|
||||
podRightMicrophoneIcon.isInvisible = !isRightPodMicrophone
|
||||
podRightMicrophoneLabel.isInvisible = !isRightPodMicrophone
|
||||
} else {
|
||||
podLeftMicrophoneIcon.isGone = true
|
||||
podLeftMicrophoneLabel.isGone = true
|
||||
|
||||
podRightMicrophoneIcon.isGone = true
|
||||
podRightMicrophoneLabel.isGone = true
|
||||
}
|
||||
}
|
||||
|
||||
// Pods wear state
|
||||
device.apply {
|
||||
if (this is HasEarDetectionDual) {
|
||||
podLeftWearIcon.isInvisible = !isLeftPodInEar
|
||||
podLeftWearLabel.isInvisible = !isLeftPodInEar
|
||||
|
||||
podRightWearIcon.isInvisible = !isRightPodInEar
|
||||
podRightWearLabel.isInvisible = !isRightPodInEar
|
||||
} else {
|
||||
podLeftWearIcon.isGone = true
|
||||
podLeftWearLabel.isGone = true
|
||||
|
||||
podRightWearIcon.isGone = true
|
||||
podRightWearLabel.isGone = true
|
||||
}
|
||||
}
|
||||
|
||||
// Case charge state
|
||||
device.apply {
|
||||
if (this is HasCase) {
|
||||
podCaseBatteryIcon.isGone = false
|
||||
podCaseBatteryIcon.setImageResource(getBatteryDrawable(batteryCasePercent))
|
||||
podCaseBatteryLabel.text = getBatteryLevelCase(context)
|
||||
|
||||
podCaseChargingIcon.isInvisible = !isCaseCharging
|
||||
podCaseChargingLabel.isInvisible = !isCaseCharging
|
||||
} else {
|
||||
podCaseBatteryIcon.isGone = true
|
||||
podCaseBatteryLabel.isGone = true
|
||||
|
||||
podCaseChargingIcon.isGone = true
|
||||
podCaseChargingLabel.isGone = true
|
||||
}
|
||||
}
|
||||
|
||||
// Case lid state
|
||||
device.apply {
|
||||
if (this is DualAirPods) {
|
||||
podCaseLidLabel.text = when (caseLidState) {
|
||||
LidState.OPEN -> context.getString(R.string.pods_case_status_open_label)
|
||||
LidState.CLOSED -> context.getString(R.string.pods_case_status_closed_label)
|
||||
else -> context.getString(R.string.pods_case_unknown_state)
|
||||
}
|
||||
|
||||
val hideInfo = !listOf(LidState.OPEN, LidState.CLOSED).contains(caseLidState)
|
||||
podCaseLidIcon.isInvisible = hideInfo
|
||||
podCaseLidLabel.isInvisible = hideInfo
|
||||
} else {
|
||||
podCaseLidIcon.isGone = true
|
||||
podCaseLidLabel.isGone = true
|
||||
}
|
||||
}
|
||||
|
||||
// Connection state
|
||||
device.apply {
|
||||
val sb = StringBuilder()
|
||||
if (this is HasStateDetection) {
|
||||
sb.append(state.getLabel(context))
|
||||
}
|
||||
if (item.showDebug) {
|
||||
sb.append("\n\n").append("---Debug---")
|
||||
sb.append("\n").append(rawDataHex)
|
||||
}
|
||||
status.text = sb
|
||||
status.isGone = sb.isEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
data class Item(
|
||||
override val now: Instant,
|
||||
override val device: DualPodDevice,
|
||||
override val showDebug: Boolean,
|
||||
override val isMainPod: Boolean,
|
||||
) : PodDeviceVH.Item
|
||||
}
|
||||
-65
@@ -1,65 +0,0 @@
|
||||
package eu.darken.capod.main.ui.overview.cards.pods
|
||||
|
||||
import android.graphics.Typeface
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isInvisible
|
||||
import eu.darken.capod.R
|
||||
import eu.darken.capod.common.lists.binding
|
||||
import eu.darken.capod.databinding.OverviewPodsAppleSingleItemBinding
|
||||
import eu.darken.capod.pods.core.apple.SingleApplePods
|
||||
import eu.darken.capod.pods.core.getBatteryDrawable
|
||||
import eu.darken.capod.pods.core.getBatteryLevelHeadset
|
||||
import eu.darken.capod.pods.core.lastSeenFormatted
|
||||
import java.time.Instant
|
||||
|
||||
class SingleApplePodsCardVH(parent: ViewGroup) :
|
||||
PodDeviceVH<SingleApplePodsCardVH.Item, OverviewPodsAppleSingleItemBinding>(
|
||||
R.layout.overview_pods_apple_single_item,
|
||||
parent
|
||||
) {
|
||||
|
||||
override val viewBinding = lazy { OverviewPodsAppleSingleItemBinding.bind(itemView) }
|
||||
|
||||
override val onBindData = binding(payload = true) { item: Item ->
|
||||
val device = item.device
|
||||
|
||||
name.apply {
|
||||
text = device.getLabel(context)
|
||||
if (item.isMainPod) setTypeface(typeface, Typeface.BOLD)
|
||||
else setTypeface(typeface, Typeface.NORMAL)
|
||||
}
|
||||
|
||||
deviceIcon.setImageResource(device.iconRes)
|
||||
|
||||
lastSeen.text = device.lastSeenFormatted(item.now)
|
||||
|
||||
reception.text = item.getReceptionText()
|
||||
|
||||
batteryLabel.text = device.getBatteryLevelHeadset(context)
|
||||
batteryIcon.setImageResource(getBatteryDrawable(device.batteryHeadsetPercent))
|
||||
|
||||
chargingIcon.isInvisible = device.isHeadsetBeingCharged
|
||||
chargingLabel.isInvisible = device.isHeadsetBeingCharged
|
||||
|
||||
wearIcon.isInvisible = device.isHeadphonesBeingWorn
|
||||
wearLabel.isInvisible = device.isHeadphonesBeingWorn
|
||||
|
||||
status.apply {
|
||||
val sb = StringBuilder()
|
||||
if (item.showDebug) {
|
||||
sb.append("--- Debug ---")
|
||||
sb.append("\n").append(device.rawDataHex)
|
||||
}
|
||||
text = sb
|
||||
isGone = !item.showDebug
|
||||
}
|
||||
}
|
||||
|
||||
data class Item(
|
||||
override val now: Instant,
|
||||
override val device: SingleApplePods,
|
||||
override val showDebug: Boolean,
|
||||
override val isMainPod: Boolean,
|
||||
) : PodDeviceVH.Item
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package eu.darken.capod.main.ui.overview.cards.pods
|
||||
|
||||
import android.graphics.Typeface
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isInvisible
|
||||
import eu.darken.capod.R
|
||||
import eu.darken.capod.common.lists.binding
|
||||
import eu.darken.capod.databinding.OverviewPodsSingleItemBinding
|
||||
import eu.darken.capod.pods.core.*
|
||||
import java.time.Instant
|
||||
|
||||
class SinglePodsCardVH(parent: ViewGroup) :
|
||||
PodDeviceVH<SinglePodsCardVH.Item, OverviewPodsSingleItemBinding>(
|
||||
R.layout.overview_pods_single_item,
|
||||
parent
|
||||
) {
|
||||
|
||||
override val viewBinding = lazy { OverviewPodsSingleItemBinding.bind(itemView) }
|
||||
|
||||
override val onBindData = binding(payload = true) { item: Item ->
|
||||
val device = item.device
|
||||
|
||||
name.apply {
|
||||
text = device.getLabel(context)
|
||||
if (item.isMainPod) setTypeface(typeface, Typeface.BOLD)
|
||||
else setTypeface(typeface, Typeface.NORMAL)
|
||||
}
|
||||
|
||||
deviceIcon.setImageResource(device.iconRes)
|
||||
|
||||
lastSeen.text = device.lastSeenFormatted(item.now)
|
||||
|
||||
reception.text = item.getReceptionText()
|
||||
|
||||
// Battery level
|
||||
device.apply {
|
||||
batteryLabel.text = getBatteryLevelHeadset(context)
|
||||
batteryIcon.setImageResource(getBatteryDrawable(batteryHeadsetPercent))
|
||||
}
|
||||
|
||||
// Charge state
|
||||
device.apply {
|
||||
if (this is HasChargeDetection) {
|
||||
chargingIcon.isInvisible = isHeadsetBeingCharged
|
||||
chargingLabel.isInvisible = isHeadsetBeingCharged
|
||||
} else {
|
||||
chargingIcon.isGone = true
|
||||
chargingLabel.isGone = true
|
||||
}
|
||||
}
|
||||
|
||||
// Has ear detection
|
||||
device.apply {
|
||||
if (this is HasEarDetection) {
|
||||
wearIcon.isInvisible = isBeingWorn
|
||||
wearLabel.isInvisible = isBeingWorn
|
||||
} else {
|
||||
wearIcon.isGone = true
|
||||
wearLabel.isGone = true
|
||||
}
|
||||
}
|
||||
|
||||
status.apply {
|
||||
val sb = StringBuilder()
|
||||
if (item.showDebug) {
|
||||
sb.append("--- Debug ---")
|
||||
sb.append("\n").append(device.rawDataHex)
|
||||
}
|
||||
text = sb
|
||||
isGone = !item.showDebug
|
||||
}
|
||||
}
|
||||
|
||||
data class Item(
|
||||
override val now: Instant,
|
||||
override val device: SinglePodDevice,
|
||||
override val showDebug: Boolean,
|
||||
override val isMainPod: Boolean,
|
||||
) : PodDeviceVH.Item
|
||||
}
|
||||
@@ -4,11 +4,11 @@ import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.preference.Preference
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import eu.darken.androidstarter.common.preferences.Settings
|
||||
import eu.darken.capod.R
|
||||
import eu.darken.capod.common.BuildConfigWrap
|
||||
import eu.darken.capod.common.PrivacyPolicy
|
||||
import eu.darken.capod.common.WebpageTool
|
||||
import eu.darken.capod.common.preferences.Settings
|
||||
import eu.darken.capod.common.uix.PreferenceFragment2
|
||||
import eu.darken.capod.main.core.GeneralSettings
|
||||
import javax.inject.Inject
|
||||
|
||||
@@ -16,7 +16,6 @@ import eu.darken.capod.main.core.GeneralSettings
|
||||
import eu.darken.capod.main.core.ScannerMode
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.PodFactory
|
||||
import eu.darken.capod.pods.core.apple.protocol.ContinuityProtocol
|
||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
@@ -62,19 +61,23 @@ class PodMonitor @Inject constructor(
|
||||
log(TAG) { "Bluetooth is enabled" }
|
||||
combine(
|
||||
generalSettings.scannerMode.flow,
|
||||
generalSettings.compatibilityMode.flow,
|
||||
debugSettings.showUnfiltered.flow
|
||||
) { mode, unfiltered -> mode to unfiltered }
|
||||
.flatMapLatest { (mode, unfiltered) ->
|
||||
val filters = if (unfiltered) {
|
||||
setOf(getUnfilteredFilter())
|
||||
} else {
|
||||
ProximityPairing.getBleScanFilter()
|
||||
}
|
||||
bleScanner.scan(
|
||||
filters = filters,
|
||||
scannerMode = mode
|
||||
).map { it.preFilterAndMap(mode) }
|
||||
) { scannerMode, compatMode, unfiltered ->
|
||||
Triple(scannerMode, compatMode, unfiltered)
|
||||
}.flatMapLatest { (mode, compat, unfiltered) ->
|
||||
log(TAG, VERBOSE) { "Starting BLEScanner mode=$mode, compat=$compat, unfiltered=$unfiltered" }
|
||||
val filters = if (unfiltered) {
|
||||
setOf(getUnfilteredFilter())
|
||||
} else {
|
||||
ProximityPairing.getBleScanFilter()
|
||||
}
|
||||
bleScanner.scan(
|
||||
filters = filters,
|
||||
scannerMode = mode,
|
||||
compatMode = compat,
|
||||
).map { it.preFilterAndMap(mode) }
|
||||
}
|
||||
} else {
|
||||
log(TAG, WARN) { "Bluetooth is currently disabled" }
|
||||
flowOf(null)
|
||||
@@ -160,23 +163,7 @@ class PodMonitor @Inject constructor(
|
||||
|
||||
|
||||
private fun getUnfilteredFilter(): ScanFilter {
|
||||
val manufacturerData = ByteArray(2).apply {
|
||||
this[0] = 0
|
||||
this[1] = 0
|
||||
}
|
||||
|
||||
val manufacturerDataMask = ByteArray(2).apply {
|
||||
this[0] = 0
|
||||
this[1] = 0
|
||||
}
|
||||
val builder = ScanFilter.Builder().apply {
|
||||
setManufacturerData(
|
||||
ContinuityProtocol.APPLE_COMPANY_IDENTIFIER,
|
||||
manufacturerData,
|
||||
manufacturerDataMask
|
||||
)
|
||||
}
|
||||
return builder.build()
|
||||
return ScanFilter.Builder().build()
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -6,8 +6,7 @@ import android.widget.RemoteViews
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import eu.darken.capod.R
|
||||
import eu.darken.capod.pods.core.*
|
||||
import eu.darken.capod.pods.core.apple.BasicSingleApplePods
|
||||
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||
import eu.darken.capod.pods.core.apple.DualAirPods
|
||||
import eu.darken.capod.pods.core.apple.SingleApplePods
|
||||
import javax.inject.Inject
|
||||
|
||||
@@ -17,13 +16,12 @@ class MonitorNotificationViewFactory @Inject constructor(
|
||||
) {
|
||||
|
||||
fun createContentView(device: PodDevice): RemoteViews = when (device) {
|
||||
is DualApplePods -> createDualApplePods(device)
|
||||
is DualAirPods -> createDualApplePods(device)
|
||||
is SingleApplePods -> createSingleApplePods(device)
|
||||
is BasicSingleApplePods -> createSingleBasicApplePods(device)
|
||||
else -> createUnknownDevice(device)
|
||||
}
|
||||
|
||||
private fun createDualApplePods(device: DualApplePods): RemoteViews = RemoteViews(
|
||||
private fun createDualApplePods(device: DualAirPods): RemoteViews = RemoteViews(
|
||||
context.packageName,
|
||||
R.layout.monitor_notification_dual_pods_small
|
||||
).apply {
|
||||
@@ -52,19 +50,12 @@ class MonitorNotificationViewFactory @Inject constructor(
|
||||
setTextViewText(R.id.headphones_label, getLabel(context))
|
||||
setImageViewResource(R.id.headphones_battery_icon, getBatteryDrawable(batteryHeadsetPercent))
|
||||
setTextViewText(R.id.headphones_battery_label, getBatteryLevelHeadset(context))
|
||||
setViewVisibility(R.id.headphones_charging, if (isHeadsetBeingCharged) View.VISIBLE else View.GONE)
|
||||
setViewVisibility(R.id.headphones_worn, if (isHeadphonesBeingWorn) View.VISIBLE else View.GONE)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createSingleBasicApplePods(device: BasicSingleApplePods): RemoteViews = RemoteViews(
|
||||
context.packageName,
|
||||
R.layout.monitor_notification_single_pods_basic_small
|
||||
).apply {
|
||||
device.apply {
|
||||
setTextViewText(R.id.headphones_label, getLabel(context))
|
||||
setImageViewResource(R.id.headphones_battery_icon, getBatteryDrawable(batteryHeadsetPercent))
|
||||
setTextViewText(R.id.headphones_battery_label, getBatteryLevelHeadset(context))
|
||||
if (this is HasEarDetection) {
|
||||
setViewVisibility(R.id.headphones_worn, if (isBeingWorn) View.VISIBLE else View.GONE)
|
||||
}
|
||||
if (this is HasChargeDetectionDual) {
|
||||
setViewVisibility(R.id.headphones_charging, if (isHeadsetBeingCharged) View.VISIBLE else View.GONE)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
package eu.darken.capod.pods.core
|
||||
|
||||
interface HasDualPods {
|
||||
interface DualPodDevice : PodDevice {
|
||||
|
||||
enum class Pod {
|
||||
LEFT,
|
||||
@@ -0,0 +1,7 @@
|
||||
package eu.darken.capod.pods.core
|
||||
|
||||
interface HasChargeDetection {
|
||||
|
||||
val isHeadsetBeingCharged: Boolean
|
||||
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package eu.darken.capod.pods.core
|
||||
|
||||
interface HasChargeDetectionDual : HasChargeDetection {
|
||||
|
||||
val isLeftPodCharging: Boolean
|
||||
|
||||
val isRightPodCharging: Boolean
|
||||
|
||||
val isEitherPodCharging: Boolean
|
||||
get() = isLeftPodCharging || isRightPodCharging
|
||||
|
||||
override val isHeadsetBeingCharged: Boolean
|
||||
get() = isEitherPodCharging
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package eu.darken.capod.pods.core
|
||||
|
||||
interface HasDualMicrophone {
|
||||
|
||||
val isLeftPodMicrophone: Boolean
|
||||
|
||||
val isRightPodMicrophone: Boolean
|
||||
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
package eu.darken.capod.pods.core
|
||||
|
||||
interface HasEarDetection : PodDevice {
|
||||
interface HasEarDetection {
|
||||
|
||||
val isBeingWorn: Boolean
|
||||
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
package eu.darken.capod.pods.core
|
||||
|
||||
interface HasDualEarDetection : HasEarDetection {
|
||||
interface HasEarDetectionDual : HasEarDetection {
|
||||
|
||||
val isLeftPodInEar: Boolean
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package eu.darken.capod.pods.core
|
||||
|
||||
import android.content.Context
|
||||
import androidx.annotation.ColorRes
|
||||
|
||||
interface HasPodStyle {
|
||||
|
||||
val podStyle: PodStyle
|
||||
|
||||
interface PodStyle {
|
||||
fun getLabel(context: Context): String
|
||||
|
||||
@ColorRes
|
||||
fun getColor(context: Context): Int
|
||||
|
||||
val identifier: String
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package eu.darken.capod.pods.core
|
||||
|
||||
import android.content.Context
|
||||
|
||||
interface HasStateDetection {
|
||||
|
||||
val state: State
|
||||
|
||||
interface State {
|
||||
fun getLabel(context: Context): String
|
||||
}
|
||||
}
|
||||
@@ -100,6 +100,9 @@ interface PodDevice {
|
||||
@Json(name = "beats.powerbeats.pro") POWERBEATS_PRO(
|
||||
"Power Beats Pro"
|
||||
),
|
||||
@Json(name = "fakes.tws.i99999") TWS_I99999(
|
||||
"TWS i99999"
|
||||
),
|
||||
@Json(name = "unknown") UNKNOWN(
|
||||
"Unknown"
|
||||
);
|
||||
|
||||
@@ -8,11 +8,11 @@ import java.time.Duration
|
||||
import java.time.Instant
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
fun HasDualPods.getBatteryLevelLeftPod(context: Context): String =
|
||||
fun DualPodDevice.getBatteryLevelLeftPod(context: Context): String =
|
||||
batteryLeftPodPercent?.let { "${(it * 100).roundToInt()}%" }
|
||||
?: context.getString(R.string.general_value_not_available_label)
|
||||
|
||||
fun HasDualPods.getBatteryLevelRightPod(context: Context): String =
|
||||
fun DualPodDevice.getBatteryLevelRightPod(context: Context): String =
|
||||
batteryRightPodPercent?.let { "${(it * 100).roundToInt()}%" }
|
||||
?: context.getString(R.string.general_value_not_available_label)
|
||||
|
||||
@@ -20,7 +20,7 @@ fun HasCase.getBatteryLevelCase(context: Context): String =
|
||||
batteryCasePercent?.let { "${(it * 100).roundToInt()}%" }
|
||||
?: context.getString(R.string.general_value_not_available_label)
|
||||
|
||||
fun HasSinglePod.getBatteryLevelHeadset(context: Context): String =
|
||||
fun SinglePodDevice.getBatteryLevelHeadset(context: Context): String =
|
||||
batteryHeadsetPercent?.let { "${(it * 100).roundToInt()}%" }
|
||||
?: context.getString(R.string.general_value_not_available_label)
|
||||
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
package eu.darken.capod.pods.core
|
||||
|
||||
interface HasSinglePod {
|
||||
interface SinglePodDevice : PodDevice {
|
||||
|
||||
val batteryHeadsetPercent: Float?
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
package eu.darken.capod.pods.core.apple
|
||||
|
||||
import eu.darken.capod.common.bluetooth.BleScanResult
|
||||
import eu.darken.capod.common.debug.Bugs
|
||||
import eu.darken.capod.common.debug.logging.Logging.Priority.WARN
|
||||
import eu.darken.capod.common.debug.logging.asLog
|
||||
import eu.darken.capod.common.debug.logging.log
|
||||
import eu.darken.capod.common.debug.logging.logTag
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.apple.misc.UnknownAppleDevice
|
||||
import eu.darken.capod.pods.core.apple.protocol.ContinuityProtocol
|
||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
@@ -54,25 +54,13 @@ class AppleFactory @Inject constructor(
|
||||
|
||||
val factory = podFactories.firstOrNull { it.isResponsible(pm) }
|
||||
|
||||
val device = (factory ?: unknownAppleFactory).create(
|
||||
return@withLock (factory ?: unknownAppleFactory).create(
|
||||
scanResult = scanResult,
|
||||
proximityMessage = pm,
|
||||
message = pm,
|
||||
)
|
||||
|
||||
if (factory == null && !SILENCED_PMS.contains(device.rawDeviceModel) && scanResult.address != "6E:9E:D1:49:D2:6D") {
|
||||
SILENCED_PMS.add(device.rawDeviceModel)
|
||||
Bugs.report(
|
||||
tag = TAG,
|
||||
message = "Unknown proximity message type",
|
||||
exception = IllegalArgumentException("Unknown ProximityMessage: $pm")
|
||||
)
|
||||
}
|
||||
|
||||
return@withLock device
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val SILENCED_PMS = mutableSetOf<UShort>()
|
||||
private val TAG = logTag("Pod", "Apple", "Factory")
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import dagger.hilt.components.SingletonComponent
|
||||
import dagger.multibindings.IntoSet
|
||||
import eu.darken.capod.pods.core.apple.airpods.*
|
||||
import eu.darken.capod.pods.core.apple.beats.*
|
||||
import eu.darken.capod.pods.core.apple.misc.Twsi99999
|
||||
|
||||
@InstallIn(SingletonComponent::class)
|
||||
@Module
|
||||
@@ -25,4 +26,6 @@ abstract class AppleFactoryModule {
|
||||
@Binds @IntoSet abstract fun beatsX(factory: BeatsX.Factory): ApplePodsFactory<out ApplePods>
|
||||
@Binds @IntoSet abstract fun powerBeats3(factory: PowerBeats3.Factory): ApplePodsFactory<out ApplePods>
|
||||
@Binds @IntoSet abstract fun powerBeatsPro(factory: PowerBeatsPro.Factory): ApplePodsFactory<out ApplePods>
|
||||
|
||||
@Binds @IntoSet abstract fun fakesTwsi999999(factory: Twsi99999.Factory): ApplePodsFactory<out ApplePods>
|
||||
}
|
||||
@@ -5,7 +5,9 @@ import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE
|
||||
import eu.darken.capod.common.debug.logging.log
|
||||
import eu.darken.capod.common.lowerNibble
|
||||
import eu.darken.capod.common.upperNibble
|
||||
import eu.darken.capod.pods.core.HasCase
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.apple.airpods.AirPodsPro
|
||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
@@ -21,9 +23,9 @@ abstract class ApplePodsFactory<PodType : ApplePods>(private val tag: String) {
|
||||
val deviceColor: UByte,
|
||||
)
|
||||
|
||||
fun ProximityPairing.Message.getApplePodsMarkings(): Markings = Markings(
|
||||
private fun ProximityPairing.Message.getApplePodsMarkings(): Markings = Markings(
|
||||
vendor = ProximityPairing.CONTINUITY_PROTOCOL_MESSAGE_TYPE_PROXIMITY_PAIRING,
|
||||
length = ProximityPairing.PROXIMITY_PAIRING_MESSAGE_LENGTH.toUByte(),
|
||||
length = ProximityPairing.PAIRING_MESSAGE_LENGTH.toUByte(),
|
||||
device = (((data[1].toInt() and 255) shl 8) or (data[2].toInt() and 255)).toUShort(),
|
||||
// Make comparison order independent
|
||||
podBatteryData = setOf(data[4].upperNibble, data[4].lowerNibble),
|
||||
@@ -70,6 +72,22 @@ abstract class ApplePodsFactory<PodType : ApplePods>(private val tag: String) {
|
||||
|
||||
internal val knownDevices = mutableMapOf<PodDevice.Id, KnownDevice>()
|
||||
|
||||
|
||||
fun KnownDevice.getLatestCaseBattery(): Float? = history
|
||||
.filterIsInstance<HasCase>()
|
||||
.mapNotNull { it.batteryCasePercent }
|
||||
.lastOrNull()
|
||||
|
||||
fun KnownDevice.getLatestCaseLidState(basic: DualAirPods): DualAirPods.LidState? {
|
||||
val definitive = setOf(DualAirPods.LidState.OPEN, DualAirPods.LidState.CLOSED)
|
||||
if (definitive.contains(basic.caseLidState)) return null
|
||||
|
||||
return history
|
||||
.filterIsInstance<AirPodsPro>()
|
||||
.lastOrNull { it.caseLidState != DualAirPods.LidState.NOT_IN_CASE }
|
||||
?.caseLidState
|
||||
}
|
||||
|
||||
internal open fun searchHistory(current: PodType): KnownDevice? {
|
||||
val scanResult = current.scanResult
|
||||
val message = current.proximityMessage
|
||||
@@ -138,10 +156,10 @@ abstract class ApplePodsFactory<PodType : ApplePods>(private val tag: String) {
|
||||
dirty = data[1]
|
||||
)
|
||||
|
||||
abstract fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean
|
||||
abstract fun isResponsible(message: ProximityPairing.Message): Boolean
|
||||
|
||||
abstract fun create(
|
||||
scanResult: BleScanResult,
|
||||
proximityMessage: ProximityPairing.Message,
|
||||
message: ProximityPairing.Message,
|
||||
): ApplePods
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
package eu.darken.capod.pods.core.apple
|
||||
|
||||
import eu.darken.capod.common.debug.logging.log
|
||||
import eu.darken.capod.common.lowerNibble
|
||||
import eu.darken.capod.pods.core.HasSinglePod
|
||||
|
||||
interface BasicSingleApplePods : ApplePods, HasSinglePod {
|
||||
|
||||
override val batteryHeadsetPercent: Float?
|
||||
get() = when (val value = rawPodsBattery.lowerNibble.toInt()) {
|
||||
15 -> null
|
||||
else -> if (value > 10) {
|
||||
log { "Left pod: Above 100% battery: $value" }
|
||||
1.0f
|
||||
} else {
|
||||
(value / 10f)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
package eu.darken.capod.pods.core.apple
|
||||
|
||||
abstract class BasicSingleApplePodsFactory(private val tag: String) : ApplePodsFactory<BasicSingleApplePods>(tag)
|
||||
+13
-37
@@ -7,12 +7,11 @@ import eu.darken.capod.common.debug.logging.log
|
||||
import eu.darken.capod.common.isBitSet
|
||||
import eu.darken.capod.common.lowerNibble
|
||||
import eu.darken.capod.common.upperNibble
|
||||
import eu.darken.capod.pods.core.HasCase
|
||||
import eu.darken.capod.pods.core.HasDualEarDetection
|
||||
import eu.darken.capod.pods.core.HasDualPods
|
||||
import eu.darken.capod.pods.core.HasDualPods.Pod
|
||||
import eu.darken.capod.pods.core.*
|
||||
import eu.darken.capod.pods.core.DualPodDevice.Pod
|
||||
|
||||
interface DualApplePods : ApplePods, HasDualPods, HasDualEarDetection, HasCase {
|
||||
interface DualAirPods : ApplePods, HasChargeDetectionDual, DualPodDevice, HasEarDetectionDual, HasCase,
|
||||
HasStateDetection, HasDualMicrophone, HasAppleColor {
|
||||
|
||||
val primaryPod: Pod
|
||||
get() = when (rawStatus.isBitSet(5)) {
|
||||
@@ -86,23 +85,23 @@ interface DualApplePods : ApplePods, HasDualPods, HasDualEarDetection, HasCase {
|
||||
* The data flip bit is set if the left pod is primary.
|
||||
* For the pod that is in the case, this is flipped again though.
|
||||
*/
|
||||
val isLeftPodMicrophone: Boolean
|
||||
override val isLeftPodMicrophone: Boolean
|
||||
get() = rawStatus.isBitSet(5) xor isThisPodInThecase
|
||||
|
||||
/**
|
||||
* The data flip bit is UNset if the right pod is primary.
|
||||
* For the pod that is in the case, this is flipped again though.
|
||||
*/
|
||||
val isRightPodMicrophone: Boolean
|
||||
override val isRightPodMicrophone: Boolean
|
||||
get() = !rawStatus.isBitSet(5) xor isThisPodInThecase
|
||||
|
||||
val isLeftPodCharging: Boolean
|
||||
override val isLeftPodCharging: Boolean
|
||||
get() = when (areValuesFlipped) {
|
||||
false -> rawFlags.isBitSet(0)
|
||||
true -> rawFlags.isBitSet(1)
|
||||
}
|
||||
|
||||
val isRightPodCharging: Boolean
|
||||
override val isRightPodCharging: Boolean
|
||||
get() = when (areValuesFlipped) {
|
||||
false -> rawFlags.isBitSet(1)
|
||||
true -> rawFlags.isBitSet(0)
|
||||
@@ -143,42 +142,19 @@ interface DualApplePods : ApplePods, HasDualPods, HasDualEarDetection, HasCase {
|
||||
UNKNOWN(0xFF..0xFF);
|
||||
}
|
||||
|
||||
val deviceColor: DeviceColor
|
||||
get() = DeviceColor.values().firstOrNull { it.raw == rawDeviceColor } ?: DeviceColor.UNKNOWN
|
||||
|
||||
enum class DeviceColor(val raw: UByte?) {
|
||||
|
||||
WHITE(0x00),
|
||||
BLACK(0x01),
|
||||
RED(0x02),
|
||||
BLUE(0x03),
|
||||
PINK(0x04),
|
||||
GRAY(0x05),
|
||||
SILVER(0x06),
|
||||
GOLD(0x07),
|
||||
ROSE_GOLD(0x08),
|
||||
SPACE_GRAY(0x09),
|
||||
DARK_BLUE(0x0a),
|
||||
LIGHT_BLUE(0x0b),
|
||||
YELLOW(0x0c),
|
||||
UNKNOWN(null);
|
||||
|
||||
constructor(raw: Int) : this(raw.toUByte())
|
||||
}
|
||||
|
||||
val connectionState: ConnectionState
|
||||
override val state: ConnectionState
|
||||
get() = ConnectionState.values().firstOrNull { rawSuffix == it.raw } ?: ConnectionState.UNKNOWN
|
||||
|
||||
fun getConnectionStateLabel(context: Context): String = context.getString(connectionState.labelRes)
|
||||
|
||||
enum class ConnectionState(val raw: UByte?, @StringRes val labelRes: Int) {
|
||||
enum class ConnectionState(val raw: UByte?, @StringRes val labelRes: Int) : HasStateDetection.State {
|
||||
DISCONNECTED(0x00, R.string.pods_connection_state_disconnected_label),
|
||||
IDLE(0x04, R.string.pods_connection_state_idle_label),
|
||||
MUSIC(0x05, R.string.pods_connection_state_music_label),
|
||||
CALL(0x06, R.string.pods_connection_state_call_label),
|
||||
RINGING(0x07, R.string.pods_connection_state_ringing_label),
|
||||
HANGING_UP(0x09, R.string.pods_connection_state_hanging_up_label),
|
||||
UNKNOWN(null, R.string.general_value_unknown_label);
|
||||
UNKNOWN(null, R.string.pods_connection_state_unknown_label);
|
||||
|
||||
override fun getLabel(context: Context): String = context.getString(labelRes)
|
||||
|
||||
constructor(raw: Int, @StringRes labelRes: Int) : this(raw.toUByte(), labelRes)
|
||||
}
|
||||
@@ -3,18 +3,17 @@ package eu.darken.capod.pods.core.apple
|
||||
import eu.darken.capod.common.debug.logging.Logging.Priority.DEBUG
|
||||
import eu.darken.capod.common.debug.logging.log
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.apple.airpods.AirPodsPro
|
||||
|
||||
abstract class DualApplePodsFactory(private val tag: String) : ApplePodsFactory<DualApplePods>(tag) {
|
||||
abstract class DualApplePodsFactory(private val tag: String) : ApplePodsFactory<DualAirPods>(tag) {
|
||||
|
||||
fun DualApplePods.getCaseMatchMarkings() = SplitPodsMarkings(
|
||||
fun DualAirPods.getCaseMatchMarkings() = SplitPodsMarkings(
|
||||
leftPodBattery = batteryLeftPodPercent,
|
||||
rightPodBattery = batteryRightPodPercent,
|
||||
microPhoneLeft = isLeftPodMicrophone,
|
||||
microPhoneRight = isRightPodMicrophone,
|
||||
chargingLeft = isLeftPodCharging,
|
||||
chargingRight = isRightPodCharging,
|
||||
color = deviceColor,
|
||||
color = rawDeviceColor,
|
||||
model = model
|
||||
)
|
||||
|
||||
@@ -28,36 +27,21 @@ abstract class DualApplePodsFactory(private val tag: String) : ApplePodsFactory<
|
||||
val microPhoneRight: Boolean,
|
||||
val chargingLeft: Boolean,
|
||||
val chargingRight: Boolean,
|
||||
val color: DualApplePods.DeviceColor,
|
||||
val color: UByte,
|
||||
val model: PodDevice.Model,
|
||||
)
|
||||
|
||||
private fun Collection<KnownDevice>.findSplitPodsMatch(device: DualApplePods): Collection<KnownDevice> {
|
||||
private fun Collection<KnownDevice>.findSplitPodsMatch(device: DualAirPods): Collection<KnownDevice> {
|
||||
val target = device.getCaseMatchMarkings()
|
||||
|
||||
return filter { known ->
|
||||
known.history
|
||||
.filterIsInstance<DualApplePods>()
|
||||
.filterIsInstance<DualAirPods>()
|
||||
.any { it.getCaseMatchMarkings() == target }
|
||||
}
|
||||
}
|
||||
|
||||
fun KnownDevice.getLatestCaseBattery(): Float? = history
|
||||
.filterIsInstance<DualApplePods>()
|
||||
.mapNotNull { it.batteryCasePercent }
|
||||
.lastOrNull()
|
||||
|
||||
fun KnownDevice.getLatestCaseLidState(basic: DualApplePods): DualApplePods.LidState? {
|
||||
val definitive = setOf(DualApplePods.LidState.OPEN, DualApplePods.LidState.CLOSED)
|
||||
if (definitive.contains(basic.caseLidState)) return null
|
||||
|
||||
return history
|
||||
.filterIsInstance<AirPodsPro>()
|
||||
.lastOrNull { it.caseLidState != DualApplePods.LidState.NOT_IN_CASE }
|
||||
?.caseLidState
|
||||
}
|
||||
|
||||
override fun searchHistory(current: DualApplePods): KnownDevice? {
|
||||
override fun searchHistory(current: DualAirPods): KnownDevice? {
|
||||
val basicResult = super.searchHistory(current)
|
||||
|
||||
val caseIgnored = knownDevices.values.findSplitPodsMatch(current)
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package eu.darken.capod.pods.core.apple
|
||||
|
||||
import android.content.Context
|
||||
import eu.darken.capod.pods.core.HasPodStyle
|
||||
|
||||
interface HasAppleColor : ApplePods, HasPodStyle {
|
||||
|
||||
override val podStyle: HasPodStyle.PodStyle
|
||||
get() = DeviceColor.values()
|
||||
.firstOrNull { it.raw == rawDeviceColor }
|
||||
?: DeviceColor.UNKNOWN
|
||||
|
||||
enum class DeviceColor(val raw: UByte?) : HasPodStyle.PodStyle {
|
||||
WHITE(0x00),
|
||||
BLACK(0x01),
|
||||
RED(0x02),
|
||||
BLUE(0x03),
|
||||
PINK(0x04),
|
||||
GRAY(0x05),
|
||||
SILVER(0x06),
|
||||
GOLD(0x07),
|
||||
ROSE_GOLD(0x08),
|
||||
SPACE_GRAY(0x09),
|
||||
DARK_BLUE(0x0a),
|
||||
LIGHT_BLUE(0x0b),
|
||||
YELLOW(0x0c),
|
||||
UNKNOWN(null);
|
||||
|
||||
override fun getLabel(context: Context): String = this.name
|
||||
|
||||
override fun getColor(context: Context): Int = android.R.color.white
|
||||
|
||||
override val identifier: String
|
||||
get() = name
|
||||
|
||||
constructor(raw: Int) : this(raw.toUByte())
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,22 @@
|
||||
package eu.darken.capod.pods.core.apple
|
||||
|
||||
import eu.darken.capod.common.isBitSet
|
||||
import eu.darken.capod.pods.core.HasEarDetection
|
||||
import eu.darken.capod.pods.core.HasSinglePod
|
||||
import eu.darken.capod.common.debug.logging.log
|
||||
import eu.darken.capod.common.lowerNibble
|
||||
import eu.darken.capod.pods.core.SinglePodDevice
|
||||
|
||||
interface SingleApplePods : BasicSingleApplePods, HasEarDetection, HasSinglePod {
|
||||
/**
|
||||
* Devices that only present a single charge level, e.g. most Beats devices
|
||||
*/
|
||||
interface SingleApplePods : ApplePods, SinglePodDevice, HasAppleColor {
|
||||
|
||||
val isHeadphonesBeingWorn: Boolean
|
||||
get() = rawStatus.isBitSet(1)
|
||||
|
||||
val isHeadsetBeingCharged: Boolean
|
||||
get() = rawFlags.isBitSet(0)
|
||||
|
||||
override val isBeingWorn: Boolean
|
||||
get() = isHeadphonesBeingWorn
|
||||
override val batteryHeadsetPercent: Float?
|
||||
get() = when (val value = rawPodsBattery.lowerNibble.toInt()) {
|
||||
15 -> null
|
||||
else -> if (value > 10) {
|
||||
log { "Headset above 100% battery: $value" }
|
||||
1.0f
|
||||
} else {
|
||||
(value / 10f)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import eu.darken.capod.common.bluetooth.BleScanResult
|
||||
import eu.darken.capod.common.debug.logging.logTag
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.apple.ApplePods
|
||||
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||
import eu.darken.capod.pods.core.apple.DualAirPods
|
||||
import eu.darken.capod.pods.core.apple.DualApplePodsFactory
|
||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||
import java.time.Instant
|
||||
@@ -20,15 +20,15 @@ data class AirPodsGen1 constructor(
|
||||
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
||||
private val rssiAverage: Int? = null,
|
||||
private val cachedBatteryPercentage: Float? = null,
|
||||
private val cachedCaseState: DualApplePods.LidState? = null
|
||||
) : DualApplePods {
|
||||
private val cachedCaseState: DualAirPods.LidState? = null
|
||||
) : DualAirPods {
|
||||
|
||||
override val model: PodDevice.Model = PodDevice.Model.AIRPODS_GEN1
|
||||
|
||||
override val batteryCasePercent: Float?
|
||||
get() = super.batteryCasePercent ?: cachedBatteryPercentage
|
||||
|
||||
override val caseLidState: DualApplePods.LidState
|
||||
override val caseLidState: DualAirPods.LidState
|
||||
get() = cachedCaseState ?: super.caseLidState
|
||||
|
||||
override val rssi: Int
|
||||
@@ -36,11 +36,13 @@ data class AirPodsGen1 constructor(
|
||||
|
||||
class Factory @Inject constructor() : DualApplePodsFactory(TAG) {
|
||||
|
||||
override fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean =
|
||||
proximityMessage.getModelInfo().full == DEVICE_CODE
|
||||
override fun isResponsible(message: ProximityPairing.Message): Boolean = message.run {
|
||||
getModelInfo().full == DEVICE_CODE && length == ProximityPairing.PAIRING_MESSAGE_LENGTH
|
||||
}
|
||||
|
||||
override fun create(scanResult: BleScanResult, proximityMessage: ProximityPairing.Message): ApplePods {
|
||||
var basic = AirPodsGen1(scanResult = scanResult, proximityMessage = proximityMessage)
|
||||
|
||||
override fun create(scanResult: BleScanResult, message: ProximityPairing.Message): ApplePods {
|
||||
var basic = AirPodsGen1(scanResult = scanResult, proximityMessage = message)
|
||||
val result = searchHistory(basic)
|
||||
|
||||
if (result != null) basic = basic.copy(identifier = result.id)
|
||||
|
||||
@@ -4,7 +4,7 @@ import eu.darken.capod.common.bluetooth.BleScanResult
|
||||
import eu.darken.capod.common.debug.logging.logTag
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.apple.ApplePods
|
||||
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||
import eu.darken.capod.pods.core.apple.DualAirPods
|
||||
import eu.darken.capod.pods.core.apple.DualApplePodsFactory
|
||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||
import java.time.Instant
|
||||
@@ -20,15 +20,15 @@ data class AirPodsGen2 constructor(
|
||||
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
||||
private val rssiAverage: Int? = null,
|
||||
private val cachedBatteryPercentage: Float? = null,
|
||||
private val cachedCaseState: DualApplePods.LidState? = null
|
||||
) : DualApplePods {
|
||||
private val cachedCaseState: DualAirPods.LidState? = null
|
||||
) : DualAirPods {
|
||||
|
||||
override val model: PodDevice.Model = PodDevice.Model.AIRPODS_GEN2
|
||||
|
||||
override val batteryCasePercent: Float?
|
||||
get() = super.batteryCasePercent ?: cachedBatteryPercentage
|
||||
|
||||
override val caseLidState: DualApplePods.LidState
|
||||
override val caseLidState: DualAirPods.LidState
|
||||
get() = cachedCaseState ?: super.caseLidState
|
||||
|
||||
override val rssi: Int
|
||||
@@ -36,11 +36,12 @@ data class AirPodsGen2 constructor(
|
||||
|
||||
class Factory @Inject constructor() : DualApplePodsFactory(TAG) {
|
||||
|
||||
override fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean =
|
||||
proximityMessage.getModelInfo().full == DEVICE_CODE
|
||||
override fun isResponsible(message: ProximityPairing.Message): Boolean = message.run {
|
||||
getModelInfo().full == DEVICE_CODE && length == ProximityPairing.PAIRING_MESSAGE_LENGTH
|
||||
}
|
||||
|
||||
override fun create(scanResult: BleScanResult, proximityMessage: ProximityPairing.Message): ApplePods {
|
||||
var basic = AirPodsGen2(scanResult = scanResult, proximityMessage = proximityMessage)
|
||||
override fun create(scanResult: BleScanResult, message: ProximityPairing.Message): ApplePods {
|
||||
var basic = AirPodsGen2(scanResult = scanResult, proximityMessage = message)
|
||||
val result = searchHistory(basic)
|
||||
|
||||
if (result != null) basic = basic.copy(identifier = result.id)
|
||||
|
||||
@@ -4,7 +4,7 @@ import eu.darken.capod.common.bluetooth.BleScanResult
|
||||
import eu.darken.capod.common.debug.logging.logTag
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.apple.ApplePods
|
||||
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||
import eu.darken.capod.pods.core.apple.DualAirPods
|
||||
import eu.darken.capod.pods.core.apple.DualApplePodsFactory
|
||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||
import java.time.Instant
|
||||
@@ -20,14 +20,14 @@ data class AirPodsGen3 constructor(
|
||||
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
||||
private val rssiAverage: Int? = null,
|
||||
private val cachedBatteryPercentage: Float? = null,
|
||||
private val cachedCaseState: DualApplePods.LidState? = null
|
||||
) : DualApplePods {
|
||||
private val cachedCaseState: DualAirPods.LidState? = null
|
||||
) : DualAirPods {
|
||||
override val model: PodDevice.Model = PodDevice.Model.AIRPODS_GEN3
|
||||
|
||||
override val batteryCasePercent: Float?
|
||||
get() = super.batteryCasePercent ?: cachedBatteryPercentage
|
||||
|
||||
override val caseLidState: DualApplePods.LidState
|
||||
override val caseLidState: DualAirPods.LidState
|
||||
get() = cachedCaseState ?: super.caseLidState
|
||||
|
||||
override val rssi: Int
|
||||
@@ -35,11 +35,12 @@ data class AirPodsGen3 constructor(
|
||||
|
||||
class Factory @Inject constructor() : DualApplePodsFactory(TAG) {
|
||||
|
||||
override fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean =
|
||||
proximityMessage.getModelInfo().full == DEVICE_CODE
|
||||
override fun isResponsible(message: ProximityPairing.Message): Boolean = message.run {
|
||||
getModelInfo().full == DEVICE_CODE && length == ProximityPairing.PAIRING_MESSAGE_LENGTH
|
||||
}
|
||||
|
||||
override fun create(scanResult: BleScanResult, proximityMessage: ProximityPairing.Message): ApplePods {
|
||||
var basic = AirPodsGen3(scanResult = scanResult, proximityMessage = proximityMessage)
|
||||
override fun create(scanResult: BleScanResult, message: ProximityPairing.Message): ApplePods {
|
||||
var basic = AirPodsGen3(scanResult = scanResult, proximityMessage = message)
|
||||
val result = searchHistory(basic)
|
||||
|
||||
if (result != null) basic = basic.copy(identifier = result.id)
|
||||
|
||||
@@ -2,6 +2,8 @@ package eu.darken.capod.pods.core.apple.airpods
|
||||
|
||||
import eu.darken.capod.common.bluetooth.BleScanResult
|
||||
import eu.darken.capod.common.debug.logging.logTag
|
||||
import eu.darken.capod.common.isBitSet
|
||||
import eu.darken.capod.pods.core.HasEarDetection
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.apple.ApplePods
|
||||
import eu.darken.capod.pods.core.apple.SingleApplePods
|
||||
@@ -19,20 +21,30 @@ data class AirPodsMax(
|
||||
override val proximityMessage: ProximityPairing.Message,
|
||||
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
||||
private val rssiAverage: Int? = null,
|
||||
) : SingleApplePods {
|
||||
) : SingleApplePods, HasEarDetection {
|
||||
|
||||
override val model: PodDevice.Model = PodDevice.Model.AIRPODS_MAX
|
||||
|
||||
override val rssi: Int
|
||||
get() = rssiAverage ?: super.rssi
|
||||
|
||||
val isHeadphonesBeingWorn: Boolean
|
||||
get() = rawStatus.isBitSet(1)
|
||||
|
||||
val isHeadsetBeingCharged: Boolean
|
||||
get() = rawFlags.isBitSet(0)
|
||||
|
||||
override val isBeingWorn: Boolean
|
||||
get() = isHeadphonesBeingWorn
|
||||
|
||||
class Factory @Inject constructor() : SingleApplePodsFactory(TAG) {
|
||||
|
||||
override fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean =
|
||||
proximityMessage.getModelInfo().dirty == DEVICE_CODE_DIRTY
|
||||
override fun isResponsible(message: ProximityPairing.Message): Boolean = message.run {
|
||||
getModelInfo().dirty == DEVICE_CODE_DIRTY && length == ProximityPairing.PAIRING_MESSAGE_LENGTH
|
||||
}
|
||||
|
||||
override fun create(scanResult: BleScanResult, proximityMessage: ProximityPairing.Message): ApplePods {
|
||||
var basic = AirPodsMax(scanResult = scanResult, proximityMessage = proximityMessage)
|
||||
override fun create(scanResult: BleScanResult, message: ProximityPairing.Message): ApplePods {
|
||||
var basic = AirPodsMax(scanResult = scanResult, proximityMessage = message)
|
||||
val result = searchHistory(basic)
|
||||
|
||||
if (result != null) basic = basic.copy(identifier = result.id)
|
||||
@@ -48,7 +60,6 @@ data class AirPodsMax(
|
||||
rssiAverage = result.averageRssi(basic.rssi),
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -4,8 +4,8 @@ import eu.darken.capod.common.bluetooth.BleScanResult
|
||||
import eu.darken.capod.common.debug.logging.logTag
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.apple.ApplePods
|
||||
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||
import eu.darken.capod.pods.core.apple.DualApplePods.LidState
|
||||
import eu.darken.capod.pods.core.apple.DualAirPods
|
||||
import eu.darken.capod.pods.core.apple.DualAirPods.LidState
|
||||
import eu.darken.capod.pods.core.apple.DualApplePodsFactory
|
||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||
import java.time.Instant
|
||||
@@ -22,7 +22,7 @@ data class AirPodsPro(
|
||||
private val rssiAverage: Int? = null,
|
||||
private val cachedBatteryPercentage: Float? = null,
|
||||
private val cachedCaseState: LidState? = null
|
||||
) : DualApplePods {
|
||||
) : DualAirPods {
|
||||
|
||||
override val model: PodDevice.Model = PodDevice.Model.AIRPODS_PRO
|
||||
|
||||
@@ -37,11 +37,12 @@ data class AirPodsPro(
|
||||
|
||||
class Factory @Inject constructor() : DualApplePodsFactory(TAG) {
|
||||
|
||||
override fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean =
|
||||
proximityMessage.getModelInfo().full == DEVICE_CODE
|
||||
override fun isResponsible(message: ProximityPairing.Message): Boolean = message.run {
|
||||
getModelInfo().full == DEVICE_CODE && length == ProximityPairing.PAIRING_MESSAGE_LENGTH
|
||||
}
|
||||
|
||||
override fun create(scanResult: BleScanResult, proximityMessage: ProximityPairing.Message): ApplePods {
|
||||
var basic = AirPodsPro(scanResult = scanResult, proximityMessage = proximityMessage)
|
||||
override fun create(scanResult: BleScanResult, message: ProximityPairing.Message): ApplePods {
|
||||
var basic = AirPodsPro(scanResult = scanResult, proximityMessage = message)
|
||||
val result = searchHistory(basic)
|
||||
|
||||
if (result != null) basic = basic.copy(identifier = result.id)
|
||||
|
||||
@@ -4,8 +4,8 @@ import eu.darken.capod.common.bluetooth.BleScanResult
|
||||
import eu.darken.capod.common.debug.logging.logTag
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.apple.ApplePods
|
||||
import eu.darken.capod.pods.core.apple.BasicSingleApplePods
|
||||
import eu.darken.capod.pods.core.apple.BasicSingleApplePodsFactory
|
||||
import eu.darken.capod.pods.core.apple.SingleApplePods
|
||||
import eu.darken.capod.pods.core.apple.SingleApplePodsFactory
|
||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||
import java.time.Instant
|
||||
import javax.inject.Inject
|
||||
@@ -19,20 +19,21 @@ data class BeatsFlex(
|
||||
override val proximityMessage: ProximityPairing.Message,
|
||||
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
||||
private val rssiAverage: Int? = null,
|
||||
) : BasicSingleApplePods {
|
||||
) : SingleApplePods {
|
||||
|
||||
override val model: PodDevice.Model = PodDevice.Model.BEATS_FLEX
|
||||
|
||||
override val rssi: Int
|
||||
get() = rssiAverage ?: super.rssi
|
||||
|
||||
class Factory @Inject constructor() : BasicSingleApplePodsFactory(TAG) {
|
||||
class Factory @Inject constructor() : SingleApplePodsFactory(TAG) {
|
||||
|
||||
override fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean =
|
||||
proximityMessage.getModelInfo().full == DEVICE_CODE
|
||||
override fun isResponsible(message: ProximityPairing.Message): Boolean = message.run {
|
||||
getModelInfo().full == DEVICE_CODE && length == ProximityPairing.PAIRING_MESSAGE_LENGTH
|
||||
}
|
||||
|
||||
override fun create(scanResult: BleScanResult, proximityMessage: ProximityPairing.Message): ApplePods {
|
||||
var basic = BeatsFlex(scanResult = scanResult, proximityMessage = proximityMessage)
|
||||
override fun create(scanResult: BleScanResult, message: ProximityPairing.Message): ApplePods {
|
||||
var basic = BeatsFlex(scanResult = scanResult, proximityMessage = message)
|
||||
val result = searchHistory(basic)
|
||||
|
||||
if (result != null) basic = basic.copy(identifier = result.id)
|
||||
|
||||
@@ -4,8 +4,8 @@ import eu.darken.capod.common.bluetooth.BleScanResult
|
||||
import eu.darken.capod.common.debug.logging.logTag
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.apple.ApplePods
|
||||
import eu.darken.capod.pods.core.apple.BasicSingleApplePods
|
||||
import eu.darken.capod.pods.core.apple.BasicSingleApplePodsFactory
|
||||
import eu.darken.capod.pods.core.apple.SingleApplePods
|
||||
import eu.darken.capod.pods.core.apple.SingleApplePodsFactory
|
||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||
import java.time.Instant
|
||||
import javax.inject.Inject
|
||||
@@ -19,19 +19,20 @@ data class BeatsSolo3(
|
||||
override val proximityMessage: ProximityPairing.Message,
|
||||
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
||||
private val rssiAverage: Int? = null,
|
||||
) : BasicSingleApplePods {
|
||||
) : SingleApplePods {
|
||||
|
||||
override val model: PodDevice.Model = PodDevice.Model.BEATS_SOLO_3
|
||||
override val rssi: Int
|
||||
get() = rssiAverage ?: super.rssi
|
||||
|
||||
class Factory @Inject constructor() : BasicSingleApplePodsFactory(TAG) {
|
||||
class Factory @Inject constructor() : SingleApplePodsFactory(TAG) {
|
||||
|
||||
override fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean =
|
||||
proximityMessage.getModelInfo().full == DEVICE_CODE
|
||||
override fun isResponsible(message: ProximityPairing.Message): Boolean = message.run {
|
||||
getModelInfo().full == DEVICE_CODE && length == ProximityPairing.PAIRING_MESSAGE_LENGTH
|
||||
}
|
||||
|
||||
override fun create(scanResult: BleScanResult, proximityMessage: ProximityPairing.Message): ApplePods {
|
||||
var basic = BeatsSolo3(scanResult = scanResult, proximityMessage = proximityMessage)
|
||||
override fun create(scanResult: BleScanResult, message: ProximityPairing.Message): ApplePods {
|
||||
var basic = BeatsSolo3(scanResult = scanResult, proximityMessage = message)
|
||||
val result = searchHistory(basic)
|
||||
|
||||
if (result != null) basic = basic.copy(identifier = result.id)
|
||||
|
||||
@@ -4,8 +4,8 @@ import eu.darken.capod.common.bluetooth.BleScanResult
|
||||
import eu.darken.capod.common.debug.logging.logTag
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.apple.ApplePods
|
||||
import eu.darken.capod.pods.core.apple.BasicSingleApplePods
|
||||
import eu.darken.capod.pods.core.apple.BasicSingleApplePodsFactory
|
||||
import eu.darken.capod.pods.core.apple.SingleApplePods
|
||||
import eu.darken.capod.pods.core.apple.SingleApplePodsFactory
|
||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||
import java.time.Instant
|
||||
import javax.inject.Inject
|
||||
@@ -19,17 +19,18 @@ data class BeatsStudio3(
|
||||
override val proximityMessage: ProximityPairing.Message,
|
||||
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
||||
private val rssiAverage: Int? = null,
|
||||
) : BasicSingleApplePods {
|
||||
) : SingleApplePods {
|
||||
|
||||
override val model: PodDevice.Model = PodDevice.Model.BEATS_STUDIO_3
|
||||
|
||||
class Factory @Inject constructor() : BasicSingleApplePodsFactory(TAG) {
|
||||
class Factory @Inject constructor() : SingleApplePodsFactory(TAG) {
|
||||
|
||||
override fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean =
|
||||
proximityMessage.getModelInfo().dirty == DEVICE_CODE_DIRTY
|
||||
override fun isResponsible(message: ProximityPairing.Message): Boolean = message.run {
|
||||
getModelInfo().dirty == DEVICE_CODE_DIRTY && length == ProximityPairing.PAIRING_MESSAGE_LENGTH
|
||||
}
|
||||
|
||||
override fun create(scanResult: BleScanResult, proximityMessage: ProximityPairing.Message): ApplePods {
|
||||
var basic = BeatsStudio3(scanResult = scanResult, proximityMessage = proximityMessage)
|
||||
override fun create(scanResult: BleScanResult, message: ProximityPairing.Message): ApplePods {
|
||||
var basic = BeatsStudio3(scanResult = scanResult, proximityMessage = message)
|
||||
val result = searchHistory(basic)
|
||||
|
||||
if (result != null) basic = basic.copy(identifier = result.id)
|
||||
@@ -45,7 +46,6 @@ data class BeatsStudio3(
|
||||
rssiAverage = result.averageRssi(basic.rssi),
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -4,8 +4,8 @@ import eu.darken.capod.common.bluetooth.BleScanResult
|
||||
import eu.darken.capod.common.debug.logging.logTag
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.apple.ApplePods
|
||||
import eu.darken.capod.pods.core.apple.BasicSingleApplePods
|
||||
import eu.darken.capod.pods.core.apple.BasicSingleApplePodsFactory
|
||||
import eu.darken.capod.pods.core.apple.SingleApplePods
|
||||
import eu.darken.capod.pods.core.apple.SingleApplePodsFactory
|
||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||
import java.time.Instant
|
||||
import javax.inject.Inject
|
||||
@@ -19,20 +19,21 @@ data class BeatsX(
|
||||
override val proximityMessage: ProximityPairing.Message,
|
||||
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
||||
private val rssiAverage: Int? = null,
|
||||
) : BasicSingleApplePods {
|
||||
) : SingleApplePods {
|
||||
|
||||
override val model: PodDevice.Model = PodDevice.Model.BEATS_X
|
||||
|
||||
override val rssi: Int
|
||||
get() = rssiAverage ?: super.rssi
|
||||
|
||||
class Factory @Inject constructor() : BasicSingleApplePodsFactory(TAG) {
|
||||
class Factory @Inject constructor() : SingleApplePodsFactory(TAG) {
|
||||
|
||||
override fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean =
|
||||
proximityMessage.getModelInfo().full == DEVICE_CODE
|
||||
override fun isResponsible(message: ProximityPairing.Message): Boolean = message.run {
|
||||
getModelInfo().full == DEVICE_CODE && length == ProximityPairing.PAIRING_MESSAGE_LENGTH
|
||||
}
|
||||
|
||||
override fun create(scanResult: BleScanResult, proximityMessage: ProximityPairing.Message): ApplePods {
|
||||
var basic = BeatsX(scanResult = scanResult, proximityMessage = proximityMessage)
|
||||
override fun create(scanResult: BleScanResult, message: ProximityPairing.Message): ApplePods {
|
||||
var basic = BeatsX(scanResult = scanResult, proximityMessage = message)
|
||||
val result = searchHistory(basic)
|
||||
|
||||
if (result != null) basic = basic.copy(identifier = result.id)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user