Compare commits

...
184 Commits
Author SHA1 Message Date
darken 84b7509f33 Adjust versioning for GPlay wear os bundle upload 2022-09-14 19:39:30 +02:00
darken a53baec9dc Version bump 2022-09-14 18:59:08 +02:00
darken 4b4bc86cf7 Fix Github release for WearOS APK 2022-09-14 18:31:34 +02:00
darken 4d3860089e Add fastlane wear screenshots 2022-09-14 18:00:18 +02:00
darken 758f55f13c Update README.md 2022-09-14 18:00:18 +02:00
darken 1e191c3adc More translation updates 2022-09-14 18:00:18 +02:00
darken 1ba2cf7c9d Sync translations with Crowdin (remove duplicate strings between modules) 2022-09-14 18:00:18 +02:00
darken 9ed1cc00d5 Fix crowdin configuration 2022-09-14 18:00:18 +02:00
darken 3be1173197 Tweak dual pods wear layout 2022-09-14 18:00:18 +02:00
darken f6e69c62b1 Clean up strings 2022-09-14 18:00:18 +02:00
darken 6d9679a7fc More cleanup 2022-09-14 18:00:18 +02:00
darken 6ea260a81f More resource cleanup 2022-09-14 18:00:18 +02:00
darken 26da600761 Fix module packagename 2022-09-14 18:00:18 +02:00
darken 33172a7aee Use the default low_latency mode for wearOS too, we want to get as many BLEScanRecords as possible in a short time frame and then sleep again. 2022-09-14 18:00:18 +02:00
darken 7eb9f6ac95 Fix failing test 2022-09-14 18:00:18 +02:00
darken fc06c285d5 Fix periodic checks aborting with no data. 2022-09-14 18:00:18 +02:00
darken ca2e4fec79 Implement periodic checks for wear 2022-09-14 18:00:18 +02:00
darken 87ca90f6cc Fix "last seen at". 2022-09-14 18:00:18 +02:00
darken 7dfea78c87 Simplify WearOS app because this is such a cluster fuck and I've ran out of motivation. 2022-09-14 18:00:18 +02:00
darken d7808009dc Tweak main overview 2022-09-14 18:00:18 +02:00
darken 62afb7a130 Cache main pod device 2022-09-14 18:00:18 +02:00
darken 87bef9cb9c Icon and reintroduce splash screen 2022-09-14 18:00:18 +02:00
darken 3860cb21c6 Code fluff 2022-09-14 18:00:18 +02:00
darken bf4ad7886c Improve wear os overview screen 2022-09-14 18:00:18 +02:00
darken 1d51c207f4 Basic working data display in wear-app 2022-09-14 18:00:18 +02:00
darken 2eb3f70e0c Wear UI wip 2022-09-14 18:00:18 +02:00
darken eb4d883194 More refactoring and modularization 2022-09-14 18:00:18 +02:00
darken 3f5c9ff9de Fix reaction settings fragment crash 2022-09-14 18:00:18 +02:00
darken 47039796de Nicer text display if we don't use raw github 2022-09-14 18:00:18 +02:00
darken fd054abcca Add codestyle 2022-09-14 18:00:18 +02:00
darken 2861bfbd47 Fix worker inject 2022-09-14 18:00:18 +02:00
darken 6ea3127f9c More refactoring to split up modules, both compile.
Wear is still WIP.
2022-09-14 18:00:18 +02:00
darken f7acb5e115 Fix copy paste regressions 2022-09-14 18:00:18 +02:00
darken b1ad1f7699 Cleanup 2022-09-14 18:00:18 +02:00
darken ab713df3c0 Fix warnings 2022-09-14 18:00:18 +02:00
darken e0d44f2f1c Use Kotlin for Gradle DSL 2022-09-14 18:00:18 +02:00
darken 61463bc05f More refactoring 2022-09-14 18:00:18 +02:00
darken a219fc3527 Fix build issues 2022-09-14 18:00:18 +02:00
darken 1cd4028ce8 Adjust crowdin config 2022-09-14 18:00:18 +02:00
darken de9a30eb93 Refactoring 2022-09-14 18:00:18 +02:00
darken 2210561af3 Refactoring 2022-09-14 18:00:18 +02:00
darken c5ddc474c4 Update translations 2022-09-14 18:00:18 +02:00
darken 688748da36 Go multi-module: app-common, app-phone, app-wear 2022-09-14 18:00:18 +02:00
darken 753d311593 Tweak versioning and bump (1.3.14-rc0) 2022-08-24 11:56:42 +02:00
darken 5bae3d9931 Remove in-app email shortcut 2022-08-24 11:40:16 +02:00
darken a3495a8218 Update translations 2022-08-24 11:33:48 +02:00
Poussinou 5fa68f77a2 Update README.md 2022-07-31 11:13:51 +02:00
darken dbba2e9791 Another fastlane gplay upload fix. 2022-07-25 21:00:43 +02:00
darken 6207f26a68 Update fastlane to 2.208 2022-07-25 20:57:24 +02:00
darken 7fcd39822e Update fastlane to 2.208 2022-07-25 20:56:45 +02:00
darken d20f8a604f Fix CI issues with Google Play upload (part 2...) 2022-07-25 20:47:26 +02:00
darken a742646127 Fix CI github release apk search path. 2022-07-25 20:09:37 +02:00
darken f5078d5ef2 Tell fastlane to skip APK upload 2022-07-25 20:04:22 +02:00
darken 3784084abc Fix fastlane build arguments 2022-07-25 19:53:23 +02:00
darken 2f463b7735 Adjust description to pass markdown parsing 2022-07-25 19:42:15 +02:00
darken 7be5625d66 Fix gplay deploy script 2022-07-25 19:40:49 +02:00
darken aef14a9cd2 Create changelog file for v1.3.13(1031300) 2022-07-25 17:42:26 +02:00
darken 67648a04d3 Update translations 2022-07-25 17:36:29 +02:00
Matthias Urhahn 58345e3194 Update FUNDING.yml 2022-07-25 10:22:32 +02:00
darken 4cb2ad9a5a Adjust support item order 2022-07-25 01:29:18 +02:00
darken 27395d8e22 Remove email from readme 2022-07-25 01:29:07 +02:00
darken 230c5ea180 Adjust donation links in foss build 2022-07-25 01:22:07 +02:00
Matthias Urhahn 4d4370ddce Update README.md 2022-07-24 16:01:07 +02:00
darken 7e913e7847 Make bugsnag opt-out in gplay and opt-in in foss builds. 2022-07-24 16:00:38 +02:00
darken ec7871e4e9 Change issue tracker icon 2022-07-24 16:00:07 +02:00
darken cc5a45a14f Update repo links 2022-07-24 16:00:07 +02:00
Matthias Urhahn 5fff3457ba Update README.md 2022-07-24 15:03:19 +02:00
darken 6f5d66c1d9 Update README.md 2022-07-24 14:02:53 +02:00
darken d94795bf4f Update PRIVACY_POLICY.md 2022-07-24 13:58:17 +02:00
darken 009b71c42a Update PP 2022-07-24 13:43:47 +02:00
darken a10716d74e Update gitignore 2022-07-24 13:43:06 +02:00
darken b44bf5f401 Setup CI for testing, gplay release and foss release. 2022-07-24 13:38:52 +02:00
Matthias Urhahn e0af1838e2 Delete local.properties 2022-07-24 13:38:43 +02:00
darken 7d5502a34f Version bump (1.3.13) 2022-07-08 19:27:28 +02:00
darken 78ccddf87c Fix notification icon/text color to match light/dark theme. 2022-05-01 17:48:32 +02:00
darken df1bc48e84 Version bump (1.3.12) 2022-05-01 17:36:17 +02:00
darken 3b48d61571 Version bump (1.3.11) 2022-05-01 17:17:53 +02:00
darken 92d5e2a40d Updated translations 2022-05-01 17:13:42 +02:00
darken 234857c414 When enabling unfiltered BLE data, also enable "show all" to make debugging easier. 2022-05-01 16:42:17 +02:00
darken b1bbe785ea Fix debug log naming 2022-04-18 11:10:10 +02:00
darken 487fb49bff Update translations 2022-04-17 13:27:23 +02:00
darken b14b7fa926 Version bump (1.3.10) 2022-04-17 13:08:42 +02:00
darken 4cc1ab143d Change fake airpods pro name. 2022-04-17 13:07:27 +02:00
darken c23de67cd2 Fix ANR 2022-04-09 14:08:07 +02:00
darken e9bb4868c8 Fix NPE on some Android 9 ROMs due to bug in AOSP 2022-04-09 13:54:14 +02:00
darken ae6fd0eabd Updated translations 2022-04-09 11:43:21 +02:00
darken f4f73906bc Version bump (1.3.9) 2022-04-08 23:24:28 +02:00
darken c437decbbf Add VarunrAirPodsPro.kt 2022-04-08 23:18:35 +02:00
darken 1d36677ead Updated translations 2022-02-16 20:03:59 +01:00
darken 542b69ca92 Add acknowledgement entry for translator names. 2022-02-16 19:58:52 +01:00
darken 8547c9c396 Version bump (1.3.7) 2022-02-13 21:52:53 +01:00
darken 4e258e65c1 Fix paddings, cards less information were unnecessarily large. 2022-02-13 21:46:12 +01:00
darken 9f49b470fe Refactor interfaces to "single" and "dual" pods that each have one or more extra features. 2022-02-13 20:48:34 +01:00
darken 4a71cfe65a Version bump (1.3.7)? 2022-02-12 09:31:51 +01:00
darken d06959f964 Update fastlane scripts 2022-02-12 09:31:06 +01:00
darken 8c339ca2eb Shorten too long titles, replacing with EN for now 2022-02-12 09:30:48 +01:00
darken 3aab53566d Updated translations 2022-02-12 07:53:45 +01:00
darken 749cfed137 Remove unknown device locking, wasn't actually that useful. 2022-02-12 07:53:07 +01:00
darken ce25ad6711 Add support for Tws i99999 2022-02-12 07:45:47 +01:00
darken 568aeec0d5 Improve logging 2022-02-10 13:09:45 +01:00
darken 6b02fd66e6 Version bump (1.3.6) 2022-02-10 10:51:48 +01:00
darken 0e313004ac Updated translations 2022-02-10 10:39:39 +01:00
darken 270de8702b Improve source strings based on AO's feedback. 2022-02-10 10:27:07 +01:00
darken 324878d68d Add compatibility mode. 2022-02-10 10:19:07 +01:00
darken 9b39240016 Remove unused string 2022-02-10 08:11:24 +01:00
darken 4d6f185b58 Fix unfiltered mode only showing apple devices. 2022-02-10 07:01:10 +01:00
darken 5979081928 Version bump (1.3.5) 2022-02-04 06:59:57 +01:00
darken 0d852675cd Update translations 2022-02-04 06:57:05 +01:00
darken 29f9d9e4be Fix string placeholders. 2022-02-03 09:35:39 +01:00
darken 59c0e818e1 Remove generic unknown value label and replace it with more specific variants. 2022-02-03 09:34:55 +01:00
darken 6d6647e511 Fix package 2022-02-02 18:04:43 +01:00
darken 612acd6d72 Fix case 2022-02-02 11:52:01 +01:00
darken 425a9f321b Version bump (1.3.4) 2022-02-02 08:56:44 +01:00
darken 55ccfa27b2 Update translations 2022-02-02 08:50:57 +01:00
darken 7029263622 Fix untranslated string 2022-02-02 08:43:50 +01:00
darken 6153cae3fd Update translations 2022-02-02 08:36:27 +01:00
darken 7f78f2599e Fix translation issues. 2022-02-01 20:28:10 +01:00
darken 3f05802483 Add missing source string 2022-02-01 20:08:11 +01:00
darken b5d0b47c77 Version bump (1.3.3) 2022-01-31 22:28:19 +01:00
darken 52415cea0d Show unknown devices of all kinds. 2022-01-31 22:23:11 +01:00
darken 72448e678f Update translations. 2022-01-31 20:54:44 +01:00
darken e95d0b59ac Add foss and gplay variant to Crowdin 2022-01-31 20:40:08 +01:00
darken 36650aa938 Update translations 2022-01-31 20:31:33 +01:00
darken b2693c6e57 Add option for unfiltered BLE scan. 2022-01-31 19:20:39 +01:00
darken 08b76f3280 Fix string issues (crowdin feedback) 2022-01-31 16:58:19 +01:00
darken 51513270ea Version bump 2022-01-29 20:33:07 +01:00
darken b4d9316665 Add translation scalfolding. 2022-01-29 20:26:15 +01:00
darken a234ac0dd2 Add crowdin scripts 2022-01-29 20:23:06 +01:00
darken 3fc18bc40b Improve sorting and heuristics. 2022-01-29 20:00:04 +01:00
darken 0f924ecb82 Track first time a device is seen and display it. 2022-01-29 18:00:13 +01:00
darken 6bd350ce42 Refactor fake data into extra class 2022-01-29 16:41:50 +01:00
darken e1b4ab14e4 Version bump 2022-01-27 23:01:01 +01:00
darken adef0b39e9 Version bump (1.3.0) 2022-01-27 22:49:51 +01:00
darken af12a0e5cb Improved signal quality heuristic and value caching. 2022-01-27 22:45:47 +01:00
darken 90fc2fac24 Improve sorting 2022-01-27 00:46:06 +01:00
darken 3be7d8b43f Improve logging 2022-01-26 18:29:02 +01:00
darken a35d8c57b4 Improve device sorting and main device determination 2022-01-26 18:17:52 +01:00
darken f792431710 Fluff 2022-01-26 18:17:35 +01:00
darken 7ff20acfc0 When bluetooth is disabled, stop emissions. 2022-01-26 14:50:48 +01:00
darken 8e20e0e108 Don't report cancellation exceptions. 2022-01-26 14:33:56 +01:00
darken a6fda04640 Fix handling of Bluetooth enable/disable and restarting the monitor. 2022-01-26 13:11:01 +01:00
darken 0bbcfb5e88 Code fluff 2022-01-26 13:09:15 +01:00
darken e79ba6bfe5 Version bump (1.2.1) 2022-01-26 12:53:13 +01:00
darken 369b0207c4 Version bump (1.2.0) 2022-01-26 12:39:30 +01:00
darken ce737b45b9 Add cooldown mechanism to lid reactions. 2022-01-26 12:28:23 +01:00
darken cde3afdcd9 Average RSSI 2022-01-26 01:02:21 +01:00
darken 07b3f4320e Remove test logging 2022-01-26 00:09:30 +01:00
darken e1c71cf6b6 Improve sorting 2022-01-26 00:08:51 +01:00
darken c233cd2445 Sort main pod up to the top. 2022-01-25 22:56:24 +01:00
darken 8f609ca989 Handle ear detection and microphone detection for dual pods better. 2022-01-25 22:54:53 +01:00
darken 2de2369600 Fix popup window overdraw 2022-01-25 12:06:09 +01:00
darken d5d2ae784c Add popup via system alert window. 2022-01-25 09:59:57 +01:00
darken 56bedb64c4 Only request "access background" when monitor mode is always. 2022-01-24 20:17:01 +01:00
darken 3b48ca90d3 Make low latency the default scanmode for better userexperience. 2022-01-24 15:43:19 +01:00
darken 272ecc006f Better logging of unknown devices. 2022-01-24 15:42:31 +01:00
darken ddacfa6841 Don't report fake device data. 2022-01-24 15:36:10 +01:00
darken c5b1c417a4 Add one-pod mode for reactions 2022-01-24 15:29:05 +01:00
darken 38bc3be5b7 Improve layouts 2022-01-24 15:10:30 +01:00
darken 57cd823b63 Improve notification layouts 2022-01-23 20:52:39 +01:00
darken 6cd799130e Clean up strings 2022-01-23 20:16:29 +01:00
darken a1c58e0a32 Improve single and basic layout. 2022-01-23 20:08:34 +01:00
darken 9cd4cea63d Improve dual pods layout 2022-01-23 19:04:23 +01:00
darken 1d5e13cc24 Improve permission texts, privacy policy link and layouts. 2022-01-21 12:18:59 +01:00
darken 6d2c4ba8ca Improve permission texts. 2022-01-21 00:05:00 +01:00
darken 357baa12e6 Improve permission texts. 2022-01-20 23:21:54 +01:00
darken 909a9ba194 Version bump 2022-01-20 23:07:23 +01:00
darken 79f68c3a0d Add more privacy policy... 2022-01-20 23:07:07 +01:00
darken 5eb97cc966 Version bump (1.1.0) 2022-01-20 17:11:58 +01:00
darken e4f405ec65 Fix unit test workflow 2022-01-20 17:10:34 +01:00
darken f392df05f9 Filter emissions to only react to distinct items. 2022-01-20 17:01:05 +01:00
darken 0f8e441a92 Fix crash on profile service disconnect 2022-01-20 16:07:11 +01:00
darken edbc944724 Reduce logspam 2022-01-20 16:06:58 +01:00
darken 65eb92dcb1 Reduce logspam 2022-01-20 16:06:47 +01:00
darken 7df24f9c76 Fix device cards being shown even if Bluetooth is disabled. 2022-01-20 15:52:12 +01:00
darken 5d72d0116b Fix Bluetooth Profile triggering illegal state exception due to already resumed coroutine when the profile service disconnects. 2022-01-20 15:49:49 +01:00
darken 91a16f55d9 Add translation link to Crowdin and fix capod private repo links. 2022-01-20 15:20:54 +01:00
darken 9ae3516a83 Add fastlane files. 2022-01-20 15:12:19 +01:00
darken f3c20caa13 Add GitHub workflows 2022-01-20 15:12:05 +01:00
darken 680bcbad2a Update internal readme. 2022-01-20 15:11:52 +01:00
darken 5f60f2b5eb Fix lid detection tests 2022-01-20 15:11:37 +01:00
darken 7efe6619d2 Merge remote-tracking branch 'origin/main' into main 2022-01-19 19:40:43 +01:00
Matthias Urhahn 16a880a92d Update README.md 2022-01-19 17:23:46 +01:00
Matthias Urhahn 79db1e2412 Update README.md 2022-01-19 17:20:11 +01:00
Matthias Urhahn b6e8d69eff Update README.md 2022-01-19 16:56:58 +01:00
1010 changed files with 15538 additions and 3718 deletions
+2 -1
View File
@@ -1,4 +1,5 @@
# These are supported funding model platforms # These are supported funding model platforms
github:
- d4rken
custom: custom:
- "https://www.buymeacoffee.com/tydarken" - "https://www.buymeacoffee.com/tydarken"
+34
View File
@@ -0,0 +1,34 @@
name: Code tests & eval
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build-and-test:
name: Build and test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up JDK 11
uses: actions/setup-java@v2
with:
java-version: '11'
distribution: 'adopt'
cache: gradle
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Build FOSS variant
run: ./gradlew assembleFossDebug
- 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,17 @@
name: "Validate Gradle Wrapper"
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
validation:
name: "Validation"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: gradle/wrapper-validation-action@v1
+157
View File
@@ -0,0 +1,157 @@
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/foss/beta/*.apk
app-wear/build/outputs/apk/foss/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/foss/release/*.apk
app-wear/build/outputs/apk/foss/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 }}
+13 -2
View File
@@ -1,3 +1,14 @@
.idea *.iml
.gradle .gradle
build/ /local.properties
.DS_Store
/build
/captures
.externalNativeBuild
.cxx
/.idea/**/*
!/.idea/codeStyles/
!/.idea/codeStyles/**/*
*.jks
.local/*
/fastlane/report.xml
+167
View File
@@ -0,0 +1,167 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<option name="RIGHT_MARGIN" value="120" />
<JSON>
<option name="OBJECT_WRAPPING" value="1" />
<option name="ARRAY_WRAPPING" value="1" />
</JSON>
<JavaCodeStyleSettings>
<option name="DO_NOT_WRAP_AFTER_SINGLE_ANNOTATION" value="true" />
<option name="IMPORT_LAYOUT_TABLE">
<value>
<package name="android" withSubpackages="true" static="false" />
<emptyLine />
<package name="com" withSubpackages="true" static="false" />
<emptyLine />
<package name="junit" withSubpackages="true" static="false" />
<emptyLine />
<package name="net" withSubpackages="true" static="false" />
<emptyLine />
<package name="org" withSubpackages="true" static="false" />
<emptyLine />
<package name="java" withSubpackages="true" static="false" />
<emptyLine />
<package name="javax" withSubpackages="true" static="false" />
<emptyLine />
<package name="" withSubpackages="true" static="false" />
<emptyLine />
<package name="" withSubpackages="true" static="true" />
<emptyLine />
</value>
</option>
</JavaCodeStyleSettings>
<JetCodeStyleSettings>
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings>
<codeStyleSettings language="JAVA">
<option name="KEEP_SIMPLE_BLOCKS_IN_ONE_LINE" value="true" />
<option name="KEEP_SIMPLE_METHODS_IN_ONE_LINE" value="true" />
<option name="KEEP_SIMPLE_LAMBDAS_IN_ONE_LINE" value="true" />
<option name="KEEP_SIMPLE_CLASSES_IN_ONE_LINE" value="true" />
<option name="FIELD_ANNOTATION_WRAP" value="1" />
</codeStyleSettings>
<codeStyleSettings language="JSON">
<indentOptions>
<option name="USE_TAB_CHARACTER" value="true" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="XML">
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions>
<arrangement>
<rules>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:android</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:id</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>style</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>ANDROID_ATTRIBUTE_ORDER</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>.*</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
</rules>
</arrangement>
</codeStyleSettings>
<codeStyleSettings language="kotlin">
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
<option name="METHOD_ANNOTATION_WRAP" value="1" />
<option name="CLASS_ANNOTATION_WRAP" value="1" />
<option name="FIELD_ANNOTATION_WRAP" value="1" />
</codeStyleSettings>
</code_scheme>
</component>
+5
View File
@@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>
+3
View File
@@ -0,0 +1,3 @@
source "https://rubygems.org"
gem "fastlane"
+218
View File
@@ -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.610.0)
aws-sdk-core (3.131.3)
aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.525.0)
aws-sigv4 (~> 1.1)
jmespath (~> 1, >= 1.6.1)
aws-sdk-kms (1.58.0)
aws-sdk-core (~> 3, >= 3.127.0)
aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.114.0)
aws-sdk-core (~> 3, >= 3.127.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.4)
aws-sigv4 (1.5.1)
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.92.4)
faraday (1.10.0)
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.4)
multipart-post (~> 2)
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.208.0)
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.25.0)
google-apis-core (>= 0.7, < 2.a)
google-apis-core (0.7.0)
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.13.0)
google-apis-core (>= 0.7, < 2.a)
google-apis-playcustomapp_v1 (0.10.0)
google-apis-core (>= 0.7, < 2.a)
google-apis-storage_v1 (0.18.0)
google-apis-core (>= 0.7, < 2.a)
google-cloud-core (1.6.0)
google-cloud-env (~> 1.0)
google-cloud-errors (~> 1.0)
google-cloud-env (1.6.0)
faraday (>= 0.17.3, < 3.0)
google-cloud-errors (1.2.0)
google-cloud-storage (1.37.0)
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.2.0)
faraday (>= 0.17.3, < 3.a)
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.5)
domain_name (~> 0.5)
httpclient (2.8.3)
jmespath (1.6.1)
json (2.6.2)
jwt (2.4.1)
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.7)
rake (13.0.6)
representable (3.2.0)
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.17.0)
addressable (~> 2.8)
faraday (>= 0.17.5, < 3.a)
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.2)
unicode-display_width (1.8.0)
webrick (1.7.0)
word_wrap (1.0.0)
xcodeproj (1.22.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
View File
@@ -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.
+29 -18
View File
@@ -1,16 +1,29 @@
<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)
[![GitHub release (latest SemVer including pre-releases)](https://img.shields.io/github/v/release/d4rken-org/capod?include_prereleases)](https://github.com/d4rken-org/capod/releases/latest)
[![Code tests & eval](https://github.com/d4rken-org/capod/actions/workflows/code-checks.yml/badge.svg)](https://github.com/d4rken/capod/actions/workflows/code-checks.yml)
[![Crowdin](https://badges.crowdin.net/capod/localized.svg)](https://crowdin.com/project/capod)
[![Github All Releases](https://img.shields.io/github/downloads/d4rken-org/capod/total.svg)](https://github.com/d4rken-org/capod/releases/latest)
![Build](https://github.com/d4rken/capod/actions/workflows/android.yml/badge.svg) A companion app that adds support for AirPod specific features to Android:
[![Github All Releases](https://img.shields.io/github/downloads/d4rken/capod/total.svg)](https://github.com/d4rken/capod/releases/latest)
A companion app that adds support for AirPod specific features to Android. * Battery level for pods & cases.
* Charging status for pods & case.
* Additional infos about connection, microphone & case.
* Can receive and show all nearby devices.
* Ear detection with automatic play/pause.
* Automatically connect phone & AirPods.
* Show popup when case is opened.
* Support for Wear OS
<img src="https://github.com/d4rken/capod/raw/master/.assets/banner.png" width="400"> 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
@@ -20,28 +33,26 @@ Currently supported models:
* Beats X * Beats X
* Beats Flex * Beats Flex
## Support the project
* 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/airpod-companion)
* [Buy me a coffee](https://www.buymeacoffee.com/tydarken)
## 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)
* [IzzyOnDroid](https://apt.izzysoft.de/fdroid/index/apk/eu.darken.capod)
## Support the project
* Buy the CAPod Pro In-App purchase on [Google Play](https://play.google.com/store/apps/details?id=eu.darken.capod)
* [Sponsor development](https://github.com/sponsors/d4rken) on GitHub
* Help translate CAPod [on Crowdin](https://crowdin.com/project/capod)
## 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)
## Screenshots ## Screenshots
<img src="https://github.com/d4rken/capod/raw/master/.assets/screenshots/1.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">
<img src="https://github.com/d4rken/capod/raw/master/.assets/screenshots/2.png" width="200">
<img src="https://github.com/d4rken/capod/raw/master/.assets/screenshots/3.png" width="200">
<img src="https://github.com/d4rken/capod/raw/master/.assets/screenshots/4.png" width="200">
## Thanks to ## Thanks to
+1
View File
@@ -0,0 +1 @@
/build
+100
View File
@@ -0,0 +1,100 @@
plugins {
id("com.android.library")
id("kotlin-android")
id("kotlin-kapt")
id("kotlin-parcelize")
}
apply(plugin = "dagger.hilt.android.plugin")
android {
compileSdk = ProjectConfig.compileSdk
defaultConfig {
minSdk = ProjectConfig.minSdk
targetSdk = ProjectConfig.targetSdk
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles("consumer-rules.pro")
buildConfigField("Long", "VERSION_CODE", "${ProjectConfig.Version.code}L")
buildConfigField("String", "VERSION_NAME", "\"${ProjectConfig.Version.name}\"")
buildConfigField("String", "APPLICATION_ID", "\"${ProjectConfig.packageName}\"")
buildConfigField("String", "GITSHA", "\"${lastCommitHash()}\"")
buildConfigField("String", "BUILDTIME", "\"${buildTime()}\"")
}
buildFeatures {
viewBinding = true
}
compileOptions {
isCoreLibraryDesugaringEnabled = true
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
freeCompilerArgs = freeCompilerArgs + listOf(
"-opt-in=kotlin.ExperimentalStdlibApi",
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
"-opt-in=kotlin.time.ExperimentalTime",
"-opt-in=kotlin.ExperimentalUnsignedTypes",
)
}
flavorDimensions.add("version")
productFlavors {
create("foss") {
dimension = "version"
}
create("gplay") {
dimension = "version"
}
}
buildTypes {
val customProguardRules = fileTree(File("../proguard")) {
include("*.pro")
}
debug {
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"))
proguardFiles(*customProguardRules.toList().toTypedArray())
proguardFiles("proguard-rules-debug.pro")
}
create("beta") {
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"))
proguardFiles(*customProguardRules.toList().toTypedArray())
}
release {
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"))
proguardFiles(*customProguardRules.toList().toTypedArray())
}
}
testOptions {
unitTests {
isIncludeAndroidResources = true
}
tasks.withType<Test> {
useJUnitPlatform()
}
}
}
dependencies {
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.1.5")
addBaseAndroid()
addBaseAndroidUi()
addBaseKotlin()
addDagger()
addMoshi()
addBaseWorkManager()
addNavigation()
addTesting()
implementation("com.bugsnag:bugsnag-android:5.9.2")
}
View File
+21
View File
@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
+4
View File
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="eu.darken.capod.common">
</manifest>
@@ -0,0 +1,45 @@
package eu.darken.capod.common
// Can't be const because that prevents them from being mocked in tests
@Suppress("MayBeConstant")
object BuildConfigWrap {
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: String = BuildConfig.APPLICATION_ID
val VERSION_CODE: Long = BuildConfig.VERSION_CODE.toLong()
val VERSION_NAME: String = BuildConfig.VERSION_NAME
val GIT_SHA: String = BuildConfig.GITSHA
val BUILDTIME: String = BuildConfig.BUILDTIME
val VERSION_DESCRIPTION_LONG: String = "v$VERSION_NAME ($VERSION_CODE) [$GIT_SHA] ${FLAVOR}_$BUILD_TYPE"
val VERSION_DESCRIPTION_SHORT: String = "v$VERSION_NAME [$GIT_SHA] $FLAVOR"
val VERSION_DESCRIPTION_TINY: String = "v$VERSION_NAME"
}
@@ -13,6 +13,6 @@ object BuildWrap {
} }
} }
fun hasApiLevel(level: Int): Boolean = BuildWrap.VERSION.SDK_INT >= level fun hasApiLevel(level: Int): Boolean = BuildWrap.VersionWrap.SDK_INT >= level
fun withinApiLevel(start: Int, end: Int): Boolean = BuildWrap.VERSION.SDK_INT in start..end fun withinApiLevel(start: Int, end: Int): Boolean = BuildWrap.VersionWrap.SDK_INT in start..end
@@ -0,0 +1,20 @@
package eu.darken.capod.common
import java.util.*
fun Byte.toHex(): String = String.format("%02X", this)
fun UByte.toHex(): String = this.toByte().toHex()
val Byte.upperNibble get() = (this.toInt() shr 4 and 0b1111).toShort()
val Byte.lowerNibble get() = (this.toInt() and 0b1111).toShort()
val UByte.upperNibble get() = (this.toInt() shr 4 and 0b1111).toUShort()
val UByte.lowerNibble get() = (this.toInt() and 0b1111).toUShort()
fun Byte.isBitSet(pos: Int): Boolean = BitSet.valueOf(arrayOf(this).toByteArray()).get(pos)
fun UByte.isBitSet(pos: Int): Boolean = this.toByte().isBitSet(pos)
fun Short.isBitSet(pos: Int): Boolean = this.toByte().isBitSet(pos)
fun UShort.isBitSet(pos: Int): Boolean = this.toShort().isBitSet(pos)
fun UShort.toBinaryString(): String = Integer.toBinaryString(this.toInt()).padStart(4, '0')
fun UByte.toBinaryString(): String = Integer.toBinaryString(this.toInt()).padStart(8, '0')
@@ -0,0 +1,5 @@
package eu.darken.capod.common
object PrivacyPolicy {
const val URL = "https://github.com/d4rken-org/capod/blob/main/PRIVACY_POLICY.md"
}
@@ -3,14 +3,19 @@ package eu.darken.capod.common.bluetooth
import android.bluetooth.le.ScanResult import android.bluetooth.le.ScanResult
import android.os.Parcelable import android.os.Parcelable
import androidx.core.util.forEach import androidx.core.util.forEach
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import java.time.Instant
@Parcelize @Parcelize
@JsonClass(generateAdapter = true)
data class BleScanResult( data class BleScanResult(
val address: String, @Json(name = "receivedAt") val receivedAt: Instant,
val rssi: Int, @Json(name = "address") val address: String,
val generatedAtNanos: Long, @Json(name = "rssi") val rssi: Int,
val manufacturerSpecificData: Map<Int, ByteArray> @Json(name = "generatedAtNanos") val generatedAtNanos: Long,
@Json(name = "manufacturerSpecificData") val manufacturerSpecificData: Map<Int, ByteArray>
) : Parcelable { ) : Parcelable {
fun getManufacturerSpecificData(id: Int): ByteArray? = manufacturerSpecificData[id] fun getManufacturerSpecificData(id: Int): ByteArray? = manufacturerSpecificData[id]
@@ -25,6 +30,7 @@ data class BleScanResult(
companion object { companion object {
fun fromScanResult(scanResult: ScanResult) = BleScanResult( fun fromScanResult(scanResult: ScanResult) = BleScanResult(
receivedAt = Instant.now(),
address = scanResult.device.address, address = scanResult.device.address,
rssi = scanResult.rssi, rssi = scanResult.rssi,
generatedAtNanos = scanResult.timestampNanos, generatedAtNanos = scanResult.timestampNanos,
@@ -0,0 +1,151 @@
package eu.darken.capod.common.bluetooth
import android.annotation.SuppressLint
import android.bluetooth.le.ScanCallback
import android.bluetooth.le.ScanFilter
import android.bluetooth.le.ScanResult
import android.bluetooth.le.ScanSettings
import android.content.Context
import dagger.hilt.android.qualifiers.ApplicationContext
import eu.darken.capod.common.debug.logging.Logging.Priority.*
import eu.darken.capod.common.debug.logging.log
import eu.darken.capod.common.debug.logging.logTag
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class BleScanner @Inject constructor(
@ApplicationContext private val context: Context,
private val bluetoothManager: BluetoothManager2,
private val fakeBleData: FakeBleData,
) {
@SuppressLint("MissingPermission") fun scan(
filters: Set<ScanFilter>,
scannerMode: ScannerMode,
compatMode: Boolean,
): Flow<List<BleScanResult>> = callbackFlow {
log(TAG, VERBOSE) { "scan(filters=$filters, scannerMode=$scannerMode, compatMode=$compatMode)" }
if (compatMode) log(TAG, WARN) { "Using compatibilityMode!" }
val adapter = bluetoothManager.adapter
val supportsOffloadFiltering = adapter.isOffloadedFilteringSupported.also {
log(TAG, if (it) DEBUG else WARN) { "isOffloadedFilteringSupported=$it" }
} && !compatMode
val supportsOffloadBatching = adapter.isOffloadedScanBatchingSupported.also {
log(TAG, if (it) DEBUG else WARN) { "isOffloadedScanBatchingSupported=$it" }
} && !compatMode
val scanner = bluetoothManager.scanner
val callback = object : ScanCallback() {
var lastScanAt = System.currentTimeMillis()
override fun onScanResult(callbackType: Int, result: ScanResult) {
log(TAG, VERBOSE) {
val delay = System.currentTimeMillis() - lastScanAt
lastScanAt = System.currentTimeMillis()
"onScanResult(delay=${delay}ms, callbackType=$callbackType, result=$result)"
}
val toSend = if (
supportsOffloadFiltering
|| filters.isEmpty()
|| filters.any { it.matchesSafe(result) }
) {
listOf(BleScanResult.fromScanResult(result))
} else {
log(TAG, VERBOSE) { "Manual filtering: No match for $result" }
emptyList()
}
trySend(toSend)
}
override fun onBatchScanResults(results: MutableList<ScanResult>) {
log(TAG, VERBOSE) {
val delay = System.currentTimeMillis() - lastScanAt
lastScanAt = System.currentTimeMillis()
"onBatchScanResults(delay=${delay}ms, results=$results)"
}
val toSend = results
.filter { result ->
val passed = when {
supportsOffloadFiltering -> true
filters.isEmpty() -> true
else -> filters.any { it.matches(result) }
}
if (!passed) log(TAG, VERBOSE) { "Manually filtered $result" }
passed
}
.map { BleScanResult.fromScanResult(it) }
trySend(toSend)
}
override fun onScanFailed(errorCode: Int) {
log(TAG, WARN) { "onScanFailed(errorCode=$errorCode)" }
}
}
val settings = ScanSettings.Builder().apply {
setScanMode(
when (scannerMode) {
ScannerMode.LOW_POWER -> ScanSettings.SCAN_MODE_LOW_POWER
ScannerMode.BALANCED -> ScanSettings.SCAN_MODE_BALANCED
ScannerMode.LOW_LATENCY -> ScanSettings.SCAN_MODE_LOW_LATENCY
}
)
if (supportsOffloadBatching) {
setReportDelay(
when (scannerMode) {
ScannerMode.LOW_POWER -> 2000L
ScannerMode.BALANCED -> 1000L
ScannerMode.LOW_LATENCY -> 500L
}
)
}
}.build()
log(TAG, VERBOSE) { "Settings created for offloaded filtering: $settings" }
val flushJob = launch {
log(TAG) { "Flush job launched" }
while (isActive) {
// Can undercut the minimum setReportDelay(), e.g. 5000ms on a Pixel5@12
log(TAG, VERBOSE) { "Flushing scan results." }
adapter.bluetoothLeScanner.flushPendingScanResults(callback)
when (scannerMode) {
ScannerMode.LOW_POWER -> break
ScannerMode.BALANCED -> delay(1000)
ScannerMode.LOW_LATENCY -> delay(500)
}
}
}
scanner.startScan(
if (supportsOffloadFiltering) filters.toList() else listOf(ScanFilter.Builder().build()),
settings,
callback
)
log(TAG) { "BleScanner started (filters=$filters, settings=$settings)" }
awaitClose {
flushJob.cancel()
scanner.stopScan(callback)
log(TAG) { "BleScanner stopped" }
}
}
.map { fakeBleData.maybeAddfakeData(it) }
companion object {
private val TAG = logTag("Bluetooth", "BleScanner")
}
}
@@ -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
}
@@ -0,0 +1,184 @@
package eu.darken.capod.common.bluetooth
import android.bluetooth.*
import android.bluetooth.le.BluetoothLeScanner
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Handler
import android.os.HandlerThread
import android.os.ParcelUuid
import dagger.hilt.android.qualifiers.ApplicationContext
import eu.darken.capod.common.coroutine.DispatcherProvider
import eu.darken.capod.common.debug.Bugs
import eu.darken.capod.common.debug.logging.Logging.Priority.*
import eu.darken.capod.common.debug.logging.log
import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.pods.core.apple.protocol.ContinuityProtocol
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import java.io.IOException
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class BluetoothManager2 @Inject constructor(
private val manager: BluetoothManager,
@ApplicationContext private val context: Context,
private val dispatcherProvider: DispatcherProvider,
) {
val adapter: BluetoothAdapter
get() = manager.adapter
val scanner: BluetoothLeScanner
get() = adapter.bluetoothLeScanner
?: throw IllegalStateException("Bluetooth is disabled or permissiong missing")
val isBluetoothEnabled: Flow<Boolean> = callbackFlow {
send(manager.adapter?.isEnabled ?: false)
val receiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (BluetoothAdapter.ACTION_STATE_CHANGED != intent.action) {
log(TAG) { "Unknown BluetoothAdapter action: $intent" }
return
}
val value = when (intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, -1)) {
BluetoothAdapter.STATE_OFF -> false
BluetoothAdapter.STATE_ON -> true
else -> false
}
trySend(value)
}
}
context.registerReceiver(receiver, IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED))
awaitClose { context.unregisterReceiver(receiver) }
}
fun getBluetoothProfile(profile: Int = BluetoothProfile.HEADSET): Flow<BluetoothProfile2> = callbackFlow {
log(TAG, VERBOSE) { "getBluetoothProfile(profile=$profile)" }
var profileProxy: BluetoothProfile2? = null
manager.adapter.getProfileProxy(context, object : BluetoothProfile.ServiceListener {
override fun onServiceConnected(profile: Int, proxy: BluetoothProfile) {
log(TAG, VERBOSE) { "onServiceConnected(profile=$profile, proxy=$proxy)" }
profileProxy = BluetoothProfile2(
profileType = profile,
profileProxy = proxy,
).also { trySend(it) }
}
override fun onServiceDisconnected(profile: Int) {
log(TAG, WARN) { "onServiceDisconnected(profile=$profile" }
close(IOException("BluetoothProfile service disconnected (profile=$profile)"))
}
}, profile)
awaitClose {
log(TAG) { "Closing BluetoothProfile: $profileProxy" }
profileProxy?.let {
manager.adapter.closeProfileProxy(it.profileType, it.proxy)
}
}
}
private fun monitorDevicesForProfile(
profile: Int = BluetoothProfile.HEADSET
): Flow<Set<BluetoothDevice>> = getBluetoothProfile(profile).flatMapLatest { bluetoothProfile ->
callbackFlow {
log(TAG, VERBOSE) { "connectedDevices(profile=$profile) starting" }
trySend(bluetoothProfile.connectedDevices)
val filter = IntentFilter().apply {
addAction(BluetoothDevice.ACTION_ACL_CONNECTED)
addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED)
}
val handlerThread = HandlerThread("BluetoothEventReceiver").apply {
start()
}
val handler = Handler(handlerThread.looper)
val receiver: BroadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
log(TAG, VERBOSE) { "Bluetooth event (intent=$intent, extras=${intent.extras})" }
val action = intent.action
if (action == null) {
log(TAG, ERROR) { "Bluetooth event without action, how did we get this?" }
return
}
val device = intent.getParcelableExtra<BluetoothDevice?>(BluetoothDevice.EXTRA_DEVICE)
if (device == null) {
log(TAG, ERROR) { "Connection event is missing EXTRA_DEVICE: ${intent.extras}" }
return
}
this@callbackFlow.launch {
val currentDevices = bluetoothProfile.connectedDevices
when (action) {
BluetoothDevice.ACTION_ACL_CONNECTED -> {
log(TAG) { "Adding $device to current devices $currentDevices" }
trySend(currentDevices.plus(device))
}
BluetoothDevice.ACTION_ACL_DISCONNECTED -> {
log(TAG) { "Removing $device from current devices $currentDevices" }
trySend(currentDevices.minus(device))
}
}
}
}
}
context.registerReceiver(receiver, filter, null, handler)
awaitClose {
log(TAG, VERBOSE) { "connectedDevices(profile=$profile) closed." }
context.unregisterReceiver(receiver)
}
}
}
fun connectedDevices(
featureFilter: Set<ParcelUuid> = ContinuityProtocol.BLE_FEATURE_UUIDS
): Flow<List<BluetoothDevice>> = isBluetoothEnabled
.flatMapLatest { monitorDevicesForProfile(BluetoothProfile.HEADSET) }
.map { devices ->
devices.filter { device ->
featureFilter.any { feature ->
device.hasFeature(feature)
}
}
}
fun bondedDevices(): Set<BluetoothDevice> = adapter.bondedDevices
suspend fun nudgeConnection(device: BluetoothDevice): Boolean = getBluetoothProfile().map { bluetoothProfile ->
try {
log(TAG) { "Nudging Android connection to $device" }
val connectMethod = BluetoothHeadset::class.java.getDeclaredMethod(
"connect", BluetoothDevice::class.java
).apply { isAccessible = true }
connectMethod.invoke(bluetoothProfile.proxy, device)
log(TAG) { "Nudged connection to $device" }
true
} catch (e: Exception) {
Bugs.report(tag = TAG, "BluetoothHeadset.connect(device) is unavailable", exception = e)
false
}
}.first()
companion object {
private val TAG = logTag("Bluetooth", "Manager2")
}
}
@@ -0,0 +1,16 @@
package eu.darken.capod.common.bluetooth
import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothProfile
data class BluetoothProfile2(
internal val profileType: Int,
private val profileProxy: BluetoothProfile,
) {
val proxy: BluetoothProfile
get() = profileProxy
val connectedDevices: Set<BluetoothDevice>
get() = proxy.connectedDevices.toSet()
}
@@ -0,0 +1,124 @@
package eu.darken.capod.common.bluetooth
import dagger.Reusable
import eu.darken.capod.common.SystemClockWrap
import eu.darken.capod.common.debug.autoreport.DebugSettings
import java.time.Instant
import javax.inject.Inject
import kotlin.random.Random
@Reusable
class FakeBleData @Inject constructor(
private val debugSettings: DebugSettings,
) {
fun maybeAddfakeData(originals: List<BleScanResult>): List<BleScanResult> {
if (!debugSettings.showFakeData.value) return originals
return originals + getFakeData()
}
fun getFakeData(): Collection<BleScanResult> {
val fakeDevices = mutableListOf<BleScanResult>()
// AirPods Gen1
BleScanResult(
receivedAt = Instant.now(),
address = "78:73:AF:B4:85:22",
rssi = Random.nextInt(100) * -1,
generatedAtNanos = SystemClockWrap.elapsedRealtimeNanos + 100,
manufacturerSpecificData = mapOf(76 to "07 19 01 02 20 75 AA B6 31 00 05 9C 5A A4 5D C0 2C A0 B4 6F B9 ED 8E CE 03 97 CA".hexToByteArray())
).run {
if (Random.nextBoolean()) {
fakeDevices.add(this)
}
}
// AirPods Gen2
BleScanResult(
receivedAt = Instant.now(),
address = "78:73:FF:B4:85:5E",
rssi = Random.nextInt(100) * -1,
generatedAtNanos = SystemClockWrap.elapsedRealtimeNanos + 100,
manufacturerSpecificData = mapOf(76 to "07 19 01 0F 20 75 AA B6 31 00 05 9C 5A A4 5D C0 2C A0 B4 6F B9 ED 8E CE 03 97 CA".hexToByteArray())
).run {
if (Random.nextBoolean()) {
fakeDevices.add(this)
}
}
// AirPods Gen3
BleScanResult(
receivedAt = Instant.now(),
address = "4E:9E:D1:49:D2:6D",
rssi = Random.nextInt(15, 75) * -1,
generatedAtNanos = SystemClockWrap.elapsedRealtimeNanos + 200,
manufacturerSpecificData = mapOf(76 to "07 19 01 13 20 55 AF 56 31 00 06 6F E4 DF 10 AF 10 60 81 03 3B 76 D9 C7 11 22 88".hexToByteArray())
).run {
if (Random.nextBoolean()) {
fakeDevices.add(this)
}
}
// AirPods Max
BleScanResult(
receivedAt = Instant.now(),
address = "7E:E5:C7:65:D2:B5",
rssi = Random.nextInt(15, 75) * -1,
generatedAtNanos = SystemClockWrap.elapsedRealtimeNanos + 300,
manufacturerSpecificData = mapOf(76 to "07 19 01 0A 20 02 05 80 04 0F 44 A7 60 9B F8 3C FD B1 D8 1C 61 EA 82 60 A3 2C 4E".hexToByteArray())
).run {
if (Random.nextBoolean()) {
fakeDevices.add(this)
}
}
// BeatsFlex
BleScanResult(
receivedAt = Instant.now(),
address = "5E:9E:D1:49:D2:6D",
rssi = Random.nextInt(15, 75) * -1,
generatedAtNanos = SystemClockWrap.elapsedRealtimeNanos + 400,
manufacturerSpecificData = mapOf(76 to "07 19 01 10 20 0A F4 8F 00 01 00 C4 71 9F 9C EF A2 E3 BA 66 FE 1D 45 9F C9 2F A0".hexToByteArray())
).run {
if (Random.nextBoolean()) {
fakeDevices.add(this)
}
}
// Tws i99999
BleScanResult(
receivedAt = Instant.now(),
address = "5E:9E:D1:29:D2:6D",
rssi = Random.nextInt(15, 75) * -1,
generatedAtNanos = SystemClockWrap.elapsedRealtimeNanos + 400,
manufacturerSpecificData = mapOf(76 to "07 13 01 02 20 71 AA 37 32 00 10 00 64 64 FF 00 00 00 00 00 00".hexToByteArray())
).run {
if (Random.nextBoolean()) {
fakeDevices.add(this)
}
}
// Unknown Device
BleScanResult(
receivedAt = Instant.now(),
address = "6E:9E:D1:49:D2:6D",
rssi = Random.nextInt(15, 75) * -1,
generatedAtNanos = SystemClockWrap.elapsedRealtimeNanos + 500,
manufacturerSpecificData = mapOf(76 to "07 19 01 FF 20 0A F4 8F 00 01 00 C4 71 9F 9C EF A2 E3 BA 66 FE 1D 45 9F C9 2F A0".hexToByteArray())
).run {
if (Random.nextBoolean()) {
fakeDevices.add(this)
}
}
return fakeDevices
}
private fun String.hexToByteArray(): ByteArray {
val trimmed = this
.replace(" ", "")
.replace(">", "")
.replace("<", "")
require(trimmed.length % 2 == 0) { "Not a HEX string" }
return trimmed.chunked(2).map { it.toInt(16).toByte() }.toByteArray()
}
}
@@ -1,9 +1,9 @@
package eu.darken.capod.main.core package eu.darken.capod.common.bluetooth
import androidx.annotation.StringRes import androidx.annotation.StringRes
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import eu.darken.capod.R import eu.darken.capod.common.R
@JsonClass(generateAdapter = false) @JsonClass(generateAdapter = false)
enum class ScannerMode( enum class ScannerMode(
@@ -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,8 +4,9 @@ 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.androidstarter.common.preferences.Settings 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.createFlowPreference import eu.darken.capod.common.preferences.createFlowPreference
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@@ -17,15 +18,21 @@ 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)
val showUnfiltered = preferences.createFlowPreference("debug.blescanner.unfiltered.enabled", false)
override val preferenceDataStore: PreferenceDataStore = PreferenceStoreMapper( override val preferenceDataStore: PreferenceDataStore = PreferenceStoreMapper(
isDebugModeEnabled, isDebugModeEnabled,
showFakeData, showFakeData,
showUnfiltered,
) )
} }
@@ -6,7 +6,6 @@ import android.content.pm.PackageManager
import com.bugsnag.android.Event import com.bugsnag.android.Event
import com.bugsnag.android.OnErrorCallback import com.bugsnag.android.OnErrorCallback
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import eu.darken.capod.BuildConfig
import eu.darken.capod.common.BuildConfigWrap import eu.darken.capod.common.BuildConfigWrap
import eu.darken.capod.common.debug.autoreport.DebugSettings import eu.darken.capod.common.debug.autoreport.DebugSettings
import eu.darken.capod.common.debug.logging.Logging.Priority.WARN import eu.darken.capod.common.debug.logging.Logging.Priority.WARN
@@ -26,13 +25,13 @@ class BugsnagErrorHandler @Inject constructor(
bugsnagLogger.injectLog(event) bugsnagLogger.injectLog(event)
TAB_APP.also { tab -> TAB_APP.also { tab ->
event.addMetadata(tab, "gitSha", BuildConfig.GITSHA) event.addMetadata(tab, "gitSha", BuildConfigWrap.GIT_SHA)
event.addMetadata(tab, "buildTime", BuildConfig.BUILDTIME) event.addMetadata(tab, "buildTime", BuildConfigWrap.BUILDTIME)
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 && !eu.darken.capod.common.BuildConfigWrap.DEBUG
} }
companion object { companion object {
@@ -1,7 +1,7 @@
package eu.darken.capod.common.error package eu.darken.capod.common.error
import android.content.Context import android.content.Context
import eu.darken.capod.R import eu.darken.capod.common.R
interface HasLocalizedError { interface HasLocalizedError {
fun getLocalizedError(context: Context): LocalizedError fun getLocalizedError(context: Context): LocalizedError
@@ -25,7 +25,8 @@ class DynamicStateFlow<T>(
loggingTag: String? = null, loggingTag: String? = null,
parentScope: CoroutineScope, parentScope: CoroutineScope,
coroutineContext: CoroutineContext = parentScope.coroutineContext, coroutineContext: CoroutineContext = parentScope.coroutineContext,
private val startValueProvider: suspend CoroutineScope.() -> T private val onRelease: CoroutineScope.(T) -> Unit = {},
private val startValueProvider: suspend CoroutineScope.() -> T,
) { ) {
private val lTag = loggingTag?.let { "$it:DSFlow" } private val lTag = loggingTag?.let { "$it:DSFlow" }
@@ -47,6 +48,12 @@ class DynamicStateFlow<T>(
} }
} }
invokeOnClose {
lTag?.let { log(it, VERBOSE) { "invokeOnClose executing..." } }
onRelease(currentValue)
lTag?.let { log(it, VERBOSE) { "internal channelFlow finished." } }
}
updateActions.collect { update -> updateActions.collect { update ->
currentValue = valueGuard.withLock { currentValue = valueGuard.withLock {
try { try {
@@ -68,12 +75,11 @@ class DynamicStateFlow<T>(
} }
} }
} }
lTag?.let { log(it, VERBOSE) { "internal channelFlow finished." } }
} }
private val internalFlow = producer private val internalFlow = producer
.onStart { lTag?.let { log(it, VERBOSE) { "Internal onStart" } } } .onStart { lTag?.let { log(it, VERBOSE) { "Internal onStart" } } }
// .onEach { value -> lTag?.let { log(it, VERBOSE) { "New value: $value" } } }
.onCompletion { err -> .onCompletion { err ->
when { when {
err is CancellationException -> { err is CancellationException -> {
@@ -34,13 +34,13 @@ fun <T : Any> Flow<T>.shareLatest(
) )
.filterNotNull() .filterNotNull()
fun <T : Any> Flow<T>.replayingShare(scope: CoroutineScope) = this.shareIn( fun <T : Any?> Flow<T>.replayingShare(scope: CoroutineScope) = this.shareIn(
scope = scope, scope = scope,
replay = 1, replay = 1,
started = SharingStarted.WhileSubscribed(replayExpiration = Duration.ZERO) started = SharingStarted.WhileSubscribed(replayExpiration = Duration.ZERO)
) )
internal fun <T> Flow<T>.withPrevious(): Flow<Pair<T?, T>> = this fun <T> Flow<T>.withPrevious(): Flow<Pair<T?, T>> = this
.scan(Pair<T?, T?>(null, null)) { previous, current -> Pair(previous.second, current) } .scan(Pair<T?, T?>(null, null)) { previous, current -> Pair(previous.second, current) }
.drop(1) .drop(1)
.map { .map {
@@ -6,8 +6,8 @@ import android.os.Build
import android.os.PowerManager import android.os.PowerManager
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import eu.darken.capod.R
import eu.darken.capod.common.BuildConfigWrap import eu.darken.capod.common.BuildConfigWrap
import eu.darken.capod.common.R
import eu.darken.capod.common.withinApiLevel import eu.darken.capod.common.withinApiLevel
enum class Permission( enum class Permission(
@@ -62,6 +62,15 @@ enum class Permission(
val pwm = it.getSystemService(Context.POWER_SERVICE) as PowerManager val pwm = it.getSystemService(Context.POWER_SERVICE) as PowerManager
pwm.isIgnoringBatteryOptimizations(BuildConfigWrap.APPLICATION_ID) pwm.isIgnoringBatteryOptimizations(BuildConfigWrap.APPLICATION_ID)
}, },
),
SYSTEM_ALERT_WINDOW(
minApiLevel = Build.VERSION_CODES.BASE,
labelRes = R.string.permission_system_alert_window_label,
descriptionRes = R.string.permission_system_alert_window_description,
permissionId = "android.permission.SYSTEM_ALERT_WINDOW",
isGranted = {
android.provider.Settings.canDrawOverlays(it)
},
) )
} }
@@ -2,7 +2,7 @@ package eu.darken.capod.common.preferences
import android.content.SharedPreferences import android.content.SharedPreferences
import androidx.core.content.edit import androidx.core.content.edit
import eu.darken.capod.common.debug.logging.Logging.Priority.* 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 kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
@@ -1,4 +1,4 @@
package eu.darken.androidstarter.common.preferences package eu.darken.capod.common.preferences
import android.content.SharedPreferences import android.content.SharedPreferences
import androidx.preference.PreferenceDataStore import androidx.preference.PreferenceDataStore
@@ -9,5 +9,5 @@ class JavaInstantAdapter {
fun toJson(obj: Instant): Long = obj.toEpochMilli() fun toJson(obj: Instant): Long = obj.toEpochMilli()
@FromJson @FromJson
fun fromJson(epochMillis: Long): Instant = Instant.ofEpochSecond(epochMillis) fun fromJson(epochMillis: Long): Instant = Instant.ofEpochMilli(epochMillis)
} }
@@ -8,7 +8,8 @@ import androidx.fragment.app.Fragment
import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import androidx.viewbinding.ViewBinding import androidx.viewbinding.ViewBinding
import eu.darken.capod.common.debug.logging.Logging.Priority.* import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE
import eu.darken.capod.common.debug.logging.Logging.Priority.WARN
import eu.darken.capod.common.debug.logging.log import eu.darken.capod.common.debug.logging.log
import kotlin.properties.ReadOnlyProperty import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty import kotlin.reflect.KProperty
@@ -5,9 +5,10 @@ import android.content.SharedPreferences
import androidx.preference.PreferenceDataStore import androidx.preference.PreferenceDataStore
import com.squareup.moshi.Moshi import com.squareup.moshi.Moshi
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import eu.darken.androidstarter.common.preferences.Settings import eu.darken.capod.common.bluetooth.ScannerMode
import eu.darken.capod.common.debug.autoreport.DebugSettings import eu.darken.capod.common.debug.autoreport.DebugSettings
import eu.darken.capod.common.preferences.PreferenceStoreMapper import eu.darken.capod.common.preferences.PreferenceStoreMapper
import eu.darken.capod.common.preferences.Settings
import eu.darken.capod.common.preferences.createFlowPreference import eu.darken.capod.common.preferences.createFlowPreference
import eu.darken.capod.pods.core.PodDevice import eu.darken.capod.pods.core.PodDevice
import javax.inject.Inject import javax.inject.Inject
@@ -30,10 +31,15 @@ class GeneralSettings @Inject constructor(
val scannerMode = preferences.createFlowPreference( val scannerMode = preferences.createFlowPreference(
"core.scanner.mode", "core.scanner.mode",
ScannerMode.BALANCED, ScannerMode.LOW_LATENCY,
moshi moshi
) )
val compatibilityMode = preferences.createFlowPreference(
"core.compatibility.enabled",
false
)
val showAll = preferences.createFlowPreference( val showAll = preferences.createFlowPreference(
"core.showall.enabled", "core.showall.enabled",
false false
@@ -58,9 +64,10 @@ class GeneralSettings @Inject constructor(
override val preferenceDataStore: PreferenceDataStore = PreferenceStoreMapper( override val preferenceDataStore: PreferenceDataStore = PreferenceStoreMapper(
monitorMode, monitorMode,
scannerMode, scannerMode,
compatibilityMode,
showAll, showAll,
minimumSignalQuality, minimumSignalQuality,
mainDeviceAddress, mainDeviceAddress,
debugSettings.isAutoReportEnabled, debugSettings.isAutoReportingEnabled,
) )
} }
@@ -3,7 +3,7 @@ package eu.darken.capod.main.core
import androidx.annotation.StringRes import androidx.annotation.StringRes
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import eu.darken.capod.R import eu.darken.capod.common.R
@JsonClass(generateAdapter = false) @JsonClass(generateAdapter = false)
enum class MonitorMode( enum class MonitorMode(

Some files were not shown because too many files have changed in this diff Show More