mirror of
https://github.com/d4rken-org/capod.git
synced 2026-09-14 18:26:11 -04:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7e913e7847 | ||
|
|
ec7871e4e9 | ||
|
|
cc5a45a14f | ||
|
|
5fff3457ba | ||
|
|
6f5d66c1d9 | ||
|
|
d94795bf4f | ||
|
|
009b71c42a | ||
|
|
a10716d74e | ||
|
|
b44bf5f401 | ||
|
|
e0af1838e2 | ||
|
|
7d5502a34f | ||
|
|
78ccddf87c | ||
|
|
df1bc48e84 | ||
|
|
3b48d61571 | ||
|
|
92d5e2a40d | ||
|
|
234857c414 | ||
|
|
b1bbe785ea | ||
|
|
487fb49bff | ||
|
|
b14b7fa926 | ||
|
|
4cc1ab143d | ||
|
|
c23de67cd2 | ||
|
|
e9bb4868c8 | ||
|
|
ae6fd0eabd | ||
|
|
f4f73906bc | ||
|
|
c437decbbf | ||
|
|
1d36677ead | ||
|
|
542b69ca92 | ||
|
|
8547c9c396 | ||
|
|
4e258e65c1 | ||
|
|
9f49b470fe | ||
|
|
4a71cfe65a | ||
|
|
d06959f964 | ||
|
|
8c339ca2eb | ||
|
|
3aab53566d | ||
|
|
749cfed137 | ||
|
|
ce25ad6711 | ||
|
|
568aeec0d5 | ||
|
|
6b02fd66e6 |
@@ -1,4 +1,4 @@
|
|||||||
name: Android CI
|
name: Code tests & eval
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
@@ -7,13 +7,13 @@ on:
|
|||||||
branches: [ main ]
|
branches: [ main ]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build-and-test:
|
||||||
name: Build and test
|
name: Build and test
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- name: set up JDK 11
|
- name: Set up JDK 11
|
||||||
uses: actions/setup-java@v2
|
uses: actions/setup-java@v2
|
||||||
with:
|
with:
|
||||||
java-version: '11'
|
java-version: '11'
|
||||||
@@ -22,7 +22,13 @@ jobs:
|
|||||||
|
|
||||||
- name: Grant execute permission for gradlew
|
- name: Grant execute permission for gradlew
|
||||||
run: chmod +x gradlew
|
run: chmod +x gradlew
|
||||||
- name: Build with Gradle
|
|
||||||
run: ./gradlew assembleDebug
|
- name: Build FOSS variant
|
||||||
- name: Run tests
|
run: ./gradlew assembleFossDebug
|
||||||
run: ./gradlew testGplayDebugUnitTest testFossDebugUnitTest
|
- name: Test FOSS variant
|
||||||
|
run: ./gradlew testFossDebugUnitTest
|
||||||
|
|
||||||
|
- name: Build Google Play variant
|
||||||
|
run: ./gradlew assembleGplayDebug
|
||||||
|
- name: Test Google Play variant
|
||||||
|
run: ./gradlew testGplayDebugUnitTest
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
name: Tagged releases
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release-github:
|
||||||
|
name: Create GitHub release
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
environment: foss-production
|
||||||
|
steps:
|
||||||
|
- name: Decode Keystore
|
||||||
|
env:
|
||||||
|
ENCODED_KEYSTORE: ${{ secrets.SIGNING_KEYSTORE_BASE64 }}
|
||||||
|
run: |
|
||||||
|
TMP_KEYSTORE_DIR_PATH="${RUNNER_TEMP}"/keystore
|
||||||
|
mkdir -p "${TMP_KEYSTORE_DIR_PATH}"
|
||||||
|
TMP_KEYSTORE_FILE_PATH="${TMP_KEYSTORE_DIR_PATH}"/keystore.jks
|
||||||
|
echo $ENCODED_KEYSTORE | base64 -di > "${TMP_KEYSTORE_FILE_PATH}"
|
||||||
|
echo "STORE_PATH=$(echo $TMP_KEYSTORE_FILE_PATH)" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Get the version
|
||||||
|
id: tagger
|
||||||
|
uses: jimschubert/query-tag-action@v2
|
||||||
|
with:
|
||||||
|
skip-unshallow: 'true'
|
||||||
|
abbrev: false
|
||||||
|
commit-ish: HEAD
|
||||||
|
|
||||||
|
- name: Install JDK ${{ matrix.java_version }}
|
||||||
|
uses: actions/setup-java@v3
|
||||||
|
with:
|
||||||
|
distribution: 'adopt'
|
||||||
|
java-version: 11
|
||||||
|
|
||||||
|
- name: Assemble beta APK
|
||||||
|
if: contains(steps.tagger.outputs.tag, '-beta')
|
||||||
|
run: ./gradlew assembleFossBeta
|
||||||
|
env:
|
||||||
|
VERSION: ${{ github.ref }}
|
||||||
|
STORE_PASSWORD: ${{ secrets.STORE_PASSWORD }}
|
||||||
|
KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
|
||||||
|
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
|
||||||
|
BUGSNAG_API_KEY: ${{ secrets.BUGSNAG_API_KEY }}
|
||||||
|
|
||||||
|
- name: Assemble production APK
|
||||||
|
if: "!contains(steps.tagger.outputs.tag, '-beta')"
|
||||||
|
run: ./gradlew assembleFossRelease
|
||||||
|
env:
|
||||||
|
VERSION: ${{ github.ref }}
|
||||||
|
STORE_PASSWORD: ${{ secrets.STORE_PASSWORD }}
|
||||||
|
KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
|
||||||
|
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
|
||||||
|
BUGSNAG_API_KEY: ${{ secrets.BUGSNAG_API_KEY }}
|
||||||
|
|
||||||
|
- name: Create pre-release
|
||||||
|
if: contains(steps.tagger.outputs.tag, '-beta')
|
||||||
|
uses: softprops/action-gh-release@v1
|
||||||
|
with:
|
||||||
|
prerelease: true
|
||||||
|
tag_name: ${{ steps.tagger.outputs.tag }}
|
||||||
|
name: ${{ steps.tagger.outputs.tag }}
|
||||||
|
generate_release_notes: true
|
||||||
|
files: app/build/outputs/apk/beta/*.apk
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Create release
|
||||||
|
if: "!contains(steps.tagger.outputs.tag, '-beta')"
|
||||||
|
uses: softprops/action-gh-release@v1
|
||||||
|
with:
|
||||||
|
prerelease: false
|
||||||
|
tag_name: ${{ steps.tagger.outputs.tag }}
|
||||||
|
name: ${{ steps.tagger.outputs.tag }}
|
||||||
|
generate_release_notes: true
|
||||||
|
files: app/build/outputs/apk/release/*.apk
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
release-gplay:
|
||||||
|
name: Create Google Play release
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
environment: gplay-production
|
||||||
|
steps:
|
||||||
|
- name: Decode Keystore
|
||||||
|
env:
|
||||||
|
ENCODED_KEYSTORE: ${{ secrets.SIGNING_KEYSTORE_BASE64 }}
|
||||||
|
run: |
|
||||||
|
TMP_KEYSTORE_DIR_PATH="${RUNNER_TEMP}"/keystore
|
||||||
|
mkdir -p "${TMP_KEYSTORE_DIR_PATH}"
|
||||||
|
TMP_KEYSTORE_FILE_PATH="${TMP_KEYSTORE_DIR_PATH}"/keystore.jks
|
||||||
|
echo $ENCODED_KEYSTORE | base64 -di > "${TMP_KEYSTORE_FILE_PATH}"
|
||||||
|
echo "STORE_PATH=$(echo $TMP_KEYSTORE_FILE_PATH)" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Decode Google Play service account key
|
||||||
|
env:
|
||||||
|
ENCODED_SERVICE_KEY: ${{ secrets.GPLAY_SERVICE_ACCOUNT_KEY_JSON_BASE64 }}
|
||||||
|
run: |
|
||||||
|
TMP_SERVICEKEY_DIR="${RUNNER_TEMP}"/gplaykey
|
||||||
|
mkdir -p "${TMP_SERVICEKEY_DIR}"
|
||||||
|
TMP_SERVICEKEY_FILE_PATH="${TMP_SERVICEKEY_DIR}"/service_account.json
|
||||||
|
echo $ENCODED_SERVICE_KEY | base64 -di > "${TMP_SERVICEKEY_FILE_PATH}"
|
||||||
|
echo "SUPPLY_JSON_KEY=$(echo $TMP_SERVICEKEY_FILE_PATH)" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Get the version
|
||||||
|
id: tagger
|
||||||
|
uses: jimschubert/query-tag-action@v2
|
||||||
|
with:
|
||||||
|
skip-unshallow: 'true'
|
||||||
|
abbrev: false
|
||||||
|
commit-ish: HEAD
|
||||||
|
|
||||||
|
- name: Install JDK ${{ matrix.java_version }}
|
||||||
|
uses: actions/setup-java@v3
|
||||||
|
with:
|
||||||
|
distribution: 'adopt'
|
||||||
|
java-version: 11
|
||||||
|
|
||||||
|
- name: Set up ruby env
|
||||||
|
uses: ruby/setup-ruby@v1
|
||||||
|
with:
|
||||||
|
ruby-version: 2.7.6
|
||||||
|
bundler-cache: true
|
||||||
|
|
||||||
|
- name: Assemble beta and upload to Google Play
|
||||||
|
if: contains(steps.tagger.outputs.tag, '-beta')
|
||||||
|
run: bundle exec fastlane beta
|
||||||
|
env:
|
||||||
|
STORE_PASSWORD: ${{ secrets.STORE_PASSWORD }}
|
||||||
|
KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
|
||||||
|
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
|
||||||
|
BUGSNAG_API_KEY: ${{ secrets.BUGSNAG_API_KEY }}
|
||||||
|
|
||||||
|
- name: Assemble production and upload to Google Play
|
||||||
|
if: "!contains(steps.tagger.outputs.tag, '-beta')"
|
||||||
|
run: bundle exec fastlane production
|
||||||
|
env:
|
||||||
|
STORE_PASSWORD: ${{ secrets.STORE_PASSWORD }}
|
||||||
|
KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
|
||||||
|
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
|
||||||
|
BUGSNAG_API_KEY: ${{ secrets.BUGSNAG_API_KEY }}
|
||||||
+14
-1
@@ -1,3 +1,16 @@
|
|||||||
.idea
|
.idea
|
||||||
.gradle
|
.gradle
|
||||||
build/
|
build/
|
||||||
|
/fastlane/report.xml
|
||||||
|
*.iml
|
||||||
|
local.properties
|
||||||
|
.DS_Store
|
||||||
|
/build
|
||||||
|
/captures
|
||||||
|
.externalNativeBuild
|
||||||
|
.cxx
|
||||||
|
/.idea/**/*
|
||||||
|
!/.idea/codeStyles/
|
||||||
|
!/.idea/codeStyles/**/*
|
||||||
|
*.jks
|
||||||
|
.local/*
|
||||||
|
|||||||
+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
|
||||||
+28
-10
@@ -1,24 +1,42 @@
|
|||||||
# Privacy policy for the app `CAPod`
|
# Privacy policy
|
||||||
|
This is the privacy policy for the Android app "CAPod - Companion for AirPods".
|
||||||
|
|
||||||
* I do not collect personal information
|
## Preamble
|
||||||
* I do not sell, monetize or otherwise misappropriate any collected data.
|
CAPod respects your privacy.
|
||||||
|
|
||||||
Anonymous device information may be collected in the event of a crash (see [Automatic crash reports](#automatic-crash-reports)).
|
I do not collect, share or sell personal information.
|
||||||
|
|
||||||
|
Send a [quick mail](mailto:support@darken.eu) if you have questions.
|
||||||
|
|
||||||
My underlying privacy principle is the [Golden Rule](https://en.wikipedia.org/wiki/Golden_Rule).
|
My underlying privacy principle is the [Golden Rule](https://en.wikipedia.org/wiki/Golden_Rule).
|
||||||
|
|
||||||
Send a [quick mail](mailto:support@darken.eu) if you have further questions.
|
## Location data
|
||||||
|
|
||||||
|
CAPod does not collect, share or sell location data.
|
||||||
|
|
||||||
|
Location permissions are required to receive Bluetooth Low Energy (BLE) data and ebale its core functionality.
|
||||||
|
The permission "access fine location" (`ACCESS_FINE_LOCATION`) and "access coarse location" (`ACCESS_COARSE_LOCATION`) are required to receive Bluetooth Low Energy data on Android 11 and lower.
|
||||||
|
On Android 12+ the newer and more fine grained `BLUETOOTH_SCAN` permission is used instead.
|
||||||
|
|
||||||
|
Bluetooth Low Energy is a technology that devices like AirPods use to communicate their status to nearby devices.
|
||||||
|
CAPod requests location permissions because these permissions are required to work with Bluetooth Low Energy data.
|
||||||
|
This is a privacy measure on Android's side because you could determine someones location by scanning for Bluetooth devices:
|
||||||
|
If you know the physical location of a Bluetooth device (e.g. AirTags) you could use Bluetooth data to calculate your position.
|
||||||
|
|
||||||
|
### Location access in the background
|
||||||
|
|
||||||
|
CAPod uses the "location access in the background" permission (`ACCESS_BACKGROUND_LOCATION`) on Android 11 and older to receive Bluetooth Low Energy data while the app is in the background. This permission enables the "Show popup" and "Autoconnect" features and allows CAPod to react to nearby devices whil the app is closed.
|
||||||
|
|
||||||
## Automatic crash reports
|
## Automatic crash reports
|
||||||
|
|
||||||
The app uses "Bugsnag" for automatic crash reports:
|
Anonymous device information may be collected in the event of an app crash or error.
|
||||||
|
|
||||||
|
To do this the app uses the service "Bugsnag":
|
||||||
https://www.bugsnag.com/
|
https://www.bugsnag.com/
|
||||||
|
|
||||||
Bugsnags privacy policy can be found here:
|
Bugsnag's privacy policy can be found here:
|
||||||
|
|
||||||
https://docs.bugsnag.com/legal/privacy-policy/
|
https://docs.bugsnag.com/legal/privacy-policy/
|
||||||
|
|
||||||
Crash reports may contain device and app related information.
|
Crash reports may contain device and app related information, e.g. your phone model, Android version and app version.
|
||||||
|
|
||||||
You can disable automatic crash reports in the app's settings.
|
You can disable automatic reports in the app's settings.
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
<img src="https://github.com/d4rken/capod/raw/main/.assets/banner.png" width="400">
|
<img src="https://github.com/d4rken-org/capod/raw/main/.assets/banner.png" width="400">
|
||||||
|
|
||||||
# Companion App for AirPods (CAPod)
|
# Companion App for AirPods (CAPod)
|
||||||
|
[](https://github.com/d4rken-org/capod/releases/latest)
|
||||||

|
[](https://github.com/d4rken/capod/actions/workflows/code-checks.yml)
|
||||||
[](https://github.com/d4rken/capod/releases/latest)
|
[](https://crowdin.com/project/capod)
|
||||||
|
[](https://github.com/d4rken-org/capod/releases/latest)
|
||||||
|
|
||||||
A companion app that adds support for AirPod specific features to Android:
|
A companion app that adds support for AirPod specific features to Android:
|
||||||
|
|
||||||
@@ -19,8 +20,9 @@ CAPod is ad-free. Some additional features require an in-app purchase.
|
|||||||
|
|
||||||
Currently supported models:
|
Currently supported models:
|
||||||
|
|
||||||
* AirPods Gen1
|
* AirPods 1. Generation
|
||||||
* AirPods Gen2
|
* AirPods 2. Generation
|
||||||
|
* AirPods 3. Generation
|
||||||
* AirPods Pro
|
* AirPods Pro
|
||||||
* AirPods Max
|
* AirPods Max
|
||||||
* Power Beats Pro
|
* Power Beats Pro
|
||||||
@@ -33,22 +35,22 @@ Currently supported models:
|
|||||||
## Download
|
## Download
|
||||||
|
|
||||||
* [Google Play](https://play.google.com/store/apps/details?id=eu.darken.capod)
|
* [Google Play](https://play.google.com/store/apps/details?id=eu.darken.capod)
|
||||||
* [GitHub](https://github.com/d4rken/capod/releases/latest)
|
* [GitHub](https://github.com/d4rken-org/capod/releases/latest)
|
||||||
|
|
||||||
## Support the project
|
## Support the project
|
||||||
|
|
||||||
* Buy the CAPod Pro In-App purchase on [Google Play](https://play.google.com/store/apps/details?id=eu.darken.capod)
|
* Buy the CAPod Pro In-App purchase on [Google Play](https://play.google.com/store/apps/details?id=eu.darken.capod)
|
||||||
* Help translate CAPod [on Crowdin](https://crowdin.com/project/capod)
|
* Help translate CAPod [on Crowdin](https://crowdin.com/project/capod)
|
||||||
* [Buy me a coffee](https://www.buymeacoffee.com/tydarken)
|
|
||||||
|
|
||||||
## Get help
|
## Get help
|
||||||
|
|
||||||
* [Github Issues](https://github.com/d4rken/capod/issues)
|
* [Github Issues](https://github.com/d4rken-org/capod/issues)
|
||||||
* [Discord](https://discord.gg/vHubYPp)
|
* [Discord](https://discord.gg/vHubYPp)
|
||||||
|
* [Email](mailto:support@darken.eu)
|
||||||
|
|
||||||
## Screenshots
|
## Screenshots
|
||||||
|
|
||||||
<img src="https://github.com/d4rken/capod/raw/main/.assets/screenshots/1.png" width="200"><img src="https://github.com/d4rken/capod/raw/main/.assets/screenshots/2.png" width="200"><img src="https://github.com/d4rken/capod/raw/main/.assets/screenshots/3.png" width="200"><img src="https://github.com/d4rken/capod/raw/main/.assets/screenshots/4.png" width="200">
|
<img src="https://github.com/d4rken-org/capod/raw/main/.assets/screenshots/1.png" width="200"><img src="https://github.com/d4rken-org/capod/raw/main/.assets/screenshots/2.png" width="200"><img src="https://github.com/d4rken-org/capod/raw/main/.assets/screenshots/3.png" width="200"><img src="https://github.com/d4rken-org/capod/raw/main/.assets/screenshots/4.png" width="200">
|
||||||
|
|
||||||
## Thanks to
|
## Thanks to
|
||||||
|
|
||||||
|
|||||||
+57
-28
@@ -16,10 +16,6 @@ android {
|
|||||||
|
|
||||||
compileSdkVersion buildConfig.compileSdk
|
compileSdkVersion buildConfig.compileSdk
|
||||||
|
|
||||||
Properties bugsnagProps = new Properties()
|
|
||||||
def bugsnagPropsFile = new File(System.properties['user.home'], ".appconfig/${packageName}/bugsnag.properties")
|
|
||||||
if (bugsnagPropsFile.canRead()) bugsnagProps.load(new FileInputStream(bugsnagPropsFile))
|
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "${packageName}"
|
applicationId "${packageName}"
|
||||||
|
|
||||||
@@ -38,22 +34,55 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
signingConfigs {
|
signingConfigs {
|
||||||
release {}
|
releaseFoss {}
|
||||||
|
releaseGplay {}
|
||||||
}
|
}
|
||||||
def signingPropFile = new File(System.properties['user.home'], ".appconfig/${packageName}/signing.properties")
|
|
||||||
if (signingPropFile.canRead()) {
|
signingConfigs {
|
||||||
Properties signingProps = new Properties()
|
releaseFoss {
|
||||||
signingProps.load(new FileInputStream(signingPropFile))
|
def signingFossPropFile = new File(System.properties['user.home'], ".appconfig/${packageName}/signing-foss.properties")
|
||||||
signingConfigs {
|
Properties signingPropsFoss = new Properties()
|
||||||
release {
|
if (signingFossPropFile.canRead()) signingPropsFoss.load(new FileInputStream(signingFossPropFile))
|
||||||
storeFile new File(signingProps['release.storePath'])
|
String keyStorePathFoss = System.getenv("STORE_PATH") ?: signingPropsFoss["release.storePath"]
|
||||||
keyAlias signingProps['release.keyAlias']
|
File keyStoreFoss = keyStorePathFoss ? new File(keyStorePathFoss) : null
|
||||||
storePassword signingProps['release.storePassword']
|
if (keyStoreFoss?.canRead()) {
|
||||||
keyPassword signingProps['release.keyPassword']
|
storeFile keyStoreFoss
|
||||||
|
storePassword System.getenv("STORE_PASSWORD") ?: signingPropsFoss['release.storePassword']
|
||||||
|
keyAlias System.getenv("KEY_ALIAS") ?: signingPropsFoss['release.keyAlias']
|
||||||
|
keyPassword System.getenv("KEY_PASSWORD") ?: signingPropsFoss['release.keyPassword']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
releaseGplay {
|
||||||
|
def signingGplayPropFile = new File(System.properties['user.home'], ".appconfig/${packageName}/signing-gplay.properties")
|
||||||
|
Properties signingPropsGplay = new Properties()
|
||||||
|
if (signingGplayPropFile.canRead()) signingPropsGplay.load(new FileInputStream(signingGplayPropFile))
|
||||||
|
String keyStorePathGplay = System.getenv("STORE_PATH") ?: signingPropsGplay["release.storePath"]
|
||||||
|
File keyStoreGplay = keyStorePathGplay ? new File(keyStorePathGplay) : null
|
||||||
|
if (keyStoreGplay?.canRead()) {
|
||||||
|
storeFile keyStoreGplay
|
||||||
|
storePassword System.getenv("STORE_PASSWORD") ?: signingPropsGplay['release.storePassword']
|
||||||
|
keyAlias System.getenv("KEY_ALIAS") ?: signingPropsGplay['release.keyAlias']
|
||||||
|
keyPassword System.getenv("KEY_PASSWORD") ?: signingPropsGplay['release.keyPassword']
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
flavorDimensions "version"
|
||||||
|
productFlavors {
|
||||||
|
foss {
|
||||||
|
signingConfig signingConfigs.releaseFoss
|
||||||
|
}
|
||||||
|
gplay {
|
||||||
|
signingConfig signingConfigs.releaseGplay
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Properties bugsnagProps = new Properties()
|
||||||
|
def bugsnagPropsFile = new File(System.properties['user.home'], ".appconfig/${packageName}/bugsnag.properties")
|
||||||
|
if (bugsnagPropsFile.canRead()) bugsnagProps.load(new FileInputStream(bugsnagPropsFile))
|
||||||
|
String bugSnagApiKey = System.getenv("BUGSNAG_API_KEY") ?: bugsnagProps.getProperty("bugsnag.apikey", "")
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
def proguardRulesRelease = fileTree(dir: "../proguard", include: ["*.pro"]).asList().toArray()
|
def proguardRulesRelease = fileTree(dir: "../proguard", include: ["*.pro"]).asList().toArray()
|
||||||
debug {
|
debug {
|
||||||
@@ -62,10 +91,9 @@ android {
|
|||||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt')
|
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt')
|
||||||
proguardFiles proguardRulesRelease
|
proguardFiles proguardRulesRelease
|
||||||
proguardFiles 'proguard-rules-debug.pro'
|
proguardFiles 'proguard-rules-debug.pro'
|
||||||
manifestPlaceholders = [bugsnagApiKey: bugsnagProps.getProperty("bugsnag.apikey", "")]
|
manifestPlaceholders = [bugsnagApiKey: bugSnagApiKey]
|
||||||
}
|
}
|
||||||
release {
|
beta {
|
||||||
signingConfig signingConfigs.release
|
|
||||||
lintOptions {
|
lintOptions {
|
||||||
abortOnError true
|
abortOnError true
|
||||||
fatal 'StopShip'
|
fatal 'StopShip'
|
||||||
@@ -74,17 +102,18 @@ android {
|
|||||||
shrinkResources true
|
shrinkResources true
|
||||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt')
|
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt')
|
||||||
proguardFiles proguardRulesRelease
|
proguardFiles proguardRulesRelease
|
||||||
manifestPlaceholders = [bugsnagApiKey: bugsnagProps.getProperty("bugsnag.apikey", "")]
|
manifestPlaceholders = [bugsnagApiKey: bugSnagApiKey]
|
||||||
}
|
}
|
||||||
}
|
release {
|
||||||
|
lintOptions {
|
||||||
flavorDimensions "version"
|
abortOnError true
|
||||||
productFlavors {
|
fatal 'StopShip'
|
||||||
gplay {
|
}
|
||||||
|
minifyEnabled true
|
||||||
}
|
shrinkResources true
|
||||||
foss {
|
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt')
|
||||||
|
proguardFiles proguardRulesRelease
|
||||||
|
manifestPlaceholders = [bugsnagApiKey: bugSnagApiKey]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ class UpgradeControlFoss @Inject constructor(
|
|||||||
upgradedAt = Instant.now(),
|
upgradedAt = Instant.now(),
|
||||||
reason = FossUpgrade.Reason.DONATED
|
reason = FossUpgrade.Reason.DONATED
|
||||||
)
|
)
|
||||||
webpageTool.open("https://github.com/d4rken/capod")
|
webpageTool.open("https://github.com/d4rken-org/capod")
|
||||||
Toast.makeText(activity, R.string.general_thank_you_label, Toast.LENGTH_SHORT).show()
|
Toast.makeText(activity, R.string.general_thank_you_label, Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
setNegativeButton(R.string.foss_upgrade_alreadydonated_label) { _, _ ->
|
setNegativeButton(R.string.foss_upgrade_alreadydonated_label) { _, _ ->
|
||||||
|
|||||||
@@ -1,2 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources></resources>
|
<resources>
|
||||||
|
<string name="foss_upgrade_donate_label">Donar</string>
|
||||||
|
<string name="foss_upgrade_alreadydonated_label">Ya doné</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"?>
|
<?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"?>
|
<?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"?>
|
<?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"?>
|
<?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,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?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 han encontrado compras. ¿Estás usando la cuenta correcta?</string>
|
||||||
|
</resources>
|
||||||
|
|||||||
@@ -1,2 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources></resources>
|
<resources>
|
||||||
|
<string name="upgrades_gplay_unavailable_error">Google Play सेवाएं अनुपलब्ध हैं।</string>
|
||||||
|
<string name="upgrades_no_purchases_found_check_account">कोई खरीदारी नहीं मिली. क्या आप सही खाते का उपयोग कर रहे हैं?</string>
|
||||||
|
</resources>
|
||||||
|
|||||||
@@ -1,2 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources></resources>
|
<resources>
|
||||||
|
<string name="upgrades_gplay_unavailable_error">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"?>
|
<?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"?>
|
<?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>
|
||||||
|
|||||||
@@ -6,10 +6,34 @@ import eu.darken.capod.BuildConfig
|
|||||||
// Can't be const because that prevents them from being mocked in tests
|
// Can't be const because that prevents them from being mocked in tests
|
||||||
@Suppress("MayBeConstant")
|
@Suppress("MayBeConstant")
|
||||||
object BuildConfigWrap {
|
object BuildConfigWrap {
|
||||||
val FLAVOR: String = BuildConfig.FLAVOR
|
|
||||||
val BUILD_TYPE: String = BuildConfig.BUILD_TYPE
|
|
||||||
val DEBUG: Boolean = BuildConfig.DEBUG
|
val DEBUG: Boolean = BuildConfig.DEBUG
|
||||||
|
|
||||||
|
val BUILD_TYPE: BuildType = when (val typ = BuildConfig.BUILD_TYPE) {
|
||||||
|
"debug" -> BuildType.DEV
|
||||||
|
"beta" -> BuildType.BETA
|
||||||
|
"release" -> BuildType.RELEASE
|
||||||
|
else -> throw IllegalArgumentException("Unknown buildtype: $typ")
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class BuildType {
|
||||||
|
DEV,
|
||||||
|
BETA,
|
||||||
|
RELEASE,
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
val FLAVOR: Flavor = when (val flav = BuildConfig.FLAVOR) {
|
||||||
|
"gplay" -> Flavor.GPLAY
|
||||||
|
"foss" -> Flavor.FOSS
|
||||||
|
else -> throw IllegalStateException("Unknown flavor: $flav")
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class Flavor {
|
||||||
|
GPLAY,
|
||||||
|
FOSS,
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
val APPLICATION_ID = BuildConfig.APPLICATION_ID
|
val APPLICATION_ID = BuildConfig.APPLICATION_ID
|
||||||
|
|
||||||
val VERSION_CODE: Long = BuildConfig.VERSION_CODE.toLong()
|
val VERSION_CODE: Long = BuildConfig.VERSION_CODE.toLong()
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
package eu.darken.capod.common
|
package eu.darken.capod.common
|
||||||
|
|
||||||
object PrivacyPolicy {
|
object PrivacyPolicy {
|
||||||
const val URL = "https://raw.githubusercontent.com/d4rken/capod-public/main/PRIVACY_POLICY.md"
|
const val URL = "https://raw.githubusercontent.com/d4rken-org/capod/main/PRIVACY_POLICY.md"
|
||||||
}
|
}
|
||||||
@@ -33,6 +33,7 @@ class BleScanner @Inject constructor(
|
|||||||
scannerMode: ScannerMode,
|
scannerMode: ScannerMode,
|
||||||
compatMode: Boolean,
|
compatMode: Boolean,
|
||||||
): Flow<List<BleScanResult>> = callbackFlow {
|
): Flow<List<BleScanResult>> = callbackFlow {
|
||||||
|
log(TAG, VERBOSE) { "scan(filters=$filters, scannerMode=$scannerMode, compatMode=$compatMode)" }
|
||||||
if (compatMode) log(TAG, WARN) { "Using compatibilityMode!" }
|
if (compatMode) log(TAG, WARN) { "Using compatibilityMode!" }
|
||||||
|
|
||||||
val adapter = bluetoothManager.adapter
|
val adapter = bluetoothManager.adapter
|
||||||
@@ -55,7 +56,11 @@ class BleScanner @Inject constructor(
|
|||||||
lastScanAt = System.currentTimeMillis()
|
lastScanAt = System.currentTimeMillis()
|
||||||
"onScanResult(delay=${delay}ms, callbackType=$callbackType, result=$result)"
|
"onScanResult(delay=${delay}ms, callbackType=$callbackType, result=$result)"
|
||||||
}
|
}
|
||||||
val toSend = if (supportsOffloadFiltering || filters.isEmpty() || filters.any { it.matches(result) }) {
|
val toSend = if (
|
||||||
|
supportsOffloadFiltering
|
||||||
|
|| filters.isEmpty()
|
||||||
|
|| filters.any { it.matchesSafe(result) }
|
||||||
|
) {
|
||||||
listOf(BleScanResult.fromScanResult(result))
|
listOf(BleScanResult.fromScanResult(result))
|
||||||
} else {
|
} else {
|
||||||
log(TAG, VERBOSE) { "Manual filtering: No match for $result" }
|
log(TAG, VERBOSE) { "Manual filtering: No match for $result" }
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
package eu.darken.capod.common.bluetooth
|
|
||||||
|
|
||||||
import android.bluetooth.BluetoothDevice
|
|
||||||
import android.os.ParcelUuid
|
|
||||||
|
|
||||||
fun BluetoothDevice.hasFeature(uuid: ParcelUuid): Boolean {
|
|
||||||
return uuids?.contains(uuid) ?: false
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package eu.darken.capod.common.bluetooth
|
||||||
|
|
||||||
|
import android.bluetooth.BluetoothDevice
|
||||||
|
import android.bluetooth.le.ScanFilter
|
||||||
|
import android.bluetooth.le.ScanResult
|
||||||
|
import android.os.ParcelUuid
|
||||||
|
import eu.darken.capod.common.debug.logging.asLog
|
||||||
|
import eu.darken.capod.common.debug.logging.log
|
||||||
|
|
||||||
|
fun BluetoothDevice.hasFeature(uuid: ParcelUuid): Boolean {
|
||||||
|
return uuids?.contains(uuid) ?: false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* java.lang.NullPointerException: Attempt to invoke virtual method 'java.lang.Object android.util.SparseArray.get(int)' on a null object reference
|
||||||
|
* at android.bluetooth.le.ScanRecord.getManufacturerSpecificData(ScanRecord.java:118)
|
||||||
|
* at android.bluetooth.le.ScanFilter.matches(ScanFilter.java:369)
|
||||||
|
* ZenFone Max Pro M1 (ZB602KL) (WW) / Max Pro M1 (ZB601KL) (IN) (ZB602KL), Android 9, PKQ1.WW_Phone-16.2017.2009.087-20200826
|
||||||
|
* Intel Gemini Lake Chromebook (octopus), Android 9, R99-14469.59.0 release-keys
|
||||||
|
*/
|
||||||
|
fun ScanFilter.matchesSafe(scanResult: ScanResult): Boolean = try {
|
||||||
|
matches(scanResult)
|
||||||
|
} catch (e: NullPointerException) {
|
||||||
|
log { "AOSP error: ${e.asLog()}" }
|
||||||
|
false
|
||||||
|
}
|
||||||
@@ -75,6 +75,19 @@ class FakeBleData @Inject constructor(
|
|||||||
fakeDevices.add(this)
|
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
|
// Unknown Device
|
||||||
BleScanResult(
|
BleScanResult(
|
||||||
address = "6E:9E:D1:49:D2:6D",
|
address = "6E:9E:D1:49:D2:6D",
|
||||||
|
|||||||
@@ -28,12 +28,12 @@ class AutoReporting @Inject constructor(
|
|||||||
) {
|
) {
|
||||||
|
|
||||||
fun setup() {
|
fun setup() {
|
||||||
val isEnabled = debugSettings.isAutoReportEnabled.value
|
val isEnabled = debugSettings.isAutoReportingEnabled.value
|
||||||
log(TAG) { "setup(): isEnabled=$isEnabled" }
|
log(TAG) { "setup(): isEnabled=$isEnabled" }
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val bugsnagConfig = Configuration.load(context).apply {
|
val bugsnagConfig = Configuration.load(context).apply {
|
||||||
if (debugSettings.isAutoReportEnabled.value) {
|
if (debugSettings.isAutoReportingEnabled.value) {
|
||||||
Logging.install(bugsnagLogger.get())
|
Logging.install(bugsnagLogger.get())
|
||||||
setUser(installId.id, null, null)
|
setUser(installId.id, null, null)
|
||||||
autoTrackSessions = true
|
autoTrackSessions = true
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import android.content.Context
|
|||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import androidx.preference.PreferenceDataStore
|
import androidx.preference.PreferenceDataStore
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import eu.darken.capod.common.BuildConfigWrap
|
||||||
import eu.darken.capod.common.preferences.PreferenceStoreMapper
|
import eu.darken.capod.common.preferences.PreferenceStoreMapper
|
||||||
import eu.darken.capod.common.preferences.Settings
|
import eu.darken.capod.common.preferences.Settings
|
||||||
import eu.darken.capod.common.preferences.createFlowPreference
|
import eu.darken.capod.common.preferences.createFlowPreference
|
||||||
@@ -17,8 +18,11 @@ class DebugSettings @Inject constructor(
|
|||||||
|
|
||||||
override val preferences: SharedPreferences = context.getSharedPreferences("settings_debug", Context.MODE_PRIVATE)
|
override val preferences: SharedPreferences = context.getSharedPreferences("settings_debug", Context.MODE_PRIVATE)
|
||||||
|
|
||||||
val isAutoReportEnabled = preferences.createFlowPreference("debug.bugreport.automatic.enabled", true)
|
val isAutoReportingEnabled = preferences.createFlowPreference(
|
||||||
|
key = "debug.bugreport.automatic.enabled",
|
||||||
|
// Reporting is opt-out for gplay, and opt-in for github builds
|
||||||
|
defaultValue = BuildConfigWrap.FLAVOR == BuildConfigWrap.Flavor.GPLAY
|
||||||
|
)
|
||||||
val isDebugModeEnabled = preferences.createFlowPreference("debug.mode.enabled", false)
|
val isDebugModeEnabled = preferences.createFlowPreference("debug.mode.enabled", false)
|
||||||
|
|
||||||
val showFakeData = preferences.createFlowPreference("debug.fakedata.enabled", false)
|
val showFakeData = preferences.createFlowPreference("debug.fakedata.enabled", false)
|
||||||
|
|||||||
+1
-1
@@ -32,7 +32,7 @@ class BugsnagErrorHandler @Inject constructor(
|
|||||||
context.tryFormattedSignature()?.let { event.addMetadata(tab, "signatures", it) }
|
context.tryFormattedSignature()?.let { event.addMetadata(tab, "signatures", it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
return debugSettings.isAutoReportEnabled.value && !BuildConfigWrap.DEBUG
|
return debugSettings.isAutoReportingEnabled.value && !BuildConfigWrap.DEBUG
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ class RecorderModule @Inject constructor(
|
|||||||
|
|
||||||
private fun createRecordingFilePath() = File(
|
private fun createRecordingFilePath() = File(
|
||||||
File(context.cacheDir, "debug/logs"),
|
File(context.cacheDir, "debug/logs"),
|
||||||
"bb_logfile_${System.currentTimeMillis()}.log"
|
"capod_logfile_${System.currentTimeMillis()}.log"
|
||||||
)
|
)
|
||||||
|
|
||||||
suspend fun startRecorder(): File {
|
suspend fun startRecorder(): File {
|
||||||
@@ -123,6 +123,6 @@ class RecorderModule @Inject constructor(
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
internal val TAG = logTag("Debug", "Log", "Recorder", "Module")
|
internal val TAG = logTag("Debug", "Log", "Recorder", "Module")
|
||||||
private const val FORCE_FILE = "bb_force_debug_run"
|
private const val FORCE_FILE = "capod_force_debug_run"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -67,6 +67,6 @@ class GeneralSettings @Inject constructor(
|
|||||||
showAll,
|
showAll,
|
||||||
minimumSignalQuality,
|
minimumSignalQuality,
|
||||||
mainDeviceAddress,
|
mainDeviceAddress,
|
||||||
debugSettings.isAutoReportEnabled,
|
debugSettings.isAutoReportingEnabled,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -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.BluetoothDisabledVH
|
||||||
import eu.darken.capod.main.ui.overview.cards.MissingMainDeviceVH
|
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.PermissionCardVH
|
||||||
import eu.darken.capod.main.ui.overview.cards.pods.BasicSingleApplePodsCardVH
|
import eu.darken.capod.main.ui.overview.cards.pods.DualPodsCardVH
|
||||||
import eu.darken.capod.main.ui.overview.cards.pods.DualApplePodsCardVH
|
import eu.darken.capod.main.ui.overview.cards.pods.SinglePodsCardVH
|
||||||
import eu.darken.capod.main.ui.overview.cards.pods.SingleApplePodsCardVH
|
|
||||||
import eu.darken.capod.main.ui.overview.cards.pods.UnknownPodDeviceCardVH
|
import eu.darken.capod.main.ui.overview.cards.pods.UnknownPodDeviceCardVH
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@@ -29,9 +28,8 @@ class OverviewAdapter @Inject constructor() :
|
|||||||
init {
|
init {
|
||||||
modules.add(DataBinderMod(data))
|
modules.add(DataBinderMod(data))
|
||||||
modules.add(TypedVHCreatorMod({ data[it] is PermissionCardVH.Item }) { PermissionCardVH(it) })
|
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 DualPodsCardVH.Item }) { DualPodsCardVH(it) })
|
||||||
modules.add(TypedVHCreatorMod({ data[it] is SingleApplePodsCardVH.Item }) { SingleApplePodsCardVH(it) })
|
modules.add(TypedVHCreatorMod({ data[it] is SinglePodsCardVH.Item }) { SinglePodsCardVH(it) })
|
||||||
modules.add(TypedVHCreatorMod({ data[it] is BasicSingleApplePodsCardVH.Item }) { BasicSingleApplePodsCardVH(it) })
|
|
||||||
modules.add(TypedVHCreatorMod({ data[it] is MissingMainDeviceVH.Item }) { MissingMainDeviceVH(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 BluetoothDisabledVH.Item }) { BluetoothDisabledVH(it) })
|
||||||
modules.add(TypedVHCreatorMod({ data[it] is UnknownPodDeviceCardVH.Item }) { UnknownPodDeviceCardVH(it) })
|
modules.add(TypedVHCreatorMod({ data[it] is UnknownPodDeviceCardVH.Item }) { UnknownPodDeviceCardVH(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.BluetoothDisabledVH
|
||||||
import eu.darken.capod.main.ui.overview.cards.MissingMainDeviceVH
|
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.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.PodMonitor
|
||||||
import eu.darken.capod.monitor.core.worker.MonitorControl
|
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.PodDevice
|
||||||
import eu.darken.capod.pods.core.apple.BasicSingleApplePods
|
import eu.darken.capod.pods.core.SinglePodDevice
|
||||||
import eu.darken.capod.pods.core.apple.DualApplePods
|
|
||||||
import eu.darken.capod.pods.core.apple.SingleApplePods
|
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
import kotlinx.coroutines.isActive
|
import kotlinx.coroutines.isActive
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.util.*
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class OverviewFragmentVM @Inject constructor(
|
class OverviewFragmentVM @Inject constructor(
|
||||||
handle: SavedStateHandle,
|
@Suppress("UNUSED_PARAMETER") handle: SavedStateHandle,
|
||||||
dispatcherProvider: DispatcherProvider,
|
dispatcherProvider: DispatcherProvider,
|
||||||
private val monitorControl: MonitorControl,
|
private val monitorControl: MonitorControl,
|
||||||
private val podMonitor: PodMonitor,
|
private val podMonitor: PodMonitor,
|
||||||
@@ -74,7 +74,7 @@ class OverviewFragmentVM @Inject constructor(
|
|||||||
monitorControl.startMonitor()
|
monitorControl.startMonitor()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.map { Unit }
|
.map { }
|
||||||
.asLiveData2()
|
.asLiveData2()
|
||||||
|
|
||||||
val requestPermissionEvent = SingleLiveEvent<Permission>()
|
val requestPermissionEvent = SingleLiveEvent<Permission>()
|
||||||
@@ -129,19 +129,13 @@ class OverviewFragmentVM @Inject constructor(
|
|||||||
pods.map {
|
pods.map {
|
||||||
val now = Instant.now()
|
val now = Instant.now()
|
||||||
when (it) {
|
when (it) {
|
||||||
is DualApplePods -> DualApplePodsCardVH.Item(
|
is DualPodDevice -> DualPodsCardVH.Item(
|
||||||
now = now,
|
now = now,
|
||||||
device = it,
|
device = it,
|
||||||
showDebug = isDebugMode,
|
showDebug = isDebugMode,
|
||||||
isMainPod = it == mainPod,
|
isMainPod = it == mainPod,
|
||||||
)
|
)
|
||||||
is SingleApplePods -> SingleApplePodsCardVH.Item(
|
is SinglePodDevice -> SinglePodsCardVH.Item(
|
||||||
now = now,
|
|
||||||
device = it,
|
|
||||||
showDebug = isDebugMode,
|
|
||||||
isMainPod = it == mainPod,
|
|
||||||
)
|
|
||||||
is BasicSingleApplePods -> BasicSingleApplePodsCardVH.Item(
|
|
||||||
now = now,
|
now = now,
|
||||||
device = it,
|
device = it,
|
||||||
showDebug = isDebugMode,
|
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.pods_case_unknown_state)
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
||||||
|
}
|
||||||
+1
@@ -34,6 +34,7 @@ class DebugSettingsFragment : PreferenceFragment2() {
|
|||||||
vm.toggleRecorder()
|
vm.toggleRecorder()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+19
@@ -3,10 +3,15 @@ package eu.darken.capod.main.ui.settings.general.debug
|
|||||||
import androidx.lifecycle.SavedStateHandle
|
import androidx.lifecycle.SavedStateHandle
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import eu.darken.capod.common.coroutine.DispatcherProvider
|
import eu.darken.capod.common.coroutine.DispatcherProvider
|
||||||
|
import eu.darken.capod.common.debug.autoreport.DebugSettings
|
||||||
|
import eu.darken.capod.common.debug.logging.log
|
||||||
import eu.darken.capod.common.debug.logging.logTag
|
import eu.darken.capod.common.debug.logging.logTag
|
||||||
import eu.darken.capod.common.debug.recording.core.RecorderModule
|
import eu.darken.capod.common.debug.recording.core.RecorderModule
|
||||||
import eu.darken.capod.common.uix.ViewModel3
|
import eu.darken.capod.common.uix.ViewModel3
|
||||||
|
import eu.darken.capod.main.core.GeneralSettings
|
||||||
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
@@ -14,10 +19,24 @@ class DebugSettingsFragmentVM @Inject constructor(
|
|||||||
private val handle: SavedStateHandle,
|
private val handle: SavedStateHandle,
|
||||||
dispatcherProvider: DispatcherProvider,
|
dispatcherProvider: DispatcherProvider,
|
||||||
private val recorderModule: RecorderModule,
|
private val recorderModule: RecorderModule,
|
||||||
|
private val generalSettings: GeneralSettings,
|
||||||
|
private val debugSettings: DebugSettings,
|
||||||
) : ViewModel3(dispatcherProvider) {
|
) : ViewModel3(dispatcherProvider) {
|
||||||
|
|
||||||
val state = recorderModule.state.asLiveData2()
|
val state = recorderModule.state.asLiveData2()
|
||||||
|
|
||||||
|
init {
|
||||||
|
debugSettings.showUnfiltered.flow
|
||||||
|
.distinctUntilChanged()
|
||||||
|
.onEach { showUnfiltered ->
|
||||||
|
if (showUnfiltered) {
|
||||||
|
log(TAG) { "Enabling 'show all' due to debug setting 'show unfiltered' enabled" }
|
||||||
|
generalSettings.showAll.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.launchInViewModel()
|
||||||
|
}
|
||||||
|
|
||||||
fun toggleRecorder() = launch {
|
fun toggleRecorder() = launch {
|
||||||
if (recorderModule.state.first().isRecording) {
|
if (recorderModule.state.first().isRecording) {
|
||||||
recorderModule.stopRecorder()
|
recorderModule.stopRecorder()
|
||||||
|
|||||||
@@ -15,9 +15,9 @@ import javax.inject.Inject
|
|||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class SupportFragmentVM @Inject constructor(
|
class SupportFragmentVM @Inject constructor(
|
||||||
private val handle: SavedStateHandle,
|
private val handle: SavedStateHandle,
|
||||||
|
private val dispatcherProvider: DispatcherProvider,
|
||||||
private val emailTool: EmailTool,
|
private val emailTool: EmailTool,
|
||||||
private val installId: InstallId,
|
private val installId: InstallId,
|
||||||
private val dispatcherProvider: DispatcherProvider,
|
|
||||||
) : ViewModel3(dispatcherProvider) {
|
) : ViewModel3(dispatcherProvider) {
|
||||||
|
|
||||||
val emailEvent = SingleLiveEvent<Intent>()
|
val emailEvent = SingleLiveEvent<Intent>()
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ class PodMonitor @Inject constructor(
|
|||||||
) { scannerMode, compatMode, unfiltered ->
|
) { scannerMode, compatMode, unfiltered ->
|
||||||
Triple(scannerMode, compatMode, unfiltered)
|
Triple(scannerMode, compatMode, unfiltered)
|
||||||
}.flatMapLatest { (mode, compat, unfiltered) ->
|
}.flatMapLatest { (mode, compat, unfiltered) ->
|
||||||
|
log(TAG, VERBOSE) { "Starting BLEScanner mode=$mode, compat=$compat, unfiltered=$unfiltered" }
|
||||||
val filters = if (unfiltered) {
|
val filters = if (unfiltered) {
|
||||||
setOf(getUnfilteredFilter())
|
setOf(getUnfilteredFilter())
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -6,8 +6,7 @@ import android.widget.RemoteViews
|
|||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import eu.darken.capod.R
|
import eu.darken.capod.R
|
||||||
import eu.darken.capod.pods.core.*
|
import eu.darken.capod.pods.core.*
|
||||||
import eu.darken.capod.pods.core.apple.BasicSingleApplePods
|
import eu.darken.capod.pods.core.apple.DualAirPods
|
||||||
import eu.darken.capod.pods.core.apple.DualApplePods
|
|
||||||
import eu.darken.capod.pods.core.apple.SingleApplePods
|
import eu.darken.capod.pods.core.apple.SingleApplePods
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@@ -17,13 +16,12 @@ class MonitorNotificationViewFactory @Inject constructor(
|
|||||||
) {
|
) {
|
||||||
|
|
||||||
fun createContentView(device: PodDevice): RemoteViews = when (device) {
|
fun createContentView(device: PodDevice): RemoteViews = when (device) {
|
||||||
is DualApplePods -> createDualApplePods(device)
|
is DualAirPods -> createDualApplePods(device)
|
||||||
is SingleApplePods -> createSingleApplePods(device)
|
is SingleApplePods -> createSingleApplePods(device)
|
||||||
is BasicSingleApplePods -> createSingleBasicApplePods(device)
|
|
||||||
else -> createUnknownDevice(device)
|
else -> createUnknownDevice(device)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createDualApplePods(device: DualApplePods): RemoteViews = RemoteViews(
|
private fun createDualApplePods(device: DualAirPods): RemoteViews = RemoteViews(
|
||||||
context.packageName,
|
context.packageName,
|
||||||
R.layout.monitor_notification_dual_pods_small
|
R.layout.monitor_notification_dual_pods_small
|
||||||
).apply {
|
).apply {
|
||||||
@@ -52,19 +50,12 @@ class MonitorNotificationViewFactory @Inject constructor(
|
|||||||
setTextViewText(R.id.headphones_label, getLabel(context))
|
setTextViewText(R.id.headphones_label, getLabel(context))
|
||||||
setImageViewResource(R.id.headphones_battery_icon, getBatteryDrawable(batteryHeadsetPercent))
|
setImageViewResource(R.id.headphones_battery_icon, getBatteryDrawable(batteryHeadsetPercent))
|
||||||
setTextViewText(R.id.headphones_battery_label, getBatteryLevelHeadset(context))
|
setTextViewText(R.id.headphones_battery_label, getBatteryLevelHeadset(context))
|
||||||
setViewVisibility(R.id.headphones_charging, if (isHeadsetBeingCharged) View.VISIBLE else View.GONE)
|
if (this is HasEarDetection) {
|
||||||
setViewVisibility(R.id.headphones_worn, if (isHeadphonesBeingWorn) View.VISIBLE else View.GONE)
|
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)
|
||||||
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))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
package eu.darken.capod.pods.core
|
package eu.darken.capod.pods.core
|
||||||
|
|
||||||
interface HasDualPods {
|
interface DualPodDevice : PodDevice {
|
||||||
|
|
||||||
enum class Pod {
|
enum class Pod {
|
||||||
LEFT,
|
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
|
package eu.darken.capod.pods.core
|
||||||
|
|
||||||
interface HasEarDetection : PodDevice {
|
interface HasEarDetection {
|
||||||
|
|
||||||
val isBeingWorn: Boolean
|
val isBeingWorn: Boolean
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
package eu.darken.capod.pods.core
|
package eu.darken.capod.pods.core
|
||||||
|
|
||||||
interface HasDualEarDetection : HasEarDetection {
|
interface HasEarDetectionDual : HasEarDetection {
|
||||||
|
|
||||||
val isLeftPodInEar: Boolean
|
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,12 @@ interface PodDevice {
|
|||||||
@Json(name = "beats.powerbeats.pro") POWERBEATS_PRO(
|
@Json(name = "beats.powerbeats.pro") POWERBEATS_PRO(
|
||||||
"Power Beats Pro"
|
"Power Beats Pro"
|
||||||
),
|
),
|
||||||
|
@Json(name = "fakes.tws.i99999") TWS_I99999(
|
||||||
|
"TWS i99999"
|
||||||
|
),
|
||||||
|
@Json(name = "fakes.varunr.airpodspro") VARUNR_AIRPODS_PRO(
|
||||||
|
"Fake AirPods Pro"
|
||||||
|
),
|
||||||
@Json(name = "unknown") UNKNOWN(
|
@Json(name = "unknown") UNKNOWN(
|
||||||
"Unknown"
|
"Unknown"
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -8,11 +8,11 @@ import java.time.Duration
|
|||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
fun HasDualPods.getBatteryLevelLeftPod(context: Context): String =
|
fun DualPodDevice.getBatteryLevelLeftPod(context: Context): String =
|
||||||
batteryLeftPodPercent?.let { "${(it * 100).roundToInt()}%" }
|
batteryLeftPodPercent?.let { "${(it * 100).roundToInt()}%" }
|
||||||
?: context.getString(R.string.general_value_not_available_label)
|
?: 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()}%" }
|
batteryRightPodPercent?.let { "${(it * 100).roundToInt()}%" }
|
||||||
?: context.getString(R.string.general_value_not_available_label)
|
?: context.getString(R.string.general_value_not_available_label)
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@ fun HasCase.getBatteryLevelCase(context: Context): String =
|
|||||||
batteryCasePercent?.let { "${(it * 100).roundToInt()}%" }
|
batteryCasePercent?.let { "${(it * 100).roundToInt()}%" }
|
||||||
?: context.getString(R.string.general_value_not_available_label)
|
?: 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()}%" }
|
batteryHeadsetPercent?.let { "${(it * 100).roundToInt()}%" }
|
||||||
?: context.getString(R.string.general_value_not_available_label)
|
?: context.getString(R.string.general_value_not_available_label)
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
package eu.darken.capod.pods.core
|
package eu.darken.capod.pods.core
|
||||||
|
|
||||||
interface HasSinglePod {
|
interface SinglePodDevice : PodDevice {
|
||||||
|
|
||||||
val batteryHeadsetPercent: Float?
|
val batteryHeadsetPercent: Float?
|
||||||
}
|
}
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
package eu.darken.capod.pods.core.apple
|
package eu.darken.capod.pods.core.apple
|
||||||
|
|
||||||
import eu.darken.capod.common.bluetooth.BleScanResult
|
import eu.darken.capod.common.bluetooth.BleScanResult
|
||||||
import eu.darken.capod.common.debug.Bugs
|
|
||||||
import eu.darken.capod.common.debug.logging.Logging.Priority.WARN
|
import eu.darken.capod.common.debug.logging.Logging.Priority.WARN
|
||||||
import eu.darken.capod.common.debug.logging.asLog
|
import eu.darken.capod.common.debug.logging.asLog
|
||||||
import eu.darken.capod.common.debug.logging.log
|
import eu.darken.capod.common.debug.logging.log
|
||||||
import eu.darken.capod.common.debug.logging.logTag
|
import eu.darken.capod.common.debug.logging.logTag
|
||||||
import eu.darken.capod.pods.core.PodDevice
|
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.ContinuityProtocol
|
||||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
@@ -54,25 +54,13 @@ class AppleFactory @Inject constructor(
|
|||||||
|
|
||||||
val factory = podFactories.firstOrNull { it.isResponsible(pm) }
|
val factory = podFactories.firstOrNull { it.isResponsible(pm) }
|
||||||
|
|
||||||
val device = (factory ?: unknownAppleFactory).create(
|
return@withLock (factory ?: unknownAppleFactory).create(
|
||||||
scanResult = scanResult,
|
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 {
|
companion object {
|
||||||
private val SILENCED_PMS = mutableSetOf<UShort>()
|
|
||||||
private val TAG = logTag("Pod", "Apple", "Factory")
|
private val TAG = logTag("Pod", "Apple", "Factory")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -7,6 +7,8 @@ import dagger.hilt.components.SingletonComponent
|
|||||||
import dagger.multibindings.IntoSet
|
import dagger.multibindings.IntoSet
|
||||||
import eu.darken.capod.pods.core.apple.airpods.*
|
import eu.darken.capod.pods.core.apple.airpods.*
|
||||||
import eu.darken.capod.pods.core.apple.beats.*
|
import eu.darken.capod.pods.core.apple.beats.*
|
||||||
|
import eu.darken.capod.pods.core.apple.misc.Twsi99999
|
||||||
|
import eu.darken.capod.pods.core.apple.misc.VarunrAirPodsPro
|
||||||
|
|
||||||
@InstallIn(SingletonComponent::class)
|
@InstallIn(SingletonComponent::class)
|
||||||
@Module
|
@Module
|
||||||
@@ -25,4 +27,8 @@ abstract class AppleFactoryModule {
|
|||||||
@Binds @IntoSet abstract fun beatsX(factory: BeatsX.Factory): ApplePodsFactory<out ApplePods>
|
@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 powerBeats3(factory: PowerBeats3.Factory): ApplePodsFactory<out ApplePods>
|
||||||
@Binds @IntoSet abstract fun powerBeatsPro(factory: PowerBeatsPro.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>
|
||||||
|
@Binds @IntoSet
|
||||||
|
abstract fun fakesVarunrAirPodsPro(factory: VarunrAirPodsPro.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.debug.logging.log
|
||||||
import eu.darken.capod.common.lowerNibble
|
import eu.darken.capod.common.lowerNibble
|
||||||
import eu.darken.capod.common.upperNibble
|
import eu.darken.capod.common.upperNibble
|
||||||
|
import eu.darken.capod.pods.core.HasCase
|
||||||
import eu.darken.capod.pods.core.PodDevice
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
|
import eu.darken.capod.pods.core.apple.airpods.AirPodsPro
|
||||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
@@ -21,9 +23,9 @@ abstract class ApplePodsFactory<PodType : ApplePods>(private val tag: String) {
|
|||||||
val deviceColor: UByte,
|
val deviceColor: UByte,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun ProximityPairing.Message.getApplePodsMarkings(): Markings = Markings(
|
private fun ProximityPairing.Message.getApplePodsMarkings(): Markings = Markings(
|
||||||
vendor = ProximityPairing.CONTINUITY_PROTOCOL_MESSAGE_TYPE_PROXIMITY_PAIRING,
|
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(),
|
device = (((data[1].toInt() and 255) shl 8) or (data[2].toInt() and 255)).toUShort(),
|
||||||
// Make comparison order independent
|
// Make comparison order independent
|
||||||
podBatteryData = setOf(data[4].upperNibble, data[4].lowerNibble),
|
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>()
|
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? {
|
internal open fun searchHistory(current: PodType): KnownDevice? {
|
||||||
val scanResult = current.scanResult
|
val scanResult = current.scanResult
|
||||||
val message = current.proximityMessage
|
val message = current.proximityMessage
|
||||||
@@ -138,10 +156,10 @@ abstract class ApplePodsFactory<PodType : ApplePods>(private val tag: String) {
|
|||||||
dirty = data[1]
|
dirty = data[1]
|
||||||
)
|
)
|
||||||
|
|
||||||
abstract fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean
|
abstract fun isResponsible(message: ProximityPairing.Message): Boolean
|
||||||
|
|
||||||
abstract fun create(
|
abstract fun create(
|
||||||
scanResult: BleScanResult,
|
scanResult: BleScanResult,
|
||||||
proximityMessage: ProximityPairing.Message,
|
message: ProximityPairing.Message,
|
||||||
): ApplePods
|
): 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)
|
|
||||||
+12
-36
@@ -7,12 +7,11 @@ import eu.darken.capod.common.debug.logging.log
|
|||||||
import eu.darken.capod.common.isBitSet
|
import eu.darken.capod.common.isBitSet
|
||||||
import eu.darken.capod.common.lowerNibble
|
import eu.darken.capod.common.lowerNibble
|
||||||
import eu.darken.capod.common.upperNibble
|
import eu.darken.capod.common.upperNibble
|
||||||
import eu.darken.capod.pods.core.HasCase
|
import eu.darken.capod.pods.core.*
|
||||||
import eu.darken.capod.pods.core.HasDualEarDetection
|
import eu.darken.capod.pods.core.DualPodDevice.Pod
|
||||||
import eu.darken.capod.pods.core.HasDualPods
|
|
||||||
import eu.darken.capod.pods.core.HasDualPods.Pod
|
|
||||||
|
|
||||||
interface DualApplePods : ApplePods, HasDualPods, HasDualEarDetection, HasCase {
|
interface DualAirPods : ApplePods, HasChargeDetectionDual, DualPodDevice, HasEarDetectionDual, HasCase,
|
||||||
|
HasStateDetection, HasDualMicrophone, HasAppleColor {
|
||||||
|
|
||||||
val primaryPod: Pod
|
val primaryPod: Pod
|
||||||
get() = when (rawStatus.isBitSet(5)) {
|
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.
|
* 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.
|
* 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
|
get() = rawStatus.isBitSet(5) xor isThisPodInThecase
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The data flip bit is UNset if the right pod is primary.
|
* 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.
|
* 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
|
get() = !rawStatus.isBitSet(5) xor isThisPodInThecase
|
||||||
|
|
||||||
val isLeftPodCharging: Boolean
|
override val isLeftPodCharging: Boolean
|
||||||
get() = when (areValuesFlipped) {
|
get() = when (areValuesFlipped) {
|
||||||
false -> rawFlags.isBitSet(0)
|
false -> rawFlags.isBitSet(0)
|
||||||
true -> rawFlags.isBitSet(1)
|
true -> rawFlags.isBitSet(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
val isRightPodCharging: Boolean
|
override val isRightPodCharging: Boolean
|
||||||
get() = when (areValuesFlipped) {
|
get() = when (areValuesFlipped) {
|
||||||
false -> rawFlags.isBitSet(1)
|
false -> rawFlags.isBitSet(1)
|
||||||
true -> rawFlags.isBitSet(0)
|
true -> rawFlags.isBitSet(0)
|
||||||
@@ -143,35 +142,10 @@ interface DualApplePods : ApplePods, HasDualPods, HasDualEarDetection, HasCase {
|
|||||||
UNKNOWN(0xFF..0xFF);
|
UNKNOWN(0xFF..0xFF);
|
||||||
}
|
}
|
||||||
|
|
||||||
val deviceColor: DeviceColor
|
override val state: ConnectionState
|
||||||
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
|
|
||||||
get() = ConnectionState.values().firstOrNull { rawSuffix == it.raw } ?: ConnectionState.UNKNOWN
|
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) : HasStateDetection.State {
|
||||||
|
|
||||||
enum class ConnectionState(val raw: UByte?, @StringRes val labelRes: Int) {
|
|
||||||
DISCONNECTED(0x00, R.string.pods_connection_state_disconnected_label),
|
DISCONNECTED(0x00, R.string.pods_connection_state_disconnected_label),
|
||||||
IDLE(0x04, R.string.pods_connection_state_idle_label),
|
IDLE(0x04, R.string.pods_connection_state_idle_label),
|
||||||
MUSIC(0x05, R.string.pods_connection_state_music_label),
|
MUSIC(0x05, R.string.pods_connection_state_music_label),
|
||||||
@@ -180,6 +154,8 @@ interface DualApplePods : ApplePods, HasDualPods, HasDualEarDetection, HasCase {
|
|||||||
HANGING_UP(0x09, R.string.pods_connection_state_hanging_up_label),
|
HANGING_UP(0x09, R.string.pods_connection_state_hanging_up_label),
|
||||||
UNKNOWN(null, R.string.pods_connection_state_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)
|
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.Logging.Priority.DEBUG
|
||||||
import eu.darken.capod.common.debug.logging.log
|
import eu.darken.capod.common.debug.logging.log
|
||||||
import eu.darken.capod.pods.core.PodDevice
|
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,
|
leftPodBattery = batteryLeftPodPercent,
|
||||||
rightPodBattery = batteryRightPodPercent,
|
rightPodBattery = batteryRightPodPercent,
|
||||||
microPhoneLeft = isLeftPodMicrophone,
|
microPhoneLeft = isLeftPodMicrophone,
|
||||||
microPhoneRight = isRightPodMicrophone,
|
microPhoneRight = isRightPodMicrophone,
|
||||||
chargingLeft = isLeftPodCharging,
|
chargingLeft = isLeftPodCharging,
|
||||||
chargingRight = isRightPodCharging,
|
chargingRight = isRightPodCharging,
|
||||||
color = deviceColor,
|
color = rawDeviceColor,
|
||||||
model = model
|
model = model
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -28,36 +27,21 @@ abstract class DualApplePodsFactory(private val tag: String) : ApplePodsFactory<
|
|||||||
val microPhoneRight: Boolean,
|
val microPhoneRight: Boolean,
|
||||||
val chargingLeft: Boolean,
|
val chargingLeft: Boolean,
|
||||||
val chargingRight: Boolean,
|
val chargingRight: Boolean,
|
||||||
val color: DualApplePods.DeviceColor,
|
val color: UByte,
|
||||||
val model: PodDevice.Model,
|
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()
|
val target = device.getCaseMatchMarkings()
|
||||||
|
|
||||||
return filter { known ->
|
return filter { known ->
|
||||||
known.history
|
known.history
|
||||||
.filterIsInstance<DualApplePods>()
|
.filterIsInstance<DualAirPods>()
|
||||||
.any { it.getCaseMatchMarkings() == target }
|
.any { it.getCaseMatchMarkings() == target }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun KnownDevice.getLatestCaseBattery(): Float? = history
|
override fun searchHistory(current: DualAirPods): KnownDevice? {
|
||||||
.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? {
|
|
||||||
val basicResult = super.searchHistory(current)
|
val basicResult = super.searchHistory(current)
|
||||||
|
|
||||||
val caseIgnored = knownDevices.values.findSplitPodsMatch(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
|
package eu.darken.capod.pods.core.apple
|
||||||
|
|
||||||
import eu.darken.capod.common.isBitSet
|
import eu.darken.capod.common.debug.logging.log
|
||||||
import eu.darken.capod.pods.core.HasEarDetection
|
import eu.darken.capod.common.lowerNibble
|
||||||
import eu.darken.capod.pods.core.HasSinglePod
|
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
|
override val batteryHeadsetPercent: Float?
|
||||||
get() = rawStatus.isBitSet(1)
|
get() = when (val value = rawPodsBattery.lowerNibble.toInt()) {
|
||||||
|
15 -> null
|
||||||
val isHeadsetBeingCharged: Boolean
|
else -> if (value > 10) {
|
||||||
get() = rawFlags.isBitSet(0)
|
log { "Headset above 100% battery: $value" }
|
||||||
|
1.0f
|
||||||
override val isBeingWorn: Boolean
|
} else {
|
||||||
get() = isHeadphonesBeingWorn
|
(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.common.debug.logging.logTag
|
||||||
import eu.darken.capod.pods.core.PodDevice
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
import eu.darken.capod.pods.core.apple.ApplePods
|
import eu.darken.capod.pods.core.apple.ApplePods
|
||||||
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.DualApplePodsFactory
|
||||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
@@ -20,15 +20,15 @@ data class AirPodsGen1 constructor(
|
|||||||
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
||||||
private val rssiAverage: Int? = null,
|
private val rssiAverage: Int? = null,
|
||||||
private val cachedBatteryPercentage: Float? = null,
|
private val cachedBatteryPercentage: Float? = null,
|
||||||
private val cachedCaseState: DualApplePods.LidState? = null
|
private val cachedCaseState: DualAirPods.LidState? = null
|
||||||
) : DualApplePods {
|
) : DualAirPods {
|
||||||
|
|
||||||
override val model: PodDevice.Model = PodDevice.Model.AIRPODS_GEN1
|
override val model: PodDevice.Model = PodDevice.Model.AIRPODS_GEN1
|
||||||
|
|
||||||
override val batteryCasePercent: Float?
|
override val batteryCasePercent: Float?
|
||||||
get() = super.batteryCasePercent ?: cachedBatteryPercentage
|
get() = super.batteryCasePercent ?: cachedBatteryPercentage
|
||||||
|
|
||||||
override val caseLidState: DualApplePods.LidState
|
override val caseLidState: DualAirPods.LidState
|
||||||
get() = cachedCaseState ?: super.caseLidState
|
get() = cachedCaseState ?: super.caseLidState
|
||||||
|
|
||||||
override val rssi: Int
|
override val rssi: Int
|
||||||
@@ -36,11 +36,13 @@ data class AirPodsGen1 constructor(
|
|||||||
|
|
||||||
class Factory @Inject constructor() : DualApplePodsFactory(TAG) {
|
class Factory @Inject constructor() : DualApplePodsFactory(TAG) {
|
||||||
|
|
||||||
override fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean =
|
override fun isResponsible(message: ProximityPairing.Message): Boolean = message.run {
|
||||||
proximityMessage.getModelInfo().full == DEVICE_CODE
|
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)
|
val result = searchHistory(basic)
|
||||||
|
|
||||||
if (result != null) basic = basic.copy(identifier = result.id)
|
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.common.debug.logging.logTag
|
||||||
import eu.darken.capod.pods.core.PodDevice
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
import eu.darken.capod.pods.core.apple.ApplePods
|
import eu.darken.capod.pods.core.apple.ApplePods
|
||||||
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.DualApplePodsFactory
|
||||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
@@ -20,15 +20,15 @@ data class AirPodsGen2 constructor(
|
|||||||
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
||||||
private val rssiAverage: Int? = null,
|
private val rssiAverage: Int? = null,
|
||||||
private val cachedBatteryPercentage: Float? = null,
|
private val cachedBatteryPercentage: Float? = null,
|
||||||
private val cachedCaseState: DualApplePods.LidState? = null
|
private val cachedCaseState: DualAirPods.LidState? = null
|
||||||
) : DualApplePods {
|
) : DualAirPods {
|
||||||
|
|
||||||
override val model: PodDevice.Model = PodDevice.Model.AIRPODS_GEN2
|
override val model: PodDevice.Model = PodDevice.Model.AIRPODS_GEN2
|
||||||
|
|
||||||
override val batteryCasePercent: Float?
|
override val batteryCasePercent: Float?
|
||||||
get() = super.batteryCasePercent ?: cachedBatteryPercentage
|
get() = super.batteryCasePercent ?: cachedBatteryPercentage
|
||||||
|
|
||||||
override val caseLidState: DualApplePods.LidState
|
override val caseLidState: DualAirPods.LidState
|
||||||
get() = cachedCaseState ?: super.caseLidState
|
get() = cachedCaseState ?: super.caseLidState
|
||||||
|
|
||||||
override val rssi: Int
|
override val rssi: Int
|
||||||
@@ -36,11 +36,12 @@ data class AirPodsGen2 constructor(
|
|||||||
|
|
||||||
class Factory @Inject constructor() : DualApplePodsFactory(TAG) {
|
class Factory @Inject constructor() : DualApplePodsFactory(TAG) {
|
||||||
|
|
||||||
override fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean =
|
override fun isResponsible(message: ProximityPairing.Message): Boolean = message.run {
|
||||||
proximityMessage.getModelInfo().full == DEVICE_CODE
|
getModelInfo().full == DEVICE_CODE && length == ProximityPairing.PAIRING_MESSAGE_LENGTH
|
||||||
|
}
|
||||||
|
|
||||||
override fun create(scanResult: BleScanResult, proximityMessage: ProximityPairing.Message): ApplePods {
|
override fun create(scanResult: BleScanResult, message: ProximityPairing.Message): ApplePods {
|
||||||
var basic = AirPodsGen2(scanResult = scanResult, proximityMessage = proximityMessage)
|
var basic = AirPodsGen2(scanResult = scanResult, proximityMessage = message)
|
||||||
val result = searchHistory(basic)
|
val result = searchHistory(basic)
|
||||||
|
|
||||||
if (result != null) basic = basic.copy(identifier = result.id)
|
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.common.debug.logging.logTag
|
||||||
import eu.darken.capod.pods.core.PodDevice
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
import eu.darken.capod.pods.core.apple.ApplePods
|
import eu.darken.capod.pods.core.apple.ApplePods
|
||||||
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.DualApplePodsFactory
|
||||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
@@ -20,14 +20,14 @@ data class AirPodsGen3 constructor(
|
|||||||
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
||||||
private val rssiAverage: Int? = null,
|
private val rssiAverage: Int? = null,
|
||||||
private val cachedBatteryPercentage: Float? = null,
|
private val cachedBatteryPercentage: Float? = null,
|
||||||
private val cachedCaseState: DualApplePods.LidState? = null
|
private val cachedCaseState: DualAirPods.LidState? = null
|
||||||
) : DualApplePods {
|
) : DualAirPods {
|
||||||
override val model: PodDevice.Model = PodDevice.Model.AIRPODS_GEN3
|
override val model: PodDevice.Model = PodDevice.Model.AIRPODS_GEN3
|
||||||
|
|
||||||
override val batteryCasePercent: Float?
|
override val batteryCasePercent: Float?
|
||||||
get() = super.batteryCasePercent ?: cachedBatteryPercentage
|
get() = super.batteryCasePercent ?: cachedBatteryPercentage
|
||||||
|
|
||||||
override val caseLidState: DualApplePods.LidState
|
override val caseLidState: DualAirPods.LidState
|
||||||
get() = cachedCaseState ?: super.caseLidState
|
get() = cachedCaseState ?: super.caseLidState
|
||||||
|
|
||||||
override val rssi: Int
|
override val rssi: Int
|
||||||
@@ -35,11 +35,12 @@ data class AirPodsGen3 constructor(
|
|||||||
|
|
||||||
class Factory @Inject constructor() : DualApplePodsFactory(TAG) {
|
class Factory @Inject constructor() : DualApplePodsFactory(TAG) {
|
||||||
|
|
||||||
override fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean =
|
override fun isResponsible(message: ProximityPairing.Message): Boolean = message.run {
|
||||||
proximityMessage.getModelInfo().full == DEVICE_CODE
|
getModelInfo().full == DEVICE_CODE && length == ProximityPairing.PAIRING_MESSAGE_LENGTH
|
||||||
|
}
|
||||||
|
|
||||||
override fun create(scanResult: BleScanResult, proximityMessage: ProximityPairing.Message): ApplePods {
|
override fun create(scanResult: BleScanResult, message: ProximityPairing.Message): ApplePods {
|
||||||
var basic = AirPodsGen3(scanResult = scanResult, proximityMessage = proximityMessage)
|
var basic = AirPodsGen3(scanResult = scanResult, proximityMessage = message)
|
||||||
val result = searchHistory(basic)
|
val result = searchHistory(basic)
|
||||||
|
|
||||||
if (result != null) basic = basic.copy(identifier = result.id)
|
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.bluetooth.BleScanResult
|
||||||
import eu.darken.capod.common.debug.logging.logTag
|
import eu.darken.capod.common.debug.logging.logTag
|
||||||
|
import eu.darken.capod.common.isBitSet
|
||||||
|
import eu.darken.capod.pods.core.HasEarDetection
|
||||||
import eu.darken.capod.pods.core.PodDevice
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
import eu.darken.capod.pods.core.apple.ApplePods
|
import eu.darken.capod.pods.core.apple.ApplePods
|
||||||
import eu.darken.capod.pods.core.apple.SingleApplePods
|
import eu.darken.capod.pods.core.apple.SingleApplePods
|
||||||
@@ -19,20 +21,30 @@ data class AirPodsMax(
|
|||||||
override val proximityMessage: ProximityPairing.Message,
|
override val proximityMessage: ProximityPairing.Message,
|
||||||
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
||||||
private val rssiAverage: Int? = null,
|
private val rssiAverage: Int? = null,
|
||||||
) : SingleApplePods {
|
) : SingleApplePods, HasEarDetection {
|
||||||
|
|
||||||
override val model: PodDevice.Model = PodDevice.Model.AIRPODS_MAX
|
override val model: PodDevice.Model = PodDevice.Model.AIRPODS_MAX
|
||||||
|
|
||||||
override val rssi: Int
|
override val rssi: Int
|
||||||
get() = rssiAverage ?: super.rssi
|
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) {
|
class Factory @Inject constructor() : SingleApplePodsFactory(TAG) {
|
||||||
|
|
||||||
override fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean =
|
override fun isResponsible(message: ProximityPairing.Message): Boolean = message.run {
|
||||||
proximityMessage.getModelInfo().dirty == DEVICE_CODE_DIRTY
|
getModelInfo().dirty == DEVICE_CODE_DIRTY && length == ProximityPairing.PAIRING_MESSAGE_LENGTH
|
||||||
|
}
|
||||||
|
|
||||||
override fun create(scanResult: BleScanResult, proximityMessage: ProximityPairing.Message): ApplePods {
|
override fun create(scanResult: BleScanResult, message: ProximityPairing.Message): ApplePods {
|
||||||
var basic = AirPodsMax(scanResult = scanResult, proximityMessage = proximityMessage)
|
var basic = AirPodsMax(scanResult = scanResult, proximityMessage = message)
|
||||||
val result = searchHistory(basic)
|
val result = searchHistory(basic)
|
||||||
|
|
||||||
if (result != null) basic = basic.copy(identifier = result.id)
|
if (result != null) basic = basic.copy(identifier = result.id)
|
||||||
@@ -48,7 +60,6 @@ data class AirPodsMax(
|
|||||||
rssiAverage = result.averageRssi(basic.rssi),
|
rssiAverage = result.averageRssi(basic.rssi),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
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.common.debug.logging.logTag
|
||||||
import eu.darken.capod.pods.core.PodDevice
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
import eu.darken.capod.pods.core.apple.ApplePods
|
import eu.darken.capod.pods.core.apple.ApplePods
|
||||||
import eu.darken.capod.pods.core.apple.DualApplePods
|
import eu.darken.capod.pods.core.apple.DualAirPods
|
||||||
import eu.darken.capod.pods.core.apple.DualApplePods.LidState
|
import eu.darken.capod.pods.core.apple.DualAirPods.LidState
|
||||||
import eu.darken.capod.pods.core.apple.DualApplePodsFactory
|
import eu.darken.capod.pods.core.apple.DualApplePodsFactory
|
||||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
@@ -22,7 +22,7 @@ data class AirPodsPro(
|
|||||||
private val rssiAverage: Int? = null,
|
private val rssiAverage: Int? = null,
|
||||||
private val cachedBatteryPercentage: Float? = null,
|
private val cachedBatteryPercentage: Float? = null,
|
||||||
private val cachedCaseState: LidState? = null
|
private val cachedCaseState: LidState? = null
|
||||||
) : DualApplePods {
|
) : DualAirPods {
|
||||||
|
|
||||||
override val model: PodDevice.Model = PodDevice.Model.AIRPODS_PRO
|
override val model: PodDevice.Model = PodDevice.Model.AIRPODS_PRO
|
||||||
|
|
||||||
@@ -37,11 +37,12 @@ data class AirPodsPro(
|
|||||||
|
|
||||||
class Factory @Inject constructor() : DualApplePodsFactory(TAG) {
|
class Factory @Inject constructor() : DualApplePodsFactory(TAG) {
|
||||||
|
|
||||||
override fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean =
|
override fun isResponsible(message: ProximityPairing.Message): Boolean = message.run {
|
||||||
proximityMessage.getModelInfo().full == DEVICE_CODE
|
getModelInfo().full == DEVICE_CODE && length == ProximityPairing.PAIRING_MESSAGE_LENGTH
|
||||||
|
}
|
||||||
|
|
||||||
override fun create(scanResult: BleScanResult, proximityMessage: ProximityPairing.Message): ApplePods {
|
override fun create(scanResult: BleScanResult, message: ProximityPairing.Message): ApplePods {
|
||||||
var basic = AirPodsPro(scanResult = scanResult, proximityMessage = proximityMessage)
|
var basic = AirPodsPro(scanResult = scanResult, proximityMessage = message)
|
||||||
val result = searchHistory(basic)
|
val result = searchHistory(basic)
|
||||||
|
|
||||||
if (result != null) basic = basic.copy(identifier = result.id)
|
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.common.debug.logging.logTag
|
||||||
import eu.darken.capod.pods.core.PodDevice
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
import eu.darken.capod.pods.core.apple.ApplePods
|
import eu.darken.capod.pods.core.apple.ApplePods
|
||||||
import eu.darken.capod.pods.core.apple.BasicSingleApplePods
|
import eu.darken.capod.pods.core.apple.SingleApplePods
|
||||||
import eu.darken.capod.pods.core.apple.BasicSingleApplePodsFactory
|
import eu.darken.capod.pods.core.apple.SingleApplePodsFactory
|
||||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@@ -19,20 +19,21 @@ data class BeatsFlex(
|
|||||||
override val proximityMessage: ProximityPairing.Message,
|
override val proximityMessage: ProximityPairing.Message,
|
||||||
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
||||||
private val rssiAverage: Int? = null,
|
private val rssiAverage: Int? = null,
|
||||||
) : BasicSingleApplePods {
|
) : SingleApplePods {
|
||||||
|
|
||||||
override val model: PodDevice.Model = PodDevice.Model.BEATS_FLEX
|
override val model: PodDevice.Model = PodDevice.Model.BEATS_FLEX
|
||||||
|
|
||||||
override val rssi: Int
|
override val rssi: Int
|
||||||
get() = rssiAverage ?: super.rssi
|
get() = rssiAverage ?: super.rssi
|
||||||
|
|
||||||
class Factory @Inject constructor() : BasicSingleApplePodsFactory(TAG) {
|
class Factory @Inject constructor() : SingleApplePodsFactory(TAG) {
|
||||||
|
|
||||||
override fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean =
|
override fun isResponsible(message: ProximityPairing.Message): Boolean = message.run {
|
||||||
proximityMessage.getModelInfo().full == DEVICE_CODE
|
getModelInfo().full == DEVICE_CODE && length == ProximityPairing.PAIRING_MESSAGE_LENGTH
|
||||||
|
}
|
||||||
|
|
||||||
override fun create(scanResult: BleScanResult, proximityMessage: ProximityPairing.Message): ApplePods {
|
override fun create(scanResult: BleScanResult, message: ProximityPairing.Message): ApplePods {
|
||||||
var basic = BeatsFlex(scanResult = scanResult, proximityMessage = proximityMessage)
|
var basic = BeatsFlex(scanResult = scanResult, proximityMessage = message)
|
||||||
val result = searchHistory(basic)
|
val result = searchHistory(basic)
|
||||||
|
|
||||||
if (result != null) basic = basic.copy(identifier = result.id)
|
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.common.debug.logging.logTag
|
||||||
import eu.darken.capod.pods.core.PodDevice
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
import eu.darken.capod.pods.core.apple.ApplePods
|
import eu.darken.capod.pods.core.apple.ApplePods
|
||||||
import eu.darken.capod.pods.core.apple.BasicSingleApplePods
|
import eu.darken.capod.pods.core.apple.SingleApplePods
|
||||||
import eu.darken.capod.pods.core.apple.BasicSingleApplePodsFactory
|
import eu.darken.capod.pods.core.apple.SingleApplePodsFactory
|
||||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@@ -19,19 +19,20 @@ data class BeatsSolo3(
|
|||||||
override val proximityMessage: ProximityPairing.Message,
|
override val proximityMessage: ProximityPairing.Message,
|
||||||
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
||||||
private val rssiAverage: Int? = null,
|
private val rssiAverage: Int? = null,
|
||||||
) : BasicSingleApplePods {
|
) : SingleApplePods {
|
||||||
|
|
||||||
override val model: PodDevice.Model = PodDevice.Model.BEATS_SOLO_3
|
override val model: PodDevice.Model = PodDevice.Model.BEATS_SOLO_3
|
||||||
override val rssi: Int
|
override val rssi: Int
|
||||||
get() = rssiAverage ?: super.rssi
|
get() = rssiAverage ?: super.rssi
|
||||||
|
|
||||||
class Factory @Inject constructor() : BasicSingleApplePodsFactory(TAG) {
|
class Factory @Inject constructor() : SingleApplePodsFactory(TAG) {
|
||||||
|
|
||||||
override fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean =
|
override fun isResponsible(message: ProximityPairing.Message): Boolean = message.run {
|
||||||
proximityMessage.getModelInfo().full == DEVICE_CODE
|
getModelInfo().full == DEVICE_CODE && length == ProximityPairing.PAIRING_MESSAGE_LENGTH
|
||||||
|
}
|
||||||
|
|
||||||
override fun create(scanResult: BleScanResult, proximityMessage: ProximityPairing.Message): ApplePods {
|
override fun create(scanResult: BleScanResult, message: ProximityPairing.Message): ApplePods {
|
||||||
var basic = BeatsSolo3(scanResult = scanResult, proximityMessage = proximityMessage)
|
var basic = BeatsSolo3(scanResult = scanResult, proximityMessage = message)
|
||||||
val result = searchHistory(basic)
|
val result = searchHistory(basic)
|
||||||
|
|
||||||
if (result != null) basic = basic.copy(identifier = result.id)
|
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.common.debug.logging.logTag
|
||||||
import eu.darken.capod.pods.core.PodDevice
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
import eu.darken.capod.pods.core.apple.ApplePods
|
import eu.darken.capod.pods.core.apple.ApplePods
|
||||||
import eu.darken.capod.pods.core.apple.BasicSingleApplePods
|
import eu.darken.capod.pods.core.apple.SingleApplePods
|
||||||
import eu.darken.capod.pods.core.apple.BasicSingleApplePodsFactory
|
import eu.darken.capod.pods.core.apple.SingleApplePodsFactory
|
||||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@@ -19,17 +19,18 @@ data class BeatsStudio3(
|
|||||||
override val proximityMessage: ProximityPairing.Message,
|
override val proximityMessage: ProximityPairing.Message,
|
||||||
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
||||||
private val rssiAverage: Int? = null,
|
private val rssiAverage: Int? = null,
|
||||||
) : BasicSingleApplePods {
|
) : SingleApplePods {
|
||||||
|
|
||||||
override val model: PodDevice.Model = PodDevice.Model.BEATS_STUDIO_3
|
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 =
|
override fun isResponsible(message: ProximityPairing.Message): Boolean = message.run {
|
||||||
proximityMessage.getModelInfo().dirty == DEVICE_CODE_DIRTY
|
getModelInfo().dirty == DEVICE_CODE_DIRTY && length == ProximityPairing.PAIRING_MESSAGE_LENGTH
|
||||||
|
}
|
||||||
|
|
||||||
override fun create(scanResult: BleScanResult, proximityMessage: ProximityPairing.Message): ApplePods {
|
override fun create(scanResult: BleScanResult, message: ProximityPairing.Message): ApplePods {
|
||||||
var basic = BeatsStudio3(scanResult = scanResult, proximityMessage = proximityMessage)
|
var basic = BeatsStudio3(scanResult = scanResult, proximityMessage = message)
|
||||||
val result = searchHistory(basic)
|
val result = searchHistory(basic)
|
||||||
|
|
||||||
if (result != null) basic = basic.copy(identifier = result.id)
|
if (result != null) basic = basic.copy(identifier = result.id)
|
||||||
@@ -45,7 +46,6 @@ data class BeatsStudio3(
|
|||||||
rssiAverage = result.averageRssi(basic.rssi),
|
rssiAverage = result.averageRssi(basic.rssi),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
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.common.debug.logging.logTag
|
||||||
import eu.darken.capod.pods.core.PodDevice
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
import eu.darken.capod.pods.core.apple.ApplePods
|
import eu.darken.capod.pods.core.apple.ApplePods
|
||||||
import eu.darken.capod.pods.core.apple.BasicSingleApplePods
|
import eu.darken.capod.pods.core.apple.SingleApplePods
|
||||||
import eu.darken.capod.pods.core.apple.BasicSingleApplePodsFactory
|
import eu.darken.capod.pods.core.apple.SingleApplePodsFactory
|
||||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@@ -19,20 +19,21 @@ data class BeatsX(
|
|||||||
override val proximityMessage: ProximityPairing.Message,
|
override val proximityMessage: ProximityPairing.Message,
|
||||||
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
||||||
private val rssiAverage: Int? = null,
|
private val rssiAverage: Int? = null,
|
||||||
) : BasicSingleApplePods {
|
) : SingleApplePods {
|
||||||
|
|
||||||
override val model: PodDevice.Model = PodDevice.Model.BEATS_X
|
override val model: PodDevice.Model = PodDevice.Model.BEATS_X
|
||||||
|
|
||||||
override val rssi: Int
|
override val rssi: Int
|
||||||
get() = rssiAverage ?: super.rssi
|
get() = rssiAverage ?: super.rssi
|
||||||
|
|
||||||
class Factory @Inject constructor() : BasicSingleApplePodsFactory(TAG) {
|
class Factory @Inject constructor() : SingleApplePodsFactory(TAG) {
|
||||||
|
|
||||||
override fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean =
|
override fun isResponsible(message: ProximityPairing.Message): Boolean = message.run {
|
||||||
proximityMessage.getModelInfo().full == DEVICE_CODE
|
getModelInfo().full == DEVICE_CODE && length == ProximityPairing.PAIRING_MESSAGE_LENGTH
|
||||||
|
}
|
||||||
|
|
||||||
override fun create(scanResult: BleScanResult, proximityMessage: ProximityPairing.Message): ApplePods {
|
override fun create(scanResult: BleScanResult, message: ProximityPairing.Message): ApplePods {
|
||||||
var basic = BeatsX(scanResult = scanResult, proximityMessage = proximityMessage)
|
var basic = BeatsX(scanResult = scanResult, proximityMessage = message)
|
||||||
val result = searchHistory(basic)
|
val result = searchHistory(basic)
|
||||||
|
|
||||||
if (result != null) basic = basic.copy(identifier = result.id)
|
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.common.debug.logging.logTag
|
||||||
import eu.darken.capod.pods.core.PodDevice
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
import eu.darken.capod.pods.core.apple.ApplePods
|
import eu.darken.capod.pods.core.apple.ApplePods
|
||||||
import eu.darken.capod.pods.core.apple.BasicSingleApplePods
|
import eu.darken.capod.pods.core.apple.SingleApplePods
|
||||||
import eu.darken.capod.pods.core.apple.BasicSingleApplePodsFactory
|
import eu.darken.capod.pods.core.apple.SingleApplePodsFactory
|
||||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@@ -19,19 +19,21 @@ data class PowerBeats3(
|
|||||||
override val proximityMessage: ProximityPairing.Message,
|
override val proximityMessage: ProximityPairing.Message,
|
||||||
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
||||||
private val rssiAverage: Int? = null,
|
private val rssiAverage: Int? = null,
|
||||||
) : BasicSingleApplePods {
|
) : SingleApplePods {
|
||||||
|
|
||||||
override val model: PodDevice.Model = PodDevice.Model.POWERBEATS_3
|
override val model: PodDevice.Model = PodDevice.Model.POWERBEATS_3
|
||||||
|
|
||||||
override val rssi: Int
|
override val rssi: Int
|
||||||
get() = rssiAverage ?: super.rssi
|
get() = rssiAverage ?: super.rssi
|
||||||
|
|
||||||
class Factory @Inject constructor() : BasicSingleApplePodsFactory(TAG) {
|
class Factory @Inject constructor() : SingleApplePodsFactory(TAG) {
|
||||||
|
|
||||||
override fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean =
|
override fun isResponsible(message: ProximityPairing.Message): Boolean = message.run {
|
||||||
proximityMessage.getModelInfo().full == DEVICE_CODE
|
getModelInfo().full == DEVICE_CODE && length == ProximityPairing.PAIRING_MESSAGE_LENGTH
|
||||||
|
}
|
||||||
|
|
||||||
override fun create(scanResult: BleScanResult, proximityMessage: ProximityPairing.Message): ApplePods {
|
override fun create(scanResult: BleScanResult, message: ProximityPairing.Message): ApplePods {
|
||||||
var basic = PowerBeats3(scanResult = scanResult, proximityMessage = proximityMessage)
|
var basic = PowerBeats3(scanResult = scanResult, proximityMessage = message)
|
||||||
val result = searchHistory(basic)
|
val result = searchHistory(basic)
|
||||||
|
|
||||||
if (result != null) basic = basic.copy(identifier = result.id)
|
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.common.debug.logging.logTag
|
||||||
import eu.darken.capod.pods.core.PodDevice
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
import eu.darken.capod.pods.core.apple.ApplePods
|
import eu.darken.capod.pods.core.apple.ApplePods
|
||||||
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.DualApplePodsFactory
|
||||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
@@ -20,15 +20,15 @@ data class PowerBeatsPro(
|
|||||||
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
||||||
private val rssiAverage: Int? = null,
|
private val rssiAverage: Int? = null,
|
||||||
private val cachedBatteryPercentage: Float? = null,
|
private val cachedBatteryPercentage: Float? = null,
|
||||||
private val cachedCaseState: DualApplePods.LidState? = null
|
private val cachedCaseState: DualAirPods.LidState? = null
|
||||||
) : DualApplePods {
|
) : DualAirPods {
|
||||||
|
|
||||||
override val model: PodDevice.Model = PodDevice.Model.POWERBEATS_PRO
|
override val model: PodDevice.Model = PodDevice.Model.POWERBEATS_PRO
|
||||||
|
|
||||||
override val batteryCasePercent: Float?
|
override val batteryCasePercent: Float?
|
||||||
get() = super.batteryCasePercent ?: cachedBatteryPercentage
|
get() = super.batteryCasePercent ?: cachedBatteryPercentage
|
||||||
|
|
||||||
override val caseLidState: DualApplePods.LidState
|
override val caseLidState: DualAirPods.LidState
|
||||||
get() = cachedCaseState ?: super.caseLidState
|
get() = cachedCaseState ?: super.caseLidState
|
||||||
|
|
||||||
override val rssi: Int
|
override val rssi: Int
|
||||||
@@ -36,11 +36,12 @@ data class PowerBeatsPro(
|
|||||||
|
|
||||||
class Factory @Inject constructor() : DualApplePodsFactory(TAG) {
|
class Factory @Inject constructor() : DualApplePodsFactory(TAG) {
|
||||||
|
|
||||||
override fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean =
|
override fun isResponsible(message: ProximityPairing.Message): Boolean = message.run {
|
||||||
proximityMessage.getModelInfo().dirty == DEVICE_CODE_DIRTY
|
getModelInfo().dirty == DEVICE_CODE_DIRTY && length == ProximityPairing.PAIRING_MESSAGE_LENGTH
|
||||||
|
}
|
||||||
|
|
||||||
override fun create(scanResult: BleScanResult, proximityMessage: ProximityPairing.Message): ApplePods {
|
override fun create(scanResult: BleScanResult, message: ProximityPairing.Message): ApplePods {
|
||||||
var basic = PowerBeatsPro(scanResult = scanResult, proximityMessage = proximityMessage)
|
var basic = PowerBeatsPro(scanResult = scanResult, proximityMessage = message)
|
||||||
val result = searchHistory(basic)
|
val result = searchHistory(basic)
|
||||||
|
|
||||||
if (result != null) basic = basic.copy(identifier = result.id)
|
if (result != null) basic = basic.copy(identifier = result.id)
|
||||||
|
|||||||
@@ -0,0 +1,143 @@
|
|||||||
|
package eu.darken.capod.pods.core.apple.misc
|
||||||
|
|
||||||
|
import eu.darken.capod.common.bluetooth.BleScanResult
|
||||||
|
import eu.darken.capod.common.debug.logging.log
|
||||||
|
import eu.darken.capod.common.debug.logging.logTag
|
||||||
|
import eu.darken.capod.common.isBitSet
|
||||||
|
import eu.darken.capod.common.lowerNibble
|
||||||
|
import eu.darken.capod.common.upperNibble
|
||||||
|
import eu.darken.capod.pods.core.DualPodDevice
|
||||||
|
import eu.darken.capod.pods.core.HasCase
|
||||||
|
import eu.darken.capod.pods.core.HasDualMicrophone
|
||||||
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
|
import eu.darken.capod.pods.core.apple.ApplePods
|
||||||
|
import eu.darken.capod.pods.core.apple.ApplePodsFactory
|
||||||
|
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||||
|
import java.time.Instant
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Basically an AirPods GEN1 clone
|
||||||
|
* Similar data structure but a lot of placeholder values or hardcoded values
|
||||||
|
*/
|
||||||
|
data class Twsi99999 constructor(
|
||||||
|
override val identifier: PodDevice.Id = PodDevice.Id(),
|
||||||
|
override val seenLastAt: Instant = Instant.now(),
|
||||||
|
override val seenFirstAt: Instant = Instant.now(),
|
||||||
|
override val seenCounter: Int = 1,
|
||||||
|
override val scanResult: BleScanResult,
|
||||||
|
override val proximityMessage: ProximityPairing.Message,
|
||||||
|
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
||||||
|
private val rssiAverage: Int? = null,
|
||||||
|
private val cachedBatteryPercentage: Float? = null,
|
||||||
|
) : ApplePods, DualPodDevice, HasDualMicrophone, HasCase {
|
||||||
|
|
||||||
|
override val model: PodDevice.Model = PodDevice.Model.TWS_I99999
|
||||||
|
|
||||||
|
override val rssi: Int
|
||||||
|
get() = rssiAverage ?: super<ApplePods>.rssi
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normally values for the left pod are in the lower nibbles, if the left pod is primary (microphone)
|
||||||
|
* If the right pod is the primary, the values are flipped.
|
||||||
|
*/
|
||||||
|
val areValuesFlipped: Boolean
|
||||||
|
get() = !rawStatus.isBitSet(5)
|
||||||
|
|
||||||
|
override val batteryLeftPodPercent: Float?
|
||||||
|
get() {
|
||||||
|
val value = when (areValuesFlipped) {
|
||||||
|
true -> rawPodsBattery.upperNibble.toInt()
|
||||||
|
false -> rawPodsBattery.lowerNibble.toInt()
|
||||||
|
}
|
||||||
|
return when (value) {
|
||||||
|
15 -> null
|
||||||
|
else -> if (value > 10) {
|
||||||
|
log { "Left pod: Above 100% battery: $value" }
|
||||||
|
1.0f
|
||||||
|
} else {
|
||||||
|
(value / 10f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override val batteryRightPodPercent: Float?
|
||||||
|
get() {
|
||||||
|
val value = when (areValuesFlipped) {
|
||||||
|
true -> rawPodsBattery.lowerNibble.toInt()
|
||||||
|
false -> rawPodsBattery.upperNibble.toInt()
|
||||||
|
}
|
||||||
|
return when (value) {
|
||||||
|
15 -> null
|
||||||
|
else -> if (value > 10) {
|
||||||
|
log { "Right pod: Above 100% battery: $value" }
|
||||||
|
1.0f
|
||||||
|
} else {
|
||||||
|
value / 10f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val isThisPodInThecase: Boolean
|
||||||
|
get() = rawStatus.isBitSet(6)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
override val isRightPodMicrophone: Boolean
|
||||||
|
get() = !rawStatus.isBitSet(5) xor isThisPodInThecase
|
||||||
|
|
||||||
|
override val batteryCasePercent: Float?
|
||||||
|
get() = when (val value = rawCaseBattery.toInt()) {
|
||||||
|
15 -> cachedBatteryPercentage
|
||||||
|
else -> if (value > 10) {
|
||||||
|
log { "Case: Above 100% battery: $value" }
|
||||||
|
1.0f
|
||||||
|
} else {
|
||||||
|
value / 10f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override val isCaseCharging: Boolean
|
||||||
|
get() = rawFlags.isBitSet(2)
|
||||||
|
|
||||||
|
class Factory @Inject constructor() : ApplePodsFactory<Twsi99999>(TAG) {
|
||||||
|
|
||||||
|
override fun isResponsible(message: ProximityPairing.Message): Boolean = message.run {
|
||||||
|
// Official message length is 19HEX, i.e. binary 25, did they copy this wrong?
|
||||||
|
getModelInfo().full == DEVICE_CODE && length == 19
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun create(scanResult: BleScanResult, message: ProximityPairing.Message): ApplePods {
|
||||||
|
var basic = Twsi99999(scanResult = scanResult, proximityMessage = message)
|
||||||
|
val result = searchHistory(basic)
|
||||||
|
|
||||||
|
if (result != null) basic = basic.copy(identifier = result.id)
|
||||||
|
updateHistory(basic)
|
||||||
|
|
||||||
|
if (result == null) return basic
|
||||||
|
|
||||||
|
return basic.copy(
|
||||||
|
identifier = result.id,
|
||||||
|
seenFirstAt = result.seenFirstAt,
|
||||||
|
seenCounter = result.seenCounter,
|
||||||
|
confidence = result.confidence,
|
||||||
|
cachedBatteryPercentage = result.getLatestCaseBattery(),
|
||||||
|
rssiAverage = result.averageRssi(basic.rssi),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val DEVICE_CODE = 0x0220.toUShort()
|
||||||
|
private val TAG = logTag("PodDevice", "Apple", "TWS", "i99999")
|
||||||
|
}
|
||||||
|
}
|
||||||
+6
-4
@@ -1,10 +1,12 @@
|
|||||||
package eu.darken.capod.pods.core.apple
|
package eu.darken.capod.pods.core.apple.misc
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import eu.darken.capod.R
|
import eu.darken.capod.R
|
||||||
import eu.darken.capod.common.bluetooth.BleScanResult
|
import eu.darken.capod.common.bluetooth.BleScanResult
|
||||||
import eu.darken.capod.common.debug.logging.logTag
|
import eu.darken.capod.common.debug.logging.logTag
|
||||||
import eu.darken.capod.pods.core.PodDevice
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
|
import eu.darken.capod.pods.core.apple.ApplePods
|
||||||
|
import eu.darken.capod.pods.core.apple.ApplePodsFactory
|
||||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@@ -28,13 +30,13 @@ data class UnknownAppleDevice(
|
|||||||
get() = rssiAverage ?: super.rssi
|
get() = rssiAverage ?: super.rssi
|
||||||
|
|
||||||
class Factory @Inject constructor() : ApplePodsFactory<ApplePods>(TAG) {
|
class Factory @Inject constructor() : ApplePodsFactory<ApplePods>(TAG) {
|
||||||
override fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean = true
|
override fun isResponsible(message: ProximityPairing.Message): Boolean = true
|
||||||
|
|
||||||
override fun create(
|
override fun create(
|
||||||
scanResult: BleScanResult,
|
scanResult: BleScanResult,
|
||||||
proximityMessage: ProximityPairing.Message,
|
message: ProximityPairing.Message,
|
||||||
): ApplePods {
|
): ApplePods {
|
||||||
var basic = UnknownAppleDevice(scanResult = scanResult, proximityMessage = proximityMessage)
|
var basic = UnknownAppleDevice(scanResult = scanResult, proximityMessage = message)
|
||||||
val result = searchHistory(basic)
|
val result = searchHistory(basic)
|
||||||
|
|
||||||
if (result != null) basic = basic.copy(identifier = result.id)
|
if (result != null) basic = basic.copy(identifier = result.id)
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
package eu.darken.capod.pods.core.apple.misc
|
||||||
|
|
||||||
|
import eu.darken.capod.common.bluetooth.BleScanResult
|
||||||
|
import eu.darken.capod.common.debug.logging.log
|
||||||
|
import eu.darken.capod.common.debug.logging.logTag
|
||||||
|
import eu.darken.capod.common.isBitSet
|
||||||
|
import eu.darken.capod.common.lowerNibble
|
||||||
|
import eu.darken.capod.common.upperNibble
|
||||||
|
import eu.darken.capod.pods.core.DualPodDevice
|
||||||
|
import eu.darken.capod.pods.core.HasCase
|
||||||
|
import eu.darken.capod.pods.core.HasDualMicrophone
|
||||||
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
|
import eu.darken.capod.pods.core.apple.ApplePods
|
||||||
|
import eu.darken.capod.pods.core.apple.ApplePodsFactory
|
||||||
|
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||||
|
import java.time.Instant
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AirPods Pro clone similar to Twsi999999.
|
||||||
|
* Shorter data structure.
|
||||||
|
*/
|
||||||
|
data class VarunrAirPodsPro constructor(
|
||||||
|
override val identifier: PodDevice.Id = PodDevice.Id(),
|
||||||
|
override val seenLastAt: Instant = Instant.now(),
|
||||||
|
override val seenFirstAt: Instant = Instant.now(),
|
||||||
|
override val seenCounter: Int = 1,
|
||||||
|
override val scanResult: BleScanResult,
|
||||||
|
override val proximityMessage: ProximityPairing.Message,
|
||||||
|
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
||||||
|
private val rssiAverage: Int? = null,
|
||||||
|
private val cachedBatteryPercentage: Float? = null,
|
||||||
|
) : ApplePods, DualPodDevice, HasDualMicrophone, HasCase {
|
||||||
|
|
||||||
|
override val model: PodDevice.Model = PodDevice.Model.VARUNR_AIRPODS_PRO
|
||||||
|
|
||||||
|
override val rssi: Int
|
||||||
|
get() = rssiAverage ?: super<ApplePods>.rssi
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normally values for the left pod are in the lower nibbles, if the left pod is primary (microphone)
|
||||||
|
* If the right pod is the primary, the values are flipped.
|
||||||
|
*/
|
||||||
|
val areValuesFlipped: Boolean
|
||||||
|
get() = !rawStatus.isBitSet(5)
|
||||||
|
|
||||||
|
override val batteryLeftPodPercent: Float?
|
||||||
|
get() {
|
||||||
|
val value = when (areValuesFlipped) {
|
||||||
|
true -> rawPodsBattery.upperNibble.toInt()
|
||||||
|
false -> rawPodsBattery.lowerNibble.toInt()
|
||||||
|
}
|
||||||
|
return when (value) {
|
||||||
|
15 -> null
|
||||||
|
else -> if (value > 10) {
|
||||||
|
log { "Left pod: Above 100% battery: $value" }
|
||||||
|
1.0f
|
||||||
|
} else {
|
||||||
|
(value / 10f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override val batteryRightPodPercent: Float?
|
||||||
|
get() {
|
||||||
|
val value = when (areValuesFlipped) {
|
||||||
|
true -> rawPodsBattery.lowerNibble.toInt()
|
||||||
|
false -> rawPodsBattery.upperNibble.toInt()
|
||||||
|
}
|
||||||
|
return when (value) {
|
||||||
|
15 -> null
|
||||||
|
else -> if (value > 10) {
|
||||||
|
log { "Right pod: Above 100% battery: $value" }
|
||||||
|
1.0f
|
||||||
|
} else {
|
||||||
|
value / 10f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val isThisPodInThecase: Boolean
|
||||||
|
get() = rawStatus.isBitSet(6)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
override val isRightPodMicrophone: Boolean
|
||||||
|
get() = !rawStatus.isBitSet(5) xor isThisPodInThecase
|
||||||
|
|
||||||
|
override val batteryCasePercent: Float?
|
||||||
|
get() = when (val value = rawCaseBattery.toInt()) {
|
||||||
|
15 -> cachedBatteryPercentage
|
||||||
|
else -> if (value > 10) {
|
||||||
|
log { "Case: Above 100% battery: $value" }
|
||||||
|
1.0f
|
||||||
|
} else {
|
||||||
|
value / 10f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override val isCaseCharging: Boolean
|
||||||
|
get() = rawFlags.isBitSet(2)
|
||||||
|
|
||||||
|
class Factory @Inject constructor() : ApplePodsFactory<VarunrAirPodsPro>(TAG) {
|
||||||
|
|
||||||
|
override fun isResponsible(message: ProximityPairing.Message): Boolean = message.run {
|
||||||
|
// Official message length is 19HEX, i.e. binary 25, did they copy this wrong?
|
||||||
|
getModelInfo().full == DEVICE_CODE && length == 19
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun create(scanResult: BleScanResult, message: ProximityPairing.Message): ApplePods {
|
||||||
|
var basic = VarunrAirPodsPro(scanResult = scanResult, proximityMessage = message)
|
||||||
|
val result = searchHistory(basic)
|
||||||
|
|
||||||
|
if (result != null) basic = basic.copy(identifier = result.id)
|
||||||
|
updateHistory(basic)
|
||||||
|
|
||||||
|
if (result == null) return basic
|
||||||
|
|
||||||
|
return basic.copy(
|
||||||
|
identifier = result.id,
|
||||||
|
seenFirstAt = result.seenFirstAt,
|
||||||
|
seenCounter = result.seenCounter,
|
||||||
|
confidence = result.confidence,
|
||||||
|
cachedBatteryPercentage = result.getLatestCaseBattery(),
|
||||||
|
rssiAverage = result.averageRssi(basic.rssi),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val DEVICE_CODE = 0x0E20.toUShort()
|
||||||
|
private val TAG = logTag("PodDevice", "Apple", "Varunr", "AirPodsPro")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -27,10 +27,6 @@ object ProximityPairing {
|
|||||||
log { "Not a proximity pairing message: $this" }
|
log { "Not a proximity pairing message: $this" }
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
if (message.length != PROXIMITY_PAIRING_MESSAGE_LENGTH) {
|
|
||||||
log { "Proximity pairing message has invalid length." }
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return Message(
|
return Message(
|
||||||
type = message.type,
|
type = message.type,
|
||||||
@@ -43,7 +39,7 @@ object ProximityPairing {
|
|||||||
fun getBleScanFilter(): Set<ScanFilter> {
|
fun getBleScanFilter(): Set<ScanFilter> {
|
||||||
val manufacturerData = ByteArray(CONTINUITY_PROTOCOL_MESSAGE_LENGTH).apply {
|
val manufacturerData = ByteArray(CONTINUITY_PROTOCOL_MESSAGE_LENGTH).apply {
|
||||||
this[0] = CONTINUITY_PROTOCOL_MESSAGE_TYPE_PROXIMITY_PAIRING.toByte()
|
this[0] = CONTINUITY_PROTOCOL_MESSAGE_TYPE_PROXIMITY_PAIRING.toByte()
|
||||||
this[1] = PROXIMITY_PAIRING_MESSAGE_LENGTH.toByte()
|
this[1] = PAIRING_MESSAGE_LENGTH.toByte()
|
||||||
}
|
}
|
||||||
|
|
||||||
val manufacturerDataMask = ByteArray(CONTINUITY_PROTOCOL_MESSAGE_LENGTH).apply {
|
val manufacturerDataMask = ByteArray(CONTINUITY_PROTOCOL_MESSAGE_LENGTH).apply {
|
||||||
@@ -62,5 +58,7 @@ object ProximityPairing {
|
|||||||
|
|
||||||
private const val CONTINUITY_PROTOCOL_MESSAGE_LENGTH = 27
|
private const val CONTINUITY_PROTOCOL_MESSAGE_LENGTH = 27
|
||||||
internal val CONTINUITY_PROTOCOL_MESSAGE_TYPE_PROXIMITY_PAIRING = 0x07.toUByte()
|
internal val CONTINUITY_PROTOCOL_MESSAGE_TYPE_PROXIMITY_PAIRING = 0x07.toUByte()
|
||||||
internal const val PROXIMITY_PAIRING_MESSAGE_LENGTH = 25
|
|
||||||
|
// This is the default message length among official Apple devices, clones may have different length
|
||||||
|
internal const val PAIRING_MESSAGE_LENGTH = 25
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ import eu.darken.capod.common.debug.logging.logTag
|
|||||||
import eu.darken.capod.common.flow.setupCommonEventHandlers
|
import eu.darken.capod.common.flow.setupCommonEventHandlers
|
||||||
import eu.darken.capod.main.core.GeneralSettings
|
import eu.darken.capod.main.core.GeneralSettings
|
||||||
import eu.darken.capod.monitor.core.PodMonitor
|
import eu.darken.capod.monitor.core.PodMonitor
|
||||||
import eu.darken.capod.pods.core.HasDualEarDetection
|
|
||||||
import eu.darken.capod.pods.core.HasEarDetection
|
import eu.darken.capod.pods.core.HasEarDetection
|
||||||
import eu.darken.capod.pods.core.apple.DualApplePods
|
import eu.darken.capod.pods.core.HasEarDetectionDual
|
||||||
|
import eu.darken.capod.pods.core.apple.DualAirPods
|
||||||
import eu.darken.capod.reaction.settings.ReactionSettings
|
import eu.darken.capod.reaction.settings.ReactionSettings
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@@ -68,12 +68,12 @@ class AutoConnect @Inject constructor(
|
|||||||
val conditionFulfilled = when (condition) {
|
val conditionFulfilled = when (condition) {
|
||||||
AutoConnectCondition.WHEN_SEEN -> true
|
AutoConnectCondition.WHEN_SEEN -> true
|
||||||
AutoConnectCondition.CASE_OPEN -> when (mainDevice) {
|
AutoConnectCondition.CASE_OPEN -> when (mainDevice) {
|
||||||
is DualApplePods -> mainDevice.caseLidState == DualApplePods.LidState.OPEN
|
is DualAirPods -> mainDevice.caseLidState == DualAirPods.LidState.OPEN
|
||||||
else -> true
|
else -> true
|
||||||
}
|
}
|
||||||
AutoConnectCondition.IN_EAR -> when (mainDevice) {
|
AutoConnectCondition.IN_EAR -> when (mainDevice) {
|
||||||
is HasEarDetection -> {
|
is HasEarDetection -> {
|
||||||
if (mainDevice is HasDualEarDetection && reactionSettings.onePodMode.value) {
|
if (mainDevice is HasEarDetectionDual && reactionSettings.onePodMode.value) {
|
||||||
mainDevice.isEitherPodInEar
|
mainDevice.isEitherPodInEar
|
||||||
} else {
|
} else {
|
||||||
mainDevice.isBeingWorn
|
mainDevice.isBeingWorn
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ import eu.darken.capod.common.debug.logging.logTag
|
|||||||
import eu.darken.capod.common.flow.setupCommonEventHandlers
|
import eu.darken.capod.common.flow.setupCommonEventHandlers
|
||||||
import eu.darken.capod.common.flow.withPrevious
|
import eu.darken.capod.common.flow.withPrevious
|
||||||
import eu.darken.capod.monitor.core.PodMonitor
|
import eu.darken.capod.monitor.core.PodMonitor
|
||||||
import eu.darken.capod.pods.core.HasDualEarDetection
|
|
||||||
import eu.darken.capod.pods.core.HasEarDetection
|
import eu.darken.capod.pods.core.HasEarDetection
|
||||||
|
import eu.darken.capod.pods.core.HasEarDetectionDual
|
||||||
import eu.darken.capod.reaction.settings.ReactionSettings
|
import eu.darken.capod.reaction.settings.ReactionSettings
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@@ -48,7 +48,7 @@ class PlayPause @Inject constructor(
|
|||||||
return@onEach
|
return@onEach
|
||||||
}
|
}
|
||||||
|
|
||||||
if (previous is HasDualEarDetection && current is HasDualEarDetection && reactionSettings.onePodMode.value) {
|
if (previous is HasEarDetectionDual && current is HasEarDetectionDual && reactionSettings.onePodMode.value) {
|
||||||
log(TAG) { "Ear status changed for dual pod device in single pod mode." }
|
log(TAG) { "Ear status changed for dual pod device in single pod mode." }
|
||||||
val previousWorn = previous.isEitherPodInEar
|
val previousWorn = previous.isEitherPodInEar
|
||||||
val currentWorn = current.isEitherPodInEar
|
val currentWorn = current.isEitherPodInEar
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import eu.darken.capod.common.flow.setupCommonEventHandlers
|
|||||||
import eu.darken.capod.common.flow.withPrevious
|
import eu.darken.capod.common.flow.withPrevious
|
||||||
import eu.darken.capod.monitor.core.PodMonitor
|
import eu.darken.capod.monitor.core.PodMonitor
|
||||||
import eu.darken.capod.pods.core.PodDevice
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
import eu.darken.capod.pods.core.apple.DualApplePods
|
import eu.darken.capod.pods.core.apple.DualAirPods
|
||||||
import eu.darken.capod.reaction.popup.ui.PopUpWindow
|
import eu.darken.capod.reaction.popup.ui.PopUpWindow
|
||||||
import eu.darken.capod.reaction.settings.ReactionSettings
|
import eu.darken.capod.reaction.settings.ReactionSettings
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
@@ -39,7 +39,7 @@ class PopUpReaction @Inject constructor(
|
|||||||
.withPrevious()
|
.withPrevious()
|
||||||
.setupCommonEventHandlers(TAG) { "monitor" }
|
.setupCommonEventHandlers(TAG) { "monitor" }
|
||||||
.map { (previous, current) ->
|
.map { (previous, current) ->
|
||||||
if (previous is DualApplePods? && current is DualApplePods) {
|
if (previous is DualAirPods? && current is DualAirPods) {
|
||||||
log(TAG, VERBOSE) {
|
log(TAG, VERBOSE) {
|
||||||
val prev = previous?.rawCaseLidState?.let { String.format("%02X", it.toByte()) }
|
val prev = previous?.rawCaseLidState?.let { String.format("%02X", it.toByte()) }
|
||||||
val cur = current.rawCaseLidState.let { String.format("%02X", it.toByte()) }
|
val cur = current.rawCaseLidState.let { String.format("%02X", it.toByte()) }
|
||||||
@@ -60,8 +60,8 @@ class PopUpReaction @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun tryPopWindow(current: DualApplePods) {
|
private suspend fun tryPopWindow(current: DualAirPods) {
|
||||||
if (current.caseLidState == DualApplePods.LidState.OPEN) {
|
if (current.caseLidState == DualAirPods.LidState.OPEN) {
|
||||||
log(TAG, INFO) { "Show popup" }
|
log(TAG, INFO) { "Show popup" }
|
||||||
|
|
||||||
val now = Instant.now()
|
val now = Instant.now()
|
||||||
@@ -78,9 +78,9 @@ class PopUpReaction @Inject constructor(
|
|||||||
withContext(dispatcherProvider.Main) {
|
withContext(dispatcherProvider.Main) {
|
||||||
popupWindow.show(current)
|
popupWindow.show(current)
|
||||||
}
|
}
|
||||||
} else if (current.caseLidState != DualApplePods.LidState.OPEN) {
|
} else if (current.caseLidState != DualAirPods.LidState.OPEN) {
|
||||||
when (current.caseLidState) {
|
when (current.caseLidState) {
|
||||||
DualApplePods.LidState.CLOSED -> {
|
DualAirPods.LidState.CLOSED -> {
|
||||||
log(TAG, INFO) { "Lid was actively closed, resetting cooldown." }
|
log(TAG, INFO) { "Lid was actively closed, resetting cooldown." }
|
||||||
coolDowns.remove(current.identifier)
|
coolDowns.remove(current.identifier)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,11 +10,9 @@ import dagger.hilt.android.qualifiers.ApplicationContext
|
|||||||
import eu.darken.capod.R
|
import eu.darken.capod.R
|
||||||
import eu.darken.capod.common.debug.autoreport.DebugSettings
|
import eu.darken.capod.common.debug.autoreport.DebugSettings
|
||||||
import eu.darken.capod.databinding.PopupNotificationDualPodsBinding
|
import eu.darken.capod.databinding.PopupNotificationDualPodsBinding
|
||||||
import eu.darken.capod.databinding.PopupNotificationSinglePodsBasicBinding
|
|
||||||
import eu.darken.capod.databinding.PopupNotificationSinglePodsBinding
|
import eu.darken.capod.databinding.PopupNotificationSinglePodsBinding
|
||||||
import eu.darken.capod.pods.core.*
|
import eu.darken.capod.pods.core.*
|
||||||
import eu.darken.capod.pods.core.apple.BasicSingleApplePods
|
import eu.darken.capod.pods.core.apple.DualAirPods
|
||||||
import eu.darken.capod.pods.core.apple.DualApplePods
|
|
||||||
import eu.darken.capod.pods.core.apple.SingleApplePods
|
import eu.darken.capod.pods.core.apple.SingleApplePods
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@@ -28,16 +26,13 @@ class PopUpPodViewFactory @Inject constructor(
|
|||||||
private val layoutInflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
|
private val layoutInflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
|
||||||
|
|
||||||
fun createContentView(parent: ViewGroup, device: PodDevice): View = when (device) {
|
fun createContentView(parent: ViewGroup, device: PodDevice): View = when (device) {
|
||||||
is DualApplePods -> createDualApplePods(parent, device)
|
is DualAirPods -> createDualApplePods(parent, device)
|
||||||
is SingleApplePods -> createSingleApplePods(parent, device) // Unused, has no case to trigger reaction?
|
// Unused, has no case to trigger reaction?
|
||||||
is BasicSingleApplePods -> createSingleBasicApplePods(
|
is SingleApplePods -> createSingleApplePods(parent, device)
|
||||||
parent,
|
|
||||||
device
|
|
||||||
) // Unused, has no case to trigger reaction?
|
|
||||||
else -> throw IllegalArgumentException("Unexpected device: $device")
|
else -> throw IllegalArgumentException("Unexpected device: $device")
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createDualApplePods(parent: ViewGroup, device: DualApplePods): View =
|
private fun createDualApplePods(parent: ViewGroup, device: DualAirPods): View =
|
||||||
PopupNotificationDualPodsBinding.inflate(layoutInflater, parent, false).apply {
|
PopupNotificationDualPodsBinding.inflate(layoutInflater, parent, false).apply {
|
||||||
device.apply {
|
device.apply {
|
||||||
podIcon.setImageResource(iconRes)
|
podIcon.setImageResource(iconRes)
|
||||||
@@ -72,17 +67,4 @@ class PopUpPodViewFactory @Inject constructor(
|
|||||||
}
|
}
|
||||||
}.root
|
}.root
|
||||||
|
|
||||||
private fun createSingleBasicApplePods(parent: ViewGroup, device: BasicSingleApplePods): View =
|
|
||||||
PopupNotificationSinglePodsBasicBinding.inflate(layoutInflater, parent, false).apply {
|
|
||||||
device.apply {
|
|
||||||
headphonesIcon.setImageResource(iconRes)
|
|
||||||
headphonesLabel.text = getLabel(context)
|
|
||||||
signal.text = getSignalQuality(context)
|
|
||||||
signal.isInvisible = debugSettings.isDebugModeEnabled.value
|
|
||||||
|
|
||||||
headphonesBatteryIcon.setImageResource(getBatteryDrawable(batteryHeadsetPercent))
|
|
||||||
headphonesBatteryLabel.text = getBatteryLevelHeadset(context)
|
|
||||||
}
|
|
||||||
}.root
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -12,13 +12,11 @@ import dagger.hilt.android.AndroidEntryPoint
|
|||||||
import eu.darken.capod.R
|
import eu.darken.capod.R
|
||||||
import eu.darken.capod.common.uix.PreferenceFragment2
|
import eu.darken.capod.common.uix.PreferenceFragment2
|
||||||
import eu.darken.capod.common.upgrade.UpgradeRepo
|
import eu.darken.capod.common.upgrade.UpgradeRepo
|
||||||
import eu.darken.capod.common.upgrade.isPro
|
|
||||||
import eu.darken.capod.main.core.GeneralSettings
|
import eu.darken.capod.main.core.GeneralSettings
|
||||||
import eu.darken.capod.main.core.MonitorMode
|
import eu.darken.capod.main.core.MonitorMode
|
||||||
import eu.darken.capod.main.ui.settings.general.DeviceSelectionDialogFactory
|
import eu.darken.capod.main.ui.settings.general.DeviceSelectionDialogFactory
|
||||||
import eu.darken.capod.reaction.autoconnect.AutoConnectCondition
|
import eu.darken.capod.reaction.autoconnect.AutoConnectCondition
|
||||||
import eu.darken.capod.reaction.settings.ReactionSettings
|
import eu.darken.capod.reaction.settings.ReactionSettings
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@Keep
|
@Keep
|
||||||
@@ -36,6 +34,7 @@ class ReactionSettingsFragment : PreferenceFragment2() {
|
|||||||
|
|
||||||
override val preferenceFile: Int = R.xml.preferences_reactions
|
override val preferenceFile: Int = R.xml.preferences_reactions
|
||||||
|
|
||||||
|
private var isPro: Boolean = false
|
||||||
private val autoConnectConditionPref by lazy { findPreference<ListPreference>(settings.autoConnectCondition.key)!! }
|
private val autoConnectConditionPref by lazy { findPreference<ListPreference>(settings.autoConnectCondition.key)!! }
|
||||||
|
|
||||||
override fun onPreferencesCreated() {
|
override fun onPreferencesCreated() {
|
||||||
@@ -44,12 +43,11 @@ class ReactionSettingsFragment : PreferenceFragment2() {
|
|||||||
entryValues = AutoConnectCondition.values().map { settings.autoConnectCondition.rawWriter(it) as String }
|
entryValues = AutoConnectCondition.values().map { settings.autoConnectCondition.rawWriter(it) as String }
|
||||||
.toTypedArray()
|
.toTypedArray()
|
||||||
}
|
}
|
||||||
|
|
||||||
super.onPreferencesCreated()
|
super.onPreferencesCreated()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPreferenceTreeClick(preference: Preference): Boolean {
|
override fun onPreferenceTreeClick(preference: Preference): Boolean {
|
||||||
val isPro = runBlocking { upgradeRepo.isPro() }
|
|
||||||
|
|
||||||
if (preference.key == reactionSettings.autoPlay.key && !isPro) {
|
if (preference.key == reactionSettings.autoPlay.key && !isPro) {
|
||||||
preference as CheckBoxPreference
|
preference as CheckBoxPreference
|
||||||
upgradeRepo.launchBillingFlow(requireActivity())
|
upgradeRepo.launchBillingFlow(requireActivity())
|
||||||
@@ -95,6 +93,8 @@ class ReactionSettingsFragment : PreferenceFragment2() {
|
|||||||
autoConnectConditionPref.isEnabled = it
|
autoConnectConditionPref.isEnabled = it
|
||||||
}
|
}
|
||||||
|
|
||||||
|
vm.isPro.observe2 { isPro = true }
|
||||||
|
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import eu.darken.capod.common.bluetooth.BluetoothManager2
|
|||||||
import eu.darken.capod.common.coroutine.DispatcherProvider
|
import eu.darken.capod.common.coroutine.DispatcherProvider
|
||||||
import eu.darken.capod.common.debug.logging.logTag
|
import eu.darken.capod.common.debug.logging.logTag
|
||||||
import eu.darken.capod.common.uix.ViewModel3
|
import eu.darken.capod.common.uix.ViewModel3
|
||||||
|
import eu.darken.capod.common.upgrade.UpgradeRepo
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
@@ -13,10 +15,13 @@ class ReactionSettingsFragmentVM @Inject constructor(
|
|||||||
private val handle: SavedStateHandle,
|
private val handle: SavedStateHandle,
|
||||||
private val dispatcherProvider: DispatcherProvider,
|
private val dispatcherProvider: DispatcherProvider,
|
||||||
private val bluetoothManager: BluetoothManager2,
|
private val bluetoothManager: BluetoothManager2,
|
||||||
|
private val upgradeRepo: UpgradeRepo,
|
||||||
) : ViewModel3(dispatcherProvider) {
|
) : ViewModel3(dispatcherProvider) {
|
||||||
|
|
||||||
val bondedDevices = bluetoothManager.bondedDevices().toList()
|
val bondedDevices = bluetoothManager.bondedDevices().toList()
|
||||||
|
|
||||||
|
val isPro = upgradeRepo.upgradeInfo.map { it.isPro }.asLiveData2()
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val TAG = logTag("Settings", "Reaction", "VM")
|
private val TAG = logTag("Settings", "Reaction", "VM")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,5 +6,5 @@
|
|||||||
android:tint="?attr/colorControlNormal">
|
android:tint="?attr/colorControlNormal">
|
||||||
<path
|
<path
|
||||||
android:fillColor="@android:color/white"
|
android:fillColor="@android:color/white"
|
||||||
android:pathData="M20.38,8.53C20.54,8.13 21.06,6.54 20.21,4.39C20.21,4.39 18.9,4 15.91,6C14.66,5.67 13.33,5.62 12,5.62C10.68,5.62 9.34,5.67 8.09,6C5.1,3.97 3.79,4.39 3.79,4.39C2.94,6.54 3.46,8.13 3.63,8.53C2.61,9.62 2,11 2,12.72C2,19.16 6.16,20.61 12,20.61C17.79,20.61 22,19.16 22,12.72C22,11 21.39,9.62 20.38,8.53M12,19.38C7.88,19.38 4.53,19.19 4.53,15.19C4.53,14.24 5,13.34 5.8,12.61C7.14,11.38 9.43,12.03 12,12.03C14.59,12.03 16.85,11.38 18.2,12.61C19,13.34 19.5,14.23 19.5,15.19C19.5,19.18 16.13,19.38 12,19.38M8.86,13.12C8.04,13.12 7.36,14.12 7.36,15.34C7.36,16.57 8.04,17.58 8.86,17.58C9.69,17.58 10.36,16.58 10.36,15.34C10.36,14.11 9.69,13.12 8.86,13.12M15.14,13.12C14.31,13.12 13.64,14.11 13.64,15.34C13.64,16.58 14.31,17.58 15.14,17.58C15.96,17.58 16.64,16.58 16.64,15.34C16.64,14.11 16,13.12 15.14,13.12Z" />
|
android:pathData="M12,2A10,10 0 0,0 2,12C2,16.42 4.87,20.17 8.84,21.5C9.34,21.58 9.5,21.27 9.5,21C9.5,20.77 9.5,20.14 9.5,19.31C6.73,19.91 6.14,17.97 6.14,17.97C5.68,16.81 5.03,16.5 5.03,16.5C4.12,15.88 5.1,15.9 5.1,15.9C6.1,15.97 6.63,16.93 6.63,16.93C7.5,18.45 8.97,18 9.54,17.76C9.63,17.11 9.89,16.67 10.17,16.42C7.95,16.17 5.62,15.31 5.62,11.5C5.62,10.39 6,9.5 6.65,8.79C6.55,8.54 6.2,7.5 6.75,6.15C6.75,6.15 7.59,5.88 9.5,7.17C10.29,6.95 11.15,6.84 12,6.84C12.85,6.84 13.71,6.95 14.5,7.17C16.41,5.88 17.25,6.15 17.25,6.15C17.8,7.5 17.45,8.54 17.35,8.79C18,9.5 18.38,10.39 18.38,11.5C18.38,15.32 16.04,16.16 13.81,16.41C14.17,16.72 14.5,17.33 14.5,18.26C14.5,19.6 14.5,20.68 14.5,21C14.5,21.27 14.66,21.59 15.17,21.5C19.14,20.16 22,16.42 22,12A10,10 0 0,0 12,2Z" />
|
||||||
</vector>
|
</vector>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24.0"
|
||||||
|
android:tint="?attr/colorControlNormal">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M20.38,8.53C20.54,8.13 21.06,6.54 20.21,4.39C20.21,4.39 18.9,4 15.91,6C14.66,5.67 13.33,5.62 12,5.62C10.68,5.62 9.34,5.67 8.09,6C5.1,3.97 3.79,4.39 3.79,4.39C2.94,6.54 3.46,8.13 3.63,8.53C2.61,9.62 2,11 2,12.72C2,19.16 6.16,20.61 12,20.61C17.79,20.61 22,19.16 22,12.72C22,11 21.39,9.62 20.38,8.53M12,19.38C7.88,19.38 4.53,19.19 4.53,15.19C4.53,14.24 5,13.34 5.8,12.61C7.14,11.38 9.43,12.03 12,12.03C14.59,12.03 16.85,11.38 18.2,12.61C19,13.34 19.5,14.23 19.5,15.19C19.5,19.18 16.13,19.38 12,19.38M8.86,13.12C8.04,13.12 7.36,14.12 7.36,15.34C7.36,16.57 8.04,17.58 8.86,17.58C9.69,17.58 10.36,16.58 10.36,15.34C10.36,14.11 9.69,13.12 8.86,13.12M15.14,13.12C14.31,13.12 13.64,14.11 13.64,15.34C13.64,16.58 14.31,17.58 15.14,17.58C15.96,17.58 16.64,16.58 16.64,15.34C16.64,14.11 16,13.12 15.14,13.12Z" />
|
||||||
|
</vector>
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
tools:listitem="@layout/overview_pods_apple_dual_item"
|
tools:listitem="@layout/overview_pods_dual_item"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@id/toolbar" />
|
app:layout_constraintTop_toBottomOf="@id/toolbar" />
|
||||||
|
|
||||||
|
|||||||
@@ -18,12 +18,12 @@
|
|||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/pod_left_icon"
|
android:id="@+id/pod_left_icon"
|
||||||
style="@style/PodInfoItemIcon"
|
style="@style/PodInfoItemIcon.Notification"
|
||||||
android:src="@drawable/ic_airpod_left_24" />
|
android:src="@drawable/ic_airpod_left_24" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/pod_left_label"
|
android:id="@+id/pod_left_label"
|
||||||
style="@style/TextAppearance.Compat.Notification.Title"
|
style="@style/PodInfoItemText.Notification"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="center"
|
android:layout_gravity="center"
|
||||||
@@ -31,12 +31,12 @@
|
|||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/pod_left_charging"
|
android:id="@+id/pod_left_charging"
|
||||||
style="@style/PodInfoItemIcon"
|
style="@style/PodInfoItemIcon.Notification"
|
||||||
android:src="@drawable/ic_baseline_power_24" />
|
android:src="@drawable/ic_baseline_power_24" />
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/pod_left_ear"
|
android:id="@+id/pod_left_ear"
|
||||||
style="@style/PodInfoItemIcon"
|
style="@style/PodInfoItemIcon.Notification"
|
||||||
android:src="@drawable/ic_baseline_hearing_24" />
|
android:src="@drawable/ic_baseline_hearing_24" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
@@ -52,12 +52,12 @@
|
|||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/pod_case_icon"
|
android:id="@+id/pod_case_icon"
|
||||||
style="@style/PodInfoItemIcon"
|
style="@style/PodInfoItemIcon.Notification"
|
||||||
android:src="@drawable/ic_airpod_case_24" />
|
android:src="@drawable/ic_airpod_case_24" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/pod_case_label"
|
android:id="@+id/pod_case_label"
|
||||||
style="@style/TextAppearance.Compat.Notification.Title"
|
style="@style/PodInfoItemText.Notification"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="center"
|
android:layout_gravity="center"
|
||||||
@@ -65,7 +65,7 @@
|
|||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/pod_case_charging"
|
android:id="@+id/pod_case_charging"
|
||||||
style="@style/PodInfoItemIcon"
|
style="@style/PodInfoItemIcon.Notification"
|
||||||
android:src="@drawable/ic_baseline_power_24" />
|
android:src="@drawable/ic_baseline_power_24" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
@@ -81,12 +81,12 @@
|
|||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/pod_right_icon"
|
android:id="@+id/pod_right_icon"
|
||||||
style="@style/PodInfoItemIcon"
|
style="@style/PodInfoItemIcon.Notification"
|
||||||
android:src="@drawable/ic_airpod_right_24" />
|
android:src="@drawable/ic_airpod_right_24" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/pod_right_label"
|
android:id="@+id/pod_right_label"
|
||||||
style="@style/TextAppearance.Compat.Notification.Title"
|
style="@style/PodInfoItemText.Notification"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="center"
|
android:layout_gravity="center"
|
||||||
@@ -94,12 +94,12 @@
|
|||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/pod_right_charging"
|
android:id="@+id/pod_right_charging"
|
||||||
style="@style/PodInfoItemIcon"
|
style="@style/PodInfoItemIcon.Notification"
|
||||||
android:src="@drawable/ic_baseline_power_24" />
|
android:src="@drawable/ic_baseline_power_24" />
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/pod_right_ear"
|
android:id="@+id/pod_right_ear"
|
||||||
style="@style/PodInfoItemIcon"
|
style="@style/PodInfoItemIcon.Notification"
|
||||||
android:src="@drawable/ic_baseline_hearing_24" />
|
android:src="@drawable/ic_baseline_hearing_24" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
|||||||
@@ -1,37 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:gravity="center_horizontal">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/headphones_label"
|
|
||||||
style="@style/TextAppearance.Compat.Notification.Title"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:gravity="center"
|
|
||||||
tools:text="Beats Solo 3" />
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginHorizontal="8dp"
|
|
||||||
android:orientation="horizontal">
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/headphones_battery_icon"
|
|
||||||
style="@style/PodInfoItemIcon"
|
|
||||||
android:src="@drawable/ic_baseline_battery_unknown_24" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/headphones_battery_label"
|
|
||||||
style="@style/TextAppearance.Compat.Notification.Title"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="center"
|
|
||||||
android:layout_marginHorizontal="8dp"
|
|
||||||
tools:text="100%" />
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/headphones_label"
|
android:id="@+id/headphones_label"
|
||||||
style="@style/TextAppearance.Compat.Notification.Title"
|
style="@style/PodInfoItemText.Notification"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
@@ -21,12 +21,12 @@
|
|||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/headphones_battery_icon"
|
android:id="@+id/headphones_battery_icon"
|
||||||
style="@style/PodInfoItemIcon"
|
style="@style/PodInfoItemIcon.Notification"
|
||||||
android:src="@drawable/ic_baseline_battery_unknown_24" />
|
android:src="@drawable/ic_baseline_battery_unknown_24" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/headphones_battery_label"
|
android:id="@+id/headphones_battery_label"
|
||||||
style="@style/TextAppearance.Compat.Notification.Title"
|
style="@style/PodInfoItemText.Notification"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="center"
|
android:layout_gravity="center"
|
||||||
@@ -35,12 +35,12 @@
|
|||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/headphones_charging"
|
android:id="@+id/headphones_charging"
|
||||||
style="@style/PodInfoItemIcon"
|
style="@style/PodInfoItemIcon.Notification"
|
||||||
android:src="@drawable/ic_baseline_power_24" />
|
android:src="@drawable/ic_baseline_power_24" />
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/headphones_worn"
|
android:id="@+id/headphones_worn"
|
||||||
style="@style/PodInfoItemIcon"
|
style="@style/PodInfoItemIcon.Notification"
|
||||||
android:src="@drawable/ic_baseline_hearing_24" />
|
android:src="@drawable/ic_baseline_hearing_24" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/device"
|
android:id="@+id/device"
|
||||||
style="@style/TextAppearance.Compat.Notification.Title"
|
style="@style/PodInfoItemText.Notification"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginHorizontal="8dp"
|
android:layout_marginHorizontal="8dp"
|
||||||
|
|||||||
@@ -1,152 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:id="@+id/card"
|
|
||||||
style="@style/MyCardView"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginVertical="8dp"
|
|
||||||
tools:context=".main.ui.MainActivity">
|
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
|
||||||
android:id="@+id/card_content"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content">
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/device_icon"
|
|
||||||
android:layout_width="24dp"
|
|
||||||
android:layout_height="24dp"
|
|
||||||
android:layout_marginStart="16dp"
|
|
||||||
app:layout_constraintBottom_toBottomOf="@id/name"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="@id/name"
|
|
||||||
app:srcCompat="@drawable/ic_device_generic_headphones" />
|
|
||||||
|
|
||||||
<com.google.android.material.textview.MaterialTextView
|
|
||||||
android:id="@+id/name"
|
|
||||||
style="@style/TextAppearance.MaterialComponents.Body2"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginHorizontal="16dp"
|
|
||||||
android:layout_marginStart="4dp"
|
|
||||||
android:layout_marginTop="16dp"
|
|
||||||
android:layout_marginEnd="8dp"
|
|
||||||
android:gravity="center_vertical"
|
|
||||||
app:layout_constraintBottom_toTopOf="@+id/last_seen"
|
|
||||||
app:layout_constraintEnd_toStartOf="@id/reception"
|
|
||||||
app:layout_constraintStart_toEndOf="@id/device_icon"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
tools:text="Power Beats 3" />
|
|
||||||
|
|
||||||
<com.google.android.material.textview.MaterialTextView
|
|
||||||
android:id="@+id/last_seen"
|
|
||||||
style="@style/TextAppearance.MaterialComponents.Caption"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginBottom="8dp"
|
|
||||||
app:layout_constraintBottom_toTopOf="@id/barrier_top"
|
|
||||||
app:layout_constraintEnd_toEndOf="@id/name"
|
|
||||||
app:layout_constraintStart_toStartOf="@id/name"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/name"
|
|
||||||
tools:text="3s ago" />
|
|
||||||
|
|
||||||
<com.google.android.material.textview.MaterialTextView
|
|
||||||
android:id="@+id/reception"
|
|
||||||
style="@style/TextAppearance.MaterialComponents.Caption"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginHorizontal="16dp"
|
|
||||||
android:layout_marginStart="8dp"
|
|
||||||
android:gravity="center"
|
|
||||||
app:layout_constraintBottom_toBottomOf="@id/name"
|
|
||||||
app:layout_constraintEnd_toStartOf="@id/reception_icon"
|
|
||||||
app:layout_constraintStart_toEndOf="@id/name"
|
|
||||||
app:layout_constraintTop_toTopOf="@+id/name"
|
|
||||||
tools:text="Yours (RSSI -61)" />
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/reception_icon"
|
|
||||||
style="@style/TextAppearance.MaterialComponents.Caption"
|
|
||||||
android:layout_width="16dp"
|
|
||||||
android:layout_height="16dp"
|
|
||||||
android:layout_marginHorizontal="16dp"
|
|
||||||
android:layout_marginStart="4dp"
|
|
||||||
android:layout_marginEnd="16dp"
|
|
||||||
android:gravity="center"
|
|
||||||
app:layout_constraintBottom_toBottomOf="@+id/reception"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toEndOf="@id/reception"
|
|
||||||
app:layout_constraintTop_toTopOf="@+id/reception"
|
|
||||||
app:srcCompat="@drawable/ic_baseline_settings_input_antenna_24"
|
|
||||||
tools:text="Yours (RSSI -61)" />
|
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.Barrier
|
|
||||||
android:id="@+id/barrier_top"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
app:barrierDirection="top"
|
|
||||||
app:constraint_referenced_ids="headphones"
|
|
||||||
tools:layout_editor_absoluteY="60dp" />
|
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
|
||||||
android:id="@+id/headphones"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginStart="16dp"
|
|
||||||
android:layout_marginEnd="8dp"
|
|
||||||
app:layout_constraintBottom_toTopOf="@id/barrier_bottom"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/barrier_top"
|
|
||||||
app:layout_constraintVertical_bias="0.0"
|
|
||||||
app:layout_goneMarginBottom="16dp">
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/battery_icon"
|
|
||||||
style="@style/PodInfoItemIcon"
|
|
||||||
android:src="@drawable/ic_baseline_battery_unknown_24"
|
|
||||||
app:layout_constraintBottom_toBottomOf="@id/battery_label"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="@id/battery_label" />
|
|
||||||
|
|
||||||
<com.google.android.material.textview.MaterialTextView
|
|
||||||
android:id="@+id/battery_label"
|
|
||||||
style="@style/PodInfoItemText"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginStart="8dp"
|
|
||||||
android:layout_marginTop="2dp"
|
|
||||||
android:text="@string/general_value_not_available_label"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toEndOf="@id/battery_icon"
|
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.Barrier
|
|
||||||
android:id="@+id/barrier_bottom"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
app:barrierDirection="bottom"
|
|
||||||
app:constraint_referenced_ids="headphones" />
|
|
||||||
|
|
||||||
<com.google.android.material.textview.MaterialTextView
|
|
||||||
android:id="@+id/status"
|
|
||||||
style="@style/TextAppearance.MaterialComponents.Body2"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginHorizontal="16dp"
|
|
||||||
android:layout_marginTop="8dp"
|
|
||||||
android:layout_marginBottom="16dp"
|
|
||||||
android:gravity="center_vertical"
|
|
||||||
android:visibility="gone"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/barrier_bottom"
|
|
||||||
tools:text="Music Active, Case Closed" />
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
||||||
</com.google.android.material.card.MaterialCardView>
|
|
||||||
+4
-4
@@ -12,7 +12,8 @@
|
|||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
android:id="@+id/card_content"
|
android:id="@+id/card_content"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content">
|
android:layout_height="wrap_content"
|
||||||
|
android:paddingBottom="16dp">
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/device_icon"
|
android:id="@+id/device_icon"
|
||||||
@@ -107,7 +108,6 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="16dp"
|
android:layout_marginStart="16dp"
|
||||||
android:layout_marginEnd="4dp"
|
android:layout_marginEnd="4dp"
|
||||||
android:minHeight="108dp"
|
|
||||||
app:layout_constraintBottom_toTopOf="@id/barrier_bottom"
|
app:layout_constraintBottom_toTopOf="@id/barrier_bottom"
|
||||||
app:layout_constraintEnd_toStartOf="@id/pod_case_container"
|
app:layout_constraintEnd_toStartOf="@id/pod_case_container"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
@@ -117,6 +117,7 @@
|
|||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/pod_left_icon"
|
android:id="@+id/pod_left_icon"
|
||||||
style="@style/PodInfoItemIcon"
|
style="@style/PodInfoItemIcon"
|
||||||
|
android:contentDescription="@string/pods_dual_left_label"
|
||||||
android:src="@drawable/ic_airpod_left_24"
|
android:src="@drawable/ic_airpod_left_24"
|
||||||
app:layout_constraintBottom_toBottomOf="@id/pod_left_label"
|
app:layout_constraintBottom_toBottomOf="@id/pod_left_label"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
@@ -322,6 +323,7 @@
|
|||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/pod_right_icon"
|
android:id="@+id/pod_right_icon"
|
||||||
style="@style/PodInfoItemIcon"
|
style="@style/PodInfoItemIcon"
|
||||||
|
android:contentDescription="@string/pods_dual_right_label"
|
||||||
android:src="@drawable/ic_airpod_right_24"
|
android:src="@drawable/ic_airpod_right_24"
|
||||||
app:layout_constraintBottom_toBottomOf="@id/pod_right_label"
|
app:layout_constraintBottom_toBottomOf="@id/pod_right_label"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
@@ -420,7 +422,6 @@
|
|||||||
app:layout_constraintTop_toBottomOf="@id/pod_right_microphone_label" />
|
app:layout_constraintTop_toBottomOf="@id/pod_right_microphone_label" />
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.Barrier
|
<androidx.constraintlayout.widget.Barrier
|
||||||
android:id="@+id/barrier_bottom"
|
android:id="@+id/barrier_bottom"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
@@ -435,7 +436,6 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginHorizontal="16dp"
|
android:layout_marginHorizontal="16dp"
|
||||||
android:layout_marginTop="8dp"
|
android:layout_marginTop="8dp"
|
||||||
android:layout_marginBottom="16dp"
|
|
||||||
android:gravity="center_vertical"
|
android:gravity="center_vertical"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
+3
-5
@@ -12,7 +12,8 @@
|
|||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
android:id="@+id/card_content"
|
android:id="@+id/card_content"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content">
|
android:layout_height="wrap_content"
|
||||||
|
android:paddingBottom="16dp">
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/device_icon"
|
android:id="@+id/device_icon"
|
||||||
@@ -96,13 +97,11 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="16dp"
|
android:layout_marginStart="16dp"
|
||||||
android:layout_marginEnd="8dp"
|
android:layout_marginEnd="8dp"
|
||||||
android:minHeight="64dp"
|
|
||||||
app:layout_constraintBottom_toTopOf="@id/barrier_bottom"
|
app:layout_constraintBottom_toTopOf="@id/barrier_bottom"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@id/barrier_top"
|
app:layout_constraintTop_toBottomOf="@id/barrier_top"
|
||||||
app:layout_constraintVertical_bias="0.0"
|
app:layout_constraintVertical_bias="0.0">
|
||||||
app:layout_goneMarginBottom="16dp">
|
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/battery_icon"
|
android:id="@+id/battery_icon"
|
||||||
@@ -180,7 +179,6 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginHorizontal="16dp"
|
android:layout_marginHorizontal="16dp"
|
||||||
android:layout_marginTop="8dp"
|
android:layout_marginTop="8dp"
|
||||||
android:layout_marginBottom="16dp"
|
|
||||||
android:gravity="center_vertical"
|
android:gravity="center_vertical"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content">
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/headphones_icon"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:src="@drawable/ic_device_generic_earbuds"
|
|
||||||
app:layout_constraintBottom_toBottomOf="@id/headphones_label"
|
|
||||||
app:layout_constraintEnd_toStartOf="@id/headphones_label"
|
|
||||||
app:layout_constraintHorizontal_bias="0.0"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="@id/headphones_label" />
|
|
||||||
|
|
||||||
<com.google.android.material.textview.MaterialTextView
|
|
||||||
android:id="@+id/headphones_label"
|
|
||||||
style="@style/TextAppearance.MaterialComponents.Body1"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="center"
|
|
||||||
android:layout_marginHorizontal="8dp"
|
|
||||||
android:layout_marginTop="4dp"
|
|
||||||
app:layout_constraintEnd_toStartOf="@id/signal"
|
|
||||||
app:layout_constraintStart_toEndOf="@id/headphones_icon"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
tools:text="Beats Solo 3" />
|
|
||||||
|
|
||||||
<com.google.android.material.textview.MaterialTextView
|
|
||||||
android:id="@+id/signal"
|
|
||||||
style="@style/TextAppearance.MaterialComponents.Caption"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:drawableEnd="@drawable/ic_baseline_signal_cellular_alt_24"
|
|
||||||
android:gravity="center"
|
|
||||||
android:minWidth="44dp"
|
|
||||||
app:layout_constraintBottom_toBottomOf="@+id/headphones_label"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="@+id/headphones_label"
|
|
||||||
tools:text="-90%" />
|
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
|
||||||
android:id="@+id/headphones_container"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginHorizontal="16dp"
|
|
||||||
android:layout_marginTop="16dp"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintHorizontal_chainStyle="spread"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/headphones_label">
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:id="@id/headphones_battery_icon"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:src="@drawable/ic_baseline_battery_unknown_24"
|
|
||||||
app:layout_constraintBottom_toBottomOf="@id/headphones_battery_label"
|
|
||||||
app:layout_constraintEnd_toStartOf="@id/headphones_battery_label"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="@id/headphones_battery_label" />
|
|
||||||
|
|
||||||
<com.google.android.material.textview.MaterialTextView
|
|
||||||
android:id="@+id/headphones_battery_label"
|
|
||||||
style="@style/TextAppearance.MaterialComponents.Body1"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginStart="4dp"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toEndOf="@id/headphones_battery_icon"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
tools:text="100%" />
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
||||||
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
||||||
@@ -64,7 +64,7 @@
|
|||||||
<string name="settings_general_description">Bütün tətbiqə təsir edən ümumi incə tənzimləmələr.</string>
|
<string name="settings_general_description">Bütün tətbiqə təsir edən ümumi incə tənzimləmələr.</string>
|
||||||
<string name="settings_acknowledgements_label">Təşəkkürlər</string>
|
<string name="settings_acknowledgements_label">Təşəkkürlər</string>
|
||||||
<string name="changelog_label">Dəyişiklik jurnalı</string>
|
<string name="changelog_label">Dəyişiklik jurnalı</string>
|
||||||
<string name="settings_support_email_developer_label">Tərtibatçıya e-poçt göndər</string>
|
<string name="settings_support_email_developer_label" comment="settings_support_email_developer_label Pressing this setting will open your default email app with a template for a mail to me and some device info (e.g. versions).">Tərtibatçıya e-poçt göndər</string>
|
||||||
<string name="settings_support_installid_label">Quraşdırma kimliyi</string>
|
<string name="settings_support_installid_label">Quraşdırma kimliyi</string>
|
||||||
<string name="settings_support_installid_desc">Avtomatik xəta hesabatları anonimdir. Tərtibatçının xəta hesabatlarınızı tapmasına ehtiyacı varsa quraşdırma kimliyinizi paylaşın.</string>
|
<string name="settings_support_installid_desc">Avtomatik xəta hesabatları anonimdir. Tərtibatçının xəta hesabatlarınızı tapmasına ehtiyacı varsa quraşdırma kimliyinizi paylaşın.</string>
|
||||||
<string name="settings_support_label">Dəstək</string>
|
<string name="settings_support_label">Dəstək</string>
|
||||||
@@ -81,7 +81,6 @@
|
|||||||
<string name="settings_monitor_mode_label">Müşahidə rejimi</string>
|
<string name="settings_monitor_mode_label">Müşahidə rejimi</string>
|
||||||
<string name="settings_monitor_mode_description">Bu tətbiq hansı şərtlər altında Bluetooth verilənlərini müşahidə edir.</string>
|
<string name="settings_monitor_mode_description">Bu tətbiq hansı şərtlər altında Bluetooth verilənlərini müşahidə edir.</string>
|
||||||
<string name="settings_scanner_mode_label">Skaner rejimi</string>
|
<string name="settings_scanner_mode_label">Skaner rejimi</string>
|
||||||
<string name="settings_scanner_mode_description">Bluetooth Zəif Enerji skaneri performansa üstünlük verməlidir, yoxsa enerjiyə qənaət etməlidir?</string>
|
|
||||||
<string name="settings_monitor_mode_manual_label">Tətbiq açılanda</string>
|
<string name="settings_monitor_mode_manual_label">Tətbiq açılanda</string>
|
||||||
<string name="settings_monitor_mode_automatic_label">Cihazla bağlantı qurulanda</string>
|
<string name="settings_monitor_mode_automatic_label">Cihazla bağlantı qurulanda</string>
|
||||||
<string name="settings_monitor_mode_always_label">Həmişə</string>
|
<string name="settings_monitor_mode_always_label">Həmişə</string>
|
||||||
@@ -89,13 +88,10 @@
|
|||||||
<string name="settings_scanner_mode_balanced_label">Tarazlı</string>
|
<string name="settings_scanner_mode_balanced_label">Tarazlı</string>
|
||||||
<string name="settings_scanner_mode_lowlatency_label">Aşağı gecikmə</string>
|
<string name="settings_scanner_mode_lowlatency_label">Aşağı gecikmə</string>
|
||||||
<string name="settings_autopause_label">Avto-fasilə</string>
|
<string name="settings_autopause_label">Avto-fasilə</string>
|
||||||
<string name="settings_autopause_description">Cihazı qulağınızdan çıxaranda musiqiyə fasilə verilsin.</string>
|
|
||||||
<string name="settings_showall_label">Bütün cihazları göstər</string>
|
<string name="settings_showall_label">Bütün cihazları göstər</string>
|
||||||
<string name="settings_showall_description">Sizə yaxın olan digər insanların cihazlarını göstər.</string>
|
<string name="settings_showall_description">Sizə yaxın olan digər insanların cihazlarını göstər.</string>
|
||||||
<string name="settings_autopplay_label">Avto-oynatma</string>
|
<string name="settings_autopplay_label">Avto-oynatma</string>
|
||||||
<string name="settings_autoplay_description">Cihazı qulağa taxanda musiqini oynat.</string>
|
|
||||||
<string name="settings_fake_data_label">Saxta verilənlər</string>
|
<string name="settings_fake_data_label">Saxta verilənlər</string>
|
||||||
<string name="settings_fake_data_description">Saxta verilənləri göstər, yəni, mövcud olmayan cihazı simulyasiya edin.</string>
|
|
||||||
<string name="settings_debug_label">Sazlama tənzimləmələri</string>
|
<string name="settings_debug_label">Sazlama tənzimləmələri</string>
|
||||||
<string name="settings_debug_description">Tətbiqlə əlaqəli problemləri aradan qaldırmağa kömək edəcək əlavə tənzimləmələr.</string>
|
<string name="settings_debug_description">Tətbiqlə əlaqəli problemləri aradan qaldırmağa kömək edəcək əlavə tənzimləmələr.</string>
|
||||||
<string name="settings_signal_minimum_label">Minimum siqnal keyfiyyəti</string>
|
<string name="settings_signal_minimum_label">Minimum siqnal keyfiyyəti</string>
|
||||||
@@ -119,11 +115,9 @@
|
|||||||
<string name="upgrade_capod_description">Əlavə özəllikləri əldə edin və tərtibatçını dəstəkləyin.</string>
|
<string name="upgrade_capod_description">Əlavə özəllikləri əldə edin və tərtibatçını dəstəkləyin.</string>
|
||||||
<string name="settings_popup_caseopen_label">Açılan pəncərəni göstər</string>
|
<string name="settings_popup_caseopen_label">Açılan pəncərəni göstər</string>
|
||||||
<string name="settings_popup_caseopen_description">Cihazın qutusu açılanda bir açılan pəncərə göstər (təcrübi).</string>
|
<string name="settings_popup_caseopen_description">Cihazın qutusu açılanda bir açılan pəncərə göstər (təcrübi).</string>
|
||||||
<string name="notification_channel_reaction_popup_label">Açılan cihaz reaksiyaları</string>
|
|
||||||
<string name="overview_bluetooth_disabled_label">Bluetooth sıradan çıxarıldı</string>
|
<string name="overview_bluetooth_disabled_label">Bluetooth sıradan çıxarıldı</string>
|
||||||
<string name="overview_bluetooth_disabled_description">Bluetooth sıradan çıxarıldı, fəallaşdırın ;)</string>
|
<string name="overview_bluetooth_disabled_description">Bluetooth sıradan çıxarıldı, fəallaşdırın ;)</string>
|
||||||
<string name="help_translate_label">Tərcümə</string>
|
<string name="help_translate_label">Tərcümə</string>
|
||||||
<string name="help_translate_description">Bunu sevimli dilinizə tərcümə etməyə kömək edin.</string>
|
|
||||||
<string name="settings_onepod_mode_label">Tək tərəf rejimi</string>
|
<string name="settings_onepod_mode_label">Tək tərəf rejimi</string>
|
||||||
<string name="settings_onepod_mode_description">Hər iki tərəfi də taxmağa ehtiyac yoxdur, reaksiyaları tətikləmək üçün tək tərəfi taxmaq yetərlidir.</string>
|
<string name="settings_onepod_mode_description">Hər iki tərəfi də taxmağa ehtiyac yoxdur, reaksiyaları tətikləmək üçün tək tərəfi taxmaq yetərlidir.</string>
|
||||||
<string name="permission_system_alert_window_label">Sistem xəbərdarlıq pəncərəsi</string>
|
<string name="permission_system_alert_window_label">Sistem xəbərdarlıq pəncərəsi</string>
|
||||||
|
|||||||
@@ -1,2 +1,29 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources></resources>
|
<resources>
|
||||||
|
<string name="app_name">CAPod</string>
|
||||||
|
<string name="app_name_pro">CAPod Pro</string>
|
||||||
|
<string name="app_name_foss">CAPod FOSS</string>
|
||||||
|
<string name="notification_channel_device_status_label">Статус прылады</string>
|
||||||
|
<string name="general_error_label">Памылка</string>
|
||||||
|
<string name="general_share_action">Падзяліцца</string>
|
||||||
|
<string name="general_done_action">Гатова</string>
|
||||||
|
<string name="general_copy_action">Скапіраваць</string>
|
||||||
|
<string name="general_thank_you_label">Дзякуй</string>
|
||||||
|
<string name="general_value_not_available_label">Н/д</string>
|
||||||
|
<string name="general_grant_permission_action">Даць дазвол</string>
|
||||||
|
<string name="general_upgrade_action">Палепшыць</string>
|
||||||
|
<string name="general_check_action">Праверыць</string>
|
||||||
|
<string name="general_close_action">Закрыць</string>
|
||||||
|
<string name="debug_debuglog_size_label">Памер</string>
|
||||||
|
<string name="debug_debuglog_size_compressed_label">Сціснуты памер</string>
|
||||||
|
<string name="debug_notification_channel_label">Апавяшчэнні адладкі</string>
|
||||||
|
<string name="debug_debuglog_file_label">Файл журнала</string>
|
||||||
|
<string name="debug_debuglog_record_action">Запісаць лог аладкі ў журнал</string>
|
||||||
|
<string name="permission_bluetooth_connect_label">Злучэнне па Bluetooth</string>
|
||||||
|
<string name="permission_bluetooth_connect_description">Гэтай праграме патрабуецца дазвол на выкарыстанне Bluetooth для ўзаемадзеяння са спалучанымі прыладамі і стварэння новых злучэнняў.</string>
|
||||||
|
<string name="permission_bluetooth_scan_label">Сканіраванне Bluetooth</string>
|
||||||
|
<string name="permission_bluetooth_scan_description">Дазвол на сканіраванне Bluetooth дасць магчымасць гэтай праграме выяўляць і атрымліваць даныя па Bluetooth з навакольных прылад, такіх як вашы AirPods.</string>
|
||||||
|
<string name="permission_bluetooth_label">Bluetooth</string>
|
||||||
|
<string name="permission_bluetooth_description">Гэтай праграме патрабуецца дазвол на выкарыстанне Bluetooth для злучэння са спалучанымі прыладамі.</string>
|
||||||
|
<string name="permission_access_fine_location_label">Доступ да дакладнага месцазнаходжання</string>
|
||||||
|
</resources>
|
||||||
|
|||||||
@@ -1,2 +1,9 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources></resources>
|
<resources>
|
||||||
|
<string name="general_done_action">সম্পন্ন</string>
|
||||||
|
<string name="general_thank_you_label">ধন্যবাদ</string>
|
||||||
|
<string name="general_grant_permission_action">অনুমতি প্রদান করুন</string>
|
||||||
|
<string name="general_upgrade_action">আপগ্রেড</string>
|
||||||
|
<string name="debug_debuglog_size_label">আকার</string>
|
||||||
|
<string name="settings_general_label">সেটিংস</string>
|
||||||
|
</resources>
|
||||||
|
|||||||
@@ -64,7 +64,7 @@
|
|||||||
<string name="settings_general_description">Configuracions generals que afecten a tota l\'aplicació.</string>
|
<string name="settings_general_description">Configuracions generals que afecten a tota l\'aplicació.</string>
|
||||||
<string name="settings_acknowledgements_label">Agraïments</string>
|
<string name="settings_acknowledgements_label">Agraïments</string>
|
||||||
<string name="changelog_label">Registre de canvis</string>
|
<string name="changelog_label">Registre de canvis</string>
|
||||||
<string name="settings_support_email_developer_label">Correu del desenvolupador</string>
|
<string name="settings_support_email_developer_label" comment="settings_support_email_developer_label Pressing this setting will open your default email app with a template for a mail to me and some device info (e.g. versions).">Correu del desenvolupador</string>
|
||||||
<string name="settings_support_installid_label">ID d\'instal·lació</string>
|
<string name="settings_support_installid_label">ID d\'instal·lació</string>
|
||||||
<string name="settings_support_installid_desc">Els informes d\'error automàtics són anònims. Compartiu el vostre ID d\'instal·lació si el desenvolupador necessita trobar els vostres informes d\'error.</string>
|
<string name="settings_support_installid_desc">Els informes d\'error automàtics són anònims. Compartiu el vostre ID d\'instal·lació si el desenvolupador necessita trobar els vostres informes d\'error.</string>
|
||||||
<string name="settings_support_label">Suport</string>
|
<string name="settings_support_label">Suport</string>
|
||||||
@@ -81,7 +81,7 @@
|
|||||||
<string name="settings_monitor_mode_label">Mode monitor</string>
|
<string name="settings_monitor_mode_label">Mode monitor</string>
|
||||||
<string name="settings_monitor_mode_description">En quines circumstàncies aquesta aplicació supervisa les dades Bluetooth.</string>
|
<string name="settings_monitor_mode_description">En quines circumstàncies aquesta aplicació supervisa les dades Bluetooth.</string>
|
||||||
<string name="settings_scanner_mode_label">Mode escàner</string>
|
<string name="settings_scanner_mode_label">Mode escàner</string>
|
||||||
<string name="settings_scanner_mode_description">L\'escàner Bluetooth Low Energy hauria de prioritzar el rendiment o estalviar energia?</string>
|
<string name="settings_scanner_mode_description">L\'escàner de dades Bluetooth Low Energy hauria de prioritzar el rendiment o estalviar energia?</string>
|
||||||
<string name="settings_monitor_mode_manual_label">Quan l\'aplicació està oberta</string>
|
<string name="settings_monitor_mode_manual_label">Quan l\'aplicació està oberta</string>
|
||||||
<string name="settings_monitor_mode_automatic_label">Quan el dispositiu està connectat</string>
|
<string name="settings_monitor_mode_automatic_label">Quan el dispositiu està connectat</string>
|
||||||
<string name="settings_monitor_mode_always_label">Sempre</string>
|
<string name="settings_monitor_mode_always_label">Sempre</string>
|
||||||
@@ -89,13 +89,13 @@
|
|||||||
<string name="settings_scanner_mode_balanced_label">Equilibrat</string>
|
<string name="settings_scanner_mode_balanced_label">Equilibrat</string>
|
||||||
<string name="settings_scanner_mode_lowlatency_label">Baixa latència</string>
|
<string name="settings_scanner_mode_lowlatency_label">Baixa latència</string>
|
||||||
<string name="settings_autopause_label">Pausa automàtica</string>
|
<string name="settings_autopause_label">Pausa automàtica</string>
|
||||||
<string name="settings_autopause_description">Pausa la música quan ús tragueu el dispositiu de l\'orella.</string>
|
<string name="settings_autopause_description">Pausa l\'àudio quan ús tragueu el dispositiu de l\'orella.</string>
|
||||||
<string name="settings_showall_label">Mostra tots els dispositius</string>
|
<string name="settings_showall_label">Mostra tots els dispositius</string>
|
||||||
<string name="settings_showall_description">Mostra els dispositius d\'altres persones que estan a prop vostre.</string>
|
<string name="settings_showall_description">Mostra els dispositius d\'altres persones que estan a prop vostre.</string>
|
||||||
<string name="settings_autopplay_label">Reproducció automàtica</string>
|
<string name="settings_autopplay_label">Reproducció automàtica</string>
|
||||||
<string name="settings_autoplay_description">Inicia la reproducció de música quan ús poseu el dispositiu.</string>
|
<string name="settings_autoplay_description">Inicia la reproducció d\'àudio en utilitzar el dispositiu.</string>
|
||||||
<string name="settings_fake_data_label">Dades falses</string>
|
<string name="settings_fake_data_label">Dades falses</string>
|
||||||
<string name="settings_fake_data_description">Mostra dades falses. Per exemple: simula un dispositiu que no existeix.</string>
|
<string name="settings_fake_data_description">Mostra dades falses. Per exemple: simula dispositius que no existeixen.</string>
|
||||||
<string name="settings_debug_label">Configuració de depuració</string>
|
<string name="settings_debug_label">Configuració de depuració</string>
|
||||||
<string name="settings_debug_description">Configuració addicional per ajudar a resoldre problemes amb l\'aplicació.</string>
|
<string name="settings_debug_description">Configuració addicional per ajudar a resoldre problemes amb l\'aplicació.</string>
|
||||||
<string name="settings_signal_minimum_label">Qualitat de senyal mínima</string>
|
<string name="settings_signal_minimum_label">Qualitat de senyal mínima</string>
|
||||||
@@ -119,7 +119,6 @@
|
|||||||
<string name="upgrade_capod_description">Obteniu característiques addicionals i doneu suport al desenvolupador.</string>
|
<string name="upgrade_capod_description">Obteniu característiques addicionals i doneu suport al desenvolupador.</string>
|
||||||
<string name="settings_popup_caseopen_label">Mostra la finestra emergent</string>
|
<string name="settings_popup_caseopen_label">Mostra la finestra emergent</string>
|
||||||
<string name="settings_popup_caseopen_description">Mostra una finestra emergent quan s\'obre la funda del dispositiu (experimental).</string>
|
<string name="settings_popup_caseopen_description">Mostra una finestra emergent quan s\'obre la funda del dispositiu (experimental).</string>
|
||||||
<string name="notification_channel_reaction_popup_label">Reaccions emergents del dispositiu</string>
|
|
||||||
<string name="overview_bluetooth_disabled_label">El Bluetooth està desactivat</string>
|
<string name="overview_bluetooth_disabled_label">El Bluetooth està desactivat</string>
|
||||||
<string name="overview_bluetooth_disabled_description">El Bluetooth està desactivat, activeu-lo ;)</string>
|
<string name="overview_bluetooth_disabled_description">El Bluetooth està desactivat, activeu-lo ;)</string>
|
||||||
<string name="help_translate_label">Traducció</string>
|
<string name="help_translate_label">Traducció</string>
|
||||||
@@ -133,4 +132,8 @@
|
|||||||
<string name="settings_blescanner_unfiltered_label">Dades BLE sense filtrar</string>
|
<string name="settings_blescanner_unfiltered_label">Dades BLE sense filtrar</string>
|
||||||
<string name="settings_blescanner_unfiltered_description">Elimina els filtres de l\'escàner BLE per mostrar totes les dades BLE emeses. Útil per afegir suport a nous tipus d\'auriculars.</string>
|
<string name="settings_blescanner_unfiltered_description">Elimina els filtres de l\'escàner BLE per mostrar totes les dades BLE emeses. Útil per afegir suport a nous tipus d\'auriculars.</string>
|
||||||
<string name="permission_required_title">Cal el permís següent:</string>
|
<string name="permission_required_title">Cal el permís següent:</string>
|
||||||
|
<string name="settings_compatibility_mode_label">Mode de compatibilitat</string>
|
||||||
|
<string name="settings_compatibility_mode_description">Desactiva les optimitzacions per millorar la compatibilitat. Proveu això si no veieu cap dada.</string>
|
||||||
|
<string name="translators_thanks_title">Traductors</string>
|
||||||
|
<string name="translators_thanks_description" comment="translators_thanks_description You can voluntarily add your name here to have it displayed in the app. It will be shown in the Settings>Acknowledgements>Thank you section. Separate names by semicolons and an optional email may be included in brackets, example: darken(darken@darken.eu);John Doe(john@example.com) "darken" is just a placeholder and does not have to be kept if/when you add yours. Be nice to each other!">Jaime Muñoz(jmmartin_5@outlook.com)</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -64,7 +64,7 @@
|
|||||||
<string name="settings_general_description">Obecná vylepšení, která ovlivňují celou aplikaci.</string>
|
<string name="settings_general_description">Obecná vylepšení, která ovlivňují celou aplikaci.</string>
|
||||||
<string name="settings_acknowledgements_label">Poděkování</string>
|
<string name="settings_acknowledgements_label">Poděkování</string>
|
||||||
<string name="changelog_label">Seznam změn</string>
|
<string name="changelog_label">Seznam změn</string>
|
||||||
<string name="settings_support_email_developer_label">E-mail vývojáři</string>
|
<string name="settings_support_email_developer_label" comment="settings_support_email_developer_label Pressing this setting will open your default email app with a template for a mail to me and some device info (e.g. versions).">E-mail vývojáři</string>
|
||||||
<string name="settings_support_installid_label">ID instalace</string>
|
<string name="settings_support_installid_label">ID instalace</string>
|
||||||
<string name="settings_support_installid_desc">Automatická hlášení chyb jsou anonymní. Sdílíte pouze své ID instalace, pokud vývojář potřebuje najít vaše chybová hlášení.</string>
|
<string name="settings_support_installid_desc">Automatická hlášení chyb jsou anonymní. Sdílíte pouze své ID instalace, pokud vývojář potřebuje najít vaše chybová hlášení.</string>
|
||||||
<string name="settings_support_label">Podpora</string>
|
<string name="settings_support_label">Podpora</string>
|
||||||
@@ -89,11 +89,11 @@
|
|||||||
<string name="settings_scanner_mode_balanced_label">Vyvážený</string>
|
<string name="settings_scanner_mode_balanced_label">Vyvážený</string>
|
||||||
<string name="settings_scanner_mode_lowlatency_label">Nízká latence</string>
|
<string name="settings_scanner_mode_lowlatency_label">Nízká latence</string>
|
||||||
<string name="settings_autopause_label">Autom. pozastavení</string>
|
<string name="settings_autopause_label">Autom. pozastavení</string>
|
||||||
<string name="settings_autopause_description">Při vyjmutí zařízení z ucha pozastavit přehrávání hudby.</string>
|
<string name="settings_autopause_description">Pozastavení zvuku při vyjmutí zařízení z ucha.</string>
|
||||||
<string name="settings_showall_label">Zobrazit všechna zařízení</string>
|
<string name="settings_showall_label">Zobrazit všechna zařízení</string>
|
||||||
<string name="settings_showall_description">Zobrazit zařízení ostatních osob, které se nacházejí ve vaší blízkosti.</string>
|
<string name="settings_showall_description">Zobrazit zařízení ostatních osob, které se nacházejí ve vaší blízkosti.</string>
|
||||||
<string name="settings_autopplay_label">Autom. přehrávání</string>
|
<string name="settings_autopplay_label">Autom. přehrávání</string>
|
||||||
<string name="settings_autoplay_description">Spustit přehrávání hudby při použití zařízení.</string>
|
<string name="settings_autoplay_description">Spuštění přehrávání zvuku při nošení zařízení.</string>
|
||||||
<string name="settings_fake_data_label">Falešná data</string>
|
<string name="settings_fake_data_label">Falešná data</string>
|
||||||
<string name="settings_fake_data_description">Zobrazit falešná data např. simulovat zařízení, které neexistuje.</string>
|
<string name="settings_fake_data_description">Zobrazit falešná data např. simulovat zařízení, které neexistuje.</string>
|
||||||
<string name="settings_debug_label">Nastavení ladění</string>
|
<string name="settings_debug_label">Nastavení ladění</string>
|
||||||
@@ -119,7 +119,6 @@
|
|||||||
<string name="upgrade_capod_description">Získat další funkce a podpořit vývojáře.</string>
|
<string name="upgrade_capod_description">Získat další funkce a podpořit vývojáře.</string>
|
||||||
<string name="settings_popup_caseopen_label">Zobrazit vyskakovací okno</string>
|
<string name="settings_popup_caseopen_label">Zobrazit vyskakovací okno</string>
|
||||||
<string name="settings_popup_caseopen_description">Zobrazit vyskakovací okno, pokud je pouzdro zařízení otevřeno (experimentální).</string>
|
<string name="settings_popup_caseopen_description">Zobrazit vyskakovací okno, pokud je pouzdro zařízení otevřeno (experimentální).</string>
|
||||||
<string name="notification_channel_reaction_popup_label">Vyskakovací okno reakcí zařízení</string>
|
|
||||||
<string name="overview_bluetooth_disabled_label">Bluetooth je vypnuto</string>
|
<string name="overview_bluetooth_disabled_label">Bluetooth je vypnuto</string>
|
||||||
<string name="overview_bluetooth_disabled_description">Bluetooth je vypnuto, zapněte jej ;)</string>
|
<string name="overview_bluetooth_disabled_description">Bluetooth je vypnuto, zapněte jej ;)</string>
|
||||||
<string name="help_translate_label">Překlad</string>
|
<string name="help_translate_label">Překlad</string>
|
||||||
@@ -133,4 +132,8 @@
|
|||||||
<string name="settings_blescanner_unfiltered_label">Nefiltrovaná data BLE</string>
|
<string name="settings_blescanner_unfiltered_label">Nefiltrovaná data BLE</string>
|
||||||
<string name="settings_blescanner_unfiltered_description">Aby se zobrazovala všechna vysílaná data BLE, odstraňte ze skenování BLE všechny filtry. Užitečné pro přidání podpory nových typů sluchátek.</string>
|
<string name="settings_blescanner_unfiltered_description">Aby se zobrazovala všechna vysílaná data BLE, odstraňte ze skenování BLE všechny filtry. Užitečné pro přidání podpory nových typů sluchátek.</string>
|
||||||
<string name="permission_required_title">Je vyžadováno následující oprávnění:</string>
|
<string name="permission_required_title">Je vyžadováno následující oprávnění:</string>
|
||||||
|
<string name="settings_compatibility_mode_label">Režim kompatibility</string>
|
||||||
|
<string name="settings_compatibility_mode_description">Vypnutí optimalizace pro zlepšení kompatibility. Zkuste to, pokud se vám nezobrazují žádná data.</string>
|
||||||
|
<string name="translators_thanks_title">Překladatelé</string>
|
||||||
|
<string name="translators_thanks_description" comment="translators_thanks_description You can voluntarily add your name here to have it displayed in the app. It will be shown in the Settings>Acknowledgements>Thank you section. Separate names by semicolons and an optional email may be included in brackets, example: darken(darken@darken.eu);John Doe(john@example.com) "darken" is just a placeholder and does not have to be kept if/when you add yours. Be nice to each other!">novas78@xda; woytazzer</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -64,7 +64,7 @@
|
|||||||
<string name="settings_general_description">Allgemeine Optimierungen, die die gesamte App betreffen.</string>
|
<string name="settings_general_description">Allgemeine Optimierungen, die die gesamte App betreffen.</string>
|
||||||
<string name="settings_acknowledgements_label">Danksagungen</string>
|
<string name="settings_acknowledgements_label">Danksagungen</string>
|
||||||
<string name="changelog_label">Änderungsprotokoll</string>
|
<string name="changelog_label">Änderungsprotokoll</string>
|
||||||
<string name="settings_support_email_developer_label">E-Mail-Entwickler</string>
|
<string name="settings_support_email_developer_label" comment="settings_support_email_developer_label Pressing this setting will open your default email app with a template for a mail to me and some device info (e.g. versions).">E-Mail-Entwickler</string>
|
||||||
<string name="settings_support_installid_label">ID installieren</string>
|
<string name="settings_support_installid_label">ID installieren</string>
|
||||||
<string name="settings_support_installid_desc">Automatische Fehlermeldungen sind anonym. Teilen Sie Ihre Installations- ID mit, wenn der Entwickler Ihre Fehlerberichte finden muss.</string>
|
<string name="settings_support_installid_desc">Automatische Fehlermeldungen sind anonym. Teilen Sie Ihre Installations- ID mit, wenn der Entwickler Ihre Fehlerberichte finden muss.</string>
|
||||||
<string name="settings_support_label">Unterstützung</string>
|
<string name="settings_support_label">Unterstützung</string>
|
||||||
@@ -81,7 +81,7 @@
|
|||||||
<string name="settings_monitor_mode_label">Überwachungsmodus</string>
|
<string name="settings_monitor_mode_label">Überwachungsmodus</string>
|
||||||
<string name="settings_monitor_mode_description">Unter welchen Umständen überwacht diese App Bluetooth-Daten.</string>
|
<string name="settings_monitor_mode_description">Unter welchen Umständen überwacht diese App Bluetooth-Daten.</string>
|
||||||
<string name="settings_scanner_mode_label">Scannermodus</string>
|
<string name="settings_scanner_mode_label">Scannermodus</string>
|
||||||
<string name="settings_scanner_mode_description">Soll der Bluetooth Low Energie-Scanner Leistung priorisieren oder Energie sparen?</string>
|
<string name="settings_scanner_mode_description">Soll der Bluetooth Low Energy Datenscanner Leistung priorisieren oder Energie sparen?</string>
|
||||||
<string name="settings_monitor_mode_manual_label">Wenn die App geöffnet ist</string>
|
<string name="settings_monitor_mode_manual_label">Wenn die App geöffnet ist</string>
|
||||||
<string name="settings_monitor_mode_automatic_label">Wenn das Gerät verbunden ist</string>
|
<string name="settings_monitor_mode_automatic_label">Wenn das Gerät verbunden ist</string>
|
||||||
<string name="settings_monitor_mode_always_label">Stets</string>
|
<string name="settings_monitor_mode_always_label">Stets</string>
|
||||||
@@ -89,13 +89,13 @@
|
|||||||
<string name="settings_scanner_mode_balanced_label">Ausgewogen</string>
|
<string name="settings_scanner_mode_balanced_label">Ausgewogen</string>
|
||||||
<string name="settings_scanner_mode_lowlatency_label">Geringe Latenz</string>
|
<string name="settings_scanner_mode_lowlatency_label">Geringe Latenz</string>
|
||||||
<string name="settings_autopause_label">Automatische Pause</string>
|
<string name="settings_autopause_label">Automatische Pause</string>
|
||||||
<string name="settings_autopause_description">Halten Sie die Musik an, wenn Sie das Gerät von Ihrem Ohr entfernen.</string>
|
<string name="settings_autopause_description">Halten Sie den Ton an, wenn Sie das Gerät von Ihrem Ohr entfernen.</string>
|
||||||
<string name="settings_showall_label">Alle Geräte anzeigen</string>
|
<string name="settings_showall_label">Alle Geräte anzeigen</string>
|
||||||
<string name="settings_showall_description">Zeigen Sie die Geräte anderer Personen in Ihrer Nähe an.</string>
|
<string name="settings_showall_description">Zeigen Sie die Geräte anderer Personen in Ihrer Nähe an.</string>
|
||||||
<string name="settings_autopplay_label">Automatisches Abspielen</string>
|
<string name="settings_autopplay_label">Automatisches Abspielen</string>
|
||||||
<string name="settings_autoplay_description">Starten Sie die Musikwiedergabe, wenn Sie das Gerät tragen.</string>
|
<string name="settings_autoplay_description">Starten Sie die Audiowiedergabe, wenn das Gerät getragen wird.</string>
|
||||||
<string name="settings_fake_data_label">Gefälschte Daten</string>
|
<string name="settings_fake_data_label">Gefälschte Daten</string>
|
||||||
<string name="settings_fake_data_description">Gefälschte Daten anzeigen, Geräte simulieren, die nicht existieren.</string>
|
<string name="settings_fake_data_description">Falsche Daten anzeigen, also nicht existierende Geräte simulieren.</string>
|
||||||
<string name="settings_debug_label">Debug-Einstellungen</string>
|
<string name="settings_debug_label">Debug-Einstellungen</string>
|
||||||
<string name="settings_debug_description">Zusätzliche Einstellungen zur Behebung von Problemen mit der App.</string>
|
<string name="settings_debug_description">Zusätzliche Einstellungen zur Behebung von Problemen mit der App.</string>
|
||||||
<string name="settings_signal_minimum_label">Minimale Signalqualität</string>
|
<string name="settings_signal_minimum_label">Minimale Signalqualität</string>
|
||||||
@@ -119,11 +119,10 @@
|
|||||||
<string name="upgrade_capod_description">Erhalten Sie zusätzliche Funktionen und unterstützen Sie den Entwickler.</string>
|
<string name="upgrade_capod_description">Erhalten Sie zusätzliche Funktionen und unterstützen Sie den Entwickler.</string>
|
||||||
<string name="settings_popup_caseopen_label">Popup zeigen</string>
|
<string name="settings_popup_caseopen_label">Popup zeigen</string>
|
||||||
<string name="settings_popup_caseopen_description">Ein Popup anzeigen, wenn das Gerätegehäuse geöffnet wird (experimentell).</string>
|
<string name="settings_popup_caseopen_description">Ein Popup anzeigen, wenn das Gerätegehäuse geöffnet wird (experimentell).</string>
|
||||||
<string name="notification_channel_reaction_popup_label">Popup-Gerätereaktionen</string>
|
|
||||||
<string name="overview_bluetooth_disabled_label">Bluetooth ist deaktiviert</string>
|
<string name="overview_bluetooth_disabled_label">Bluetooth ist deaktiviert</string>
|
||||||
<string name="overview_bluetooth_disabled_description">Bluetooth ist deaktiviert, aktiviere es ;)</string>
|
<string name="overview_bluetooth_disabled_description">Bluetooth ist deaktiviert, aktiviere es ;)</string>
|
||||||
<string name="help_translate_label">Übersetzung</string>
|
<string name="help_translate_label">Übersetzung</string>
|
||||||
<string name="help_translate_description">Helfen Sie mit, dies in Ihre Lieblingssprache zu übersetzen.</string>
|
<string name="help_translate_description">Helfen Sie mit, diese App in Ihre Lieblingssprache zu übersetzen.</string>
|
||||||
<string name="settings_onepod_mode_label">Ein-Pod-Modus</string>
|
<string name="settings_onepod_mode_label">Ein-Pod-Modus</string>
|
||||||
<string name="settings_onepod_mode_description">Das Tragen beider Pods ist nicht erforderlich, das Tragen eines einzigen Pods reicht aus, um Reaktionen auszulösen.</string>
|
<string name="settings_onepod_mode_description">Das Tragen beider Pods ist nicht erforderlich, das Tragen eines einzigen Pods reicht aus, um Reaktionen auszulösen.</string>
|
||||||
<string name="permission_system_alert_window_label">Systemwarnungsfenster</string>
|
<string name="permission_system_alert_window_label">Systemwarnungsfenster</string>
|
||||||
@@ -133,4 +132,8 @@
|
|||||||
<string name="settings_blescanner_unfiltered_label">Ungefilterte BLE-Daten</string>
|
<string name="settings_blescanner_unfiltered_label">Ungefilterte BLE-Daten</string>
|
||||||
<string name="settings_blescanner_unfiltered_description">Entfernen Sie alle Filter aus dem BLE-Scanner, um alle übertragenen BLE-Daten anzuzeigen. Nützlich, um Unterstützung für neue Kopfhörertypen hinzuzufügen.</string>
|
<string name="settings_blescanner_unfiltered_description">Entfernen Sie alle Filter aus dem BLE-Scanner, um alle übertragenen BLE-Daten anzuzeigen. Nützlich, um Unterstützung für neue Kopfhörertypen hinzuzufügen.</string>
|
||||||
<string name="permission_required_title">Die folgende Genehmigung ist erforderlich:</string>
|
<string name="permission_required_title">Die folgende Genehmigung ist erforderlich:</string>
|
||||||
|
<string name="settings_compatibility_mode_label">Kompatibilitätsmodus</string>
|
||||||
|
<string name="settings_compatibility_mode_description">Deaktivieren Sie Optimierungen, um die Kompatibilität zu verbessern. Versuchen Sie dies, wenn Sie keine Daten sehen.</string>
|
||||||
|
<string name="translators_thanks_title">Übersetzer</string>
|
||||||
|
<string name="translators_thanks_description" comment="translators_thanks_description You can voluntarily add your name here to have it displayed in the app. It will be shown in the Settings>Acknowledgements>Thank you section. Separate names by semicolons and an optional email may be included in brackets, example: darken(darken@darken.eu);John Doe(john@example.com) "darken" is just a placeholder and does not have to be kept if/when you add yours. Be nice to each other!">Gamechanger181</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user